如何在高并发下尽可能实现无锁编程

一个在线2k游戏,每秒的并发量都很吓人。传统的hibernate直接插件库基本不可行。我一步步推导出一个无锁的数据库操作。

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);

也就是说,作为一个时间序列,可以保证无锁和全局唯一的自增。