增删改查是大部分框架的功能,如果有两个并发请求修改同一个数据,这个时候,你会怎么办?或者插入本来应该是唯一却重复的数据时应该怎么办?再或者插入和修改时有其它辅助动作比如保存到另外的表,这些情况,有什么好的解决方案?
我想最开始,你会首先想到“事务”,事务确实能够让一组操作一起可靠安的全执行,他们要么全部执行,要么一个也别想执行。但如果有两个同时发生的并发事务怎么办?使用事务隔离级别,这是ACID中的定义,关系数据库内部机制中就是这么做的。
但是,如果使用隔离级别,比如可串行化serializable (以及可重复读),你的系统会变得很慢,依赖于不同关系数据库,同时发生的事务也许需要应用代码编码指定重试几次,这就很复杂,其他不是很严格的隔离级别则会带来更新丢失或幻读(幻读是不可重复读的一种特殊场景,当事务没有获取范围锁的情况下执行SELECT … WHERE操作就可能会发生幻读)。
即使你正确地设置了合适的隔离级别,你也能用代码正确处理了事务的失败错误情况,但是隔离并不能解决所有并发问题,比如应用级别的数据约束,也就是说,一种复杂的业务逻辑约束或规则,很难使用数据库的表键约束来实现的;单纯使用数据库技术也不能解决重复插入的问题;更不能解决应用级别的并发问题;不能解决数据并发等问题。也许你试图通过获得数据库锁来解决这些问题,但是锁是可怕的,锁有写锁、读锁和排他锁,如何避免死锁?不是每个程序员能够有经验和锁打交道的。
双重提交就是个很好的佐证,它说明了不是所有问题都可以通过数据库方式单独解决的。双重提交很多人的解决办法是:使用一个token代表每个请求,并存储在数据库,使用数据库的唯一键约束,这样,重复记录就无法插入,这种问题使用API比较复杂,因为你得根据API的用户才能产生合适的token。另外,虽然你使用数据库唯一约束,但是还得在应用代码中进行检查,因为两行记录虽然键不同但是值相同还是可能被插入的。
大部分并发是运行在单机上,这可以使用语言的并发特性来确保执行的串行化,双重重复不可能发生,但是当你部署应用在几台机器以上,并发问题变得困难。
下面是不使用事务而使用并发的解决思路:
- 类似Hazelcast之类提供分布式锁,整个集群都遵循锁语义如同单机一样,但是适用场景不多。
- 使用消息队列– 将所有请求推入消息队列,队列会被单个异步worker处理,但是可能不适合业务上需要立即返回给用户的场景。
- 使用Akka和其集群,能保证一个actor(可看成一个服务)一次只处理一个消息,但是因为akka完全改变了使用范式,难以使用和跟踪调试,而且和语言平台特点有关。
- 使用数据库的应用级别锁,比如关系数据库Postgre提供 advisory锁,MySQL也有类似的get_lock。使用关系数据库作为分布式锁机制,锁是被应用管理,不需要表库做任何事,只要请求为entityType, entityId字段请求一个锁,保证没有其他应用线程只有在获得数据库锁的情况下才能执行应用中指定一段代码,相当于用数据库锁替代语言同步锁,然后使用Spring的 @Before 之类AOP方式拦截服务的方法。
- 使用CRDT,它是一种幂等的数据结构,不管操作其之上的操作顺序,最终都是同样的结果状态。但是完全幂等的操作在实际中也是很少碰到。
- 使用“insert-only”只追加模型。像Datomic之类数据库内部使用这种模型,你可以在任何数据库中使用这种模型,只有新增追加,没有删除和更新,每次使用新的版本号插入新记录. 这样版本号的唯一性保证不会有重复记录。你不会丢失数据,相当于免费得到一个校订日志(banq注:实际是EventSourcing 事件流日志)。
上面列出的方法都是在不损失性能情况下如何串行化请求,包括了各种锁机制 队列和非堵塞I/O。
另外,与本文内容无关。再推荐一个开源的并发框架Disruptor,它能够在无锁的情况下实现网络的Queue并发操作。Martin Fowler曾在自己网站上写了一篇LMAX架构的文章,在文章中他介绍了LMAX是一种新型零售金融交易平台,它能够以很低的延迟产生大量交易。这个系统是建立在JVM平台上,其核心是一个业务逻辑处理器,它能够在一个线程里每秒处理6百万订单。业务逻辑处理器完全是运行在内存中,使用事件源驱动方式。而业务逻辑处理器的核心是Disruptor,点击阅读原文链接(送你一个很不错的PPT),了解关于Disruptor的更多信息。
今天再给大家推荐一个不错的网站Jdon.com,JDON自2002年建站以来,内容一直在持续更新,致力于跟踪国际最新软件设计前沿,吸引软件专家和思想领袖参与探索、分享、交流、解惑、传道;主要方向包括有关软件建模、领域驱动设计、设计模式、云计算架构、敏捷开发等方面实战经验和原创思想 。本文也是出自JDON创始人板桥先生之手。