替代传统事务的并发建议

增删改查是大部分框架的功能,如果有两个并发请求修改同一个数据怎么办?或者插入本来应该是唯一却重复的数据怎么办?或者插入和修改有其他辅助动作比如保存到另外的表比如校订审计日志。

你会首先想到“事务”,事务确实能够让一组操作一起可靠安全执行,要么全部执行,要么一个也别想执行,如果有两个同时发生的并发事务怎么办?使用事务隔离级别,这是ACID中的定义,关系数据库内部机制中就是这么做的。

但是,如果使用隔离级别,比如可串行化serializable (以及可重复读),你的系统会变得很慢,依赖于不同关系数据库,同时发生的事务也许需要应用代码编码指定重试几次,这就很复杂,其他不是很严格的隔离级别则会带来更新丢失或幽灵phantom 读。

即使你正确地设置了合适隔离级别,你也能用代码正确处理了事务的失败错误情况,但是隔离并不能解决所有并发问题,比如应用级别的数据约束,也就是说,是一种复杂的业务逻辑约束或规则,很难使用数据库的表键约束来实现的;单纯使用数据库技术也不能解决重复插入的问题;更不能解决应用级别的并发问题;不能解决数据并发等问题。也许你试图通过获得数据库锁来解决这些问题,但是锁是可怕的,锁有写锁 读锁和排他锁,如何避免死锁?不是每个程序员能够有经验和锁打交道的。

双重提交问题是经典问题,它说明了不是所有问题都可以通过数据库方式单独解决的,双重提交很多人的解决办法是:使用一个token代表每个请求,并存储在数据库,使用数据库的唯一键约束,这样,重复记录就无法插入,这种问题使用API比较复杂,因为你得根据API的用户才能产生合适的token。另外,虽然你使用数据库唯一约束,但是还得在应用代码中进行检查,因为两行记录虽然键不同但是值相同还是可能被插入的。

大部分并发是运行在单机上,这可以使用语言的并发特性来确保执行的串行化,双重重复不可能发生,但是当你部署应用在几台机器以上,并发问题变得困难。

下面是不使用事务而使用并发的解决思路:
1.类似Hazelcast之类提供分布式锁,整个集群都遵循锁语义如同单机一样,但是适用场景不多。

2.使用消息队列– 将所有请求推入消息队列,队列会被单个异步worker处理,但是可能不适合业务上需要立即返回给用户的场景。

3. 使用Akka和其集群,能保证一个actor (可看成一个服务)一次只处理一个消息,但是因为akka完全改变了使用范式,难以使用和跟踪调试,而且和语言平台特点有关。

4.使用数据库的应用级别锁,比如关系数据库Postgre提供 advisory锁, MySQL也有类似的get_lock, 使用关系数据库作为分布式锁机制,锁是被应用管理,不需要表库做任何事,只要请求为entityType, entityId字段请求一个锁,保证没有其他应用线程只有在获得数据库锁的情况下才能执行应用中指定一段代码,相当于用数据库锁替代语言同步锁,,然后使用Spring的 @Before 之类AOP方式拦截服务的方法。

5.使用CRDT. 它是一种幂等的数据结构,不管操作其之上的操作顺序,最终都是同样的结果状态。但是完全幂等的操作在实际中也是很少碰到。

6.使用“insert-only”只追加模型. 像Datomic之类数据库内部使用这种模型,你可以在任何数据库中使用这种模型,只有新增追加,没有删除和更新,每次使用新的版本号插入新记录. 这样版本号的唯一性保证不会有重复记录。你不会丢失数据,相当于免费得到一个校订日志(banq注:实际是EventSourcing 事件流日志)

上面办法都是在不损失性能情况下如何串行化请求,包括了各种锁机制 队列和非堵塞I/O。

原文参考:
A Beginner’s Guide to Addressing Concurrency Issue

参考:
微服务的最终一致性与事件流
Reactive响应式编程
[该贴被banq于2016-04-26 14:49修改过]