如何在高并发下尽可能实现无锁编程
1.如何锁定并发?
一个很简单的想法,把并发变成单线程。Java的颠覆者就是一个很好的例子。如果用java的concurrentCollection类来做,原理就是启动一个线程,运行一个队列。并发时,任务被推入队列,线程轮换读取队列,然后逐个执行。
在这种设计模式下,任何并发都会变成单线程操作,速度非常快。当前的node.js,或者更常见的ARPG服务器,就是用这种“大循环”架构设计的。
这样我们原来的系统就有了两个环境:并发环境和“大循环”环境。
并发环境是我们传统的低性能锁环境。
“大循环”环境是我们用Disruptor开发的单线程无锁环境,性能很强。
2.如何提高“大周期”环境下的处理性能?
一旦并发变成单线程,一旦其中一个线程出现性能问题,整个处理必然变慢。因此,单线程中的任何操作都不得涉及IO处理。数据库操作呢?
增加缓存。这个想法很简单,直接从内存中读取必然很快。至于写和更新操作,我们采用类似的思路,将操作提交到一个队列中,然后单独运行一个线程,一个一个地获取插件。这样可以确保IO操作不参与“大循环”。
问题再次出现:
如果我们的游戏只有大循环,那就很容易解决,因为它提供了完美的同步,没有锁。
但实际上游戏环境是并发和“大循环”的,也就是以上两种环境。那么无论我们怎么设计,都难免会发现缓存上会有锁。
3.并发和“大循环”消除锁有什么区别?
我们知道,如果我们想避免“大循环”中的锁操作,那么我们应该使用“异步”并将操作留给线程。结合这两个特征,我稍微改变了数据库架构。
原始缓存层必然会有锁,例如:
公共TableCache
{
私有散列表& lt字符串,对象& gtcaches = new ConcurrentHashMap & lt字符串,对象& gt();
}
这种结构是不可避免的,保证了缓存在并发环境下能够准确操作。但是,“大循环”不能直接操作这个缓存来修改它,所以必须启动一个线程来更新缓存,例如:
private static final ExecutorService EXECUTOR = executors . newsinglethreadexecutor();
EXECUTOR.execute(新latency processor(logs));
类LatencyProcessor实现Runnable
{
公共无效运行()
{
//你可以在这里随意修改内存数据。采用异步。
}
}
好的,看起来很漂亮。但是另一个问题出现了。在高速访问的过程中,很有可能缓存还没更新就被其他请求取回,得到的是旧数据。
4.如何保证并发环境下缓存数据的唯一性和正确性?
我们知道,如果只有读操作,没有写操作,那么这个行为就不需要被锁定。
我用这个技术,在缓存上面再加一层缓存成为“一级缓存”,原来的自然就成了“二级缓存”。有点像CPU,对吧?
一级缓存只能被“大循环”修改,但是可以被并发和“大循环”同时获取,所以不需要加锁。
当数据库发生变化时,有两种情况:
1)当数据库在并发环境下发生变化时,我们允许锁的存在,所以直接操作二级缓存是没有问题的。
2)当数据库在“大周期”环境下发生变化时,我们先将变化的数据存储在一级缓存中,然后交给异步修正二级缓存,修正后删除一级缓存。
这样,无论在哪个环境下读取数据,都是先判断一级缓存,不再判断二级缓存。
这种架构确保了内存数据的绝对准确性。
而重要的是,我们有一个高效的无锁空间来实现我们任意的业务逻辑。
最后,有一些提高性能的技巧。
1.由于我们的数据库操作是异步处理的,所以有时可能会有大量数据要插入数据库。通过对表、主键、操作类型的排序,我们可以删除一些无效的操作。例如:
a)同一个表和同一个主键的多次更新,最后一次更新。
b)同一个表有相同的主键。只要出现删除,之前的所有操作都无效。
2.既然要对操作进行排序,必然会有按时间排序。怎么才能保证没有锁呢?使用
private final static AtomicLong _ seq = new AtomicLong(0);
也就是说,作为一个时间序列,可以保证无锁和全局唯一的自增。