DDD实战:应对并发挑战,五个技巧让你轻松应对
当前位置:点晴教程→知识管理交流
→『 技术文档交流 』
并发管理是一个高级话题,也是设计中的难点,一不小心就会出问题。让每个开发人员都成为并发高手又是一件不太现实的事,但,好在存在很多并发管理的成熟方案,业务开发者按照场景进行落地即可。
在业务开发中,事务一致性核心在于“原子性”,则并发管理的核心在于“隔离性”。
1. 无处不在的并发并发管理是指在多个用户同时访问、修改同一数据时,如何保证数据的准确性、一致性和完整性的一系列管理措施。 并发无处不在是指在当前的业务系统和应用程序中,几乎所有的操作都是并发的。无论是网络请求、数据库操作、I/O读写操作等,都可能在同一时刻被多个线程或进程同时执行。这意味着在业务开发中,必须充分考虑并发处理问题,避免出现数据竞争、死锁等问题,同时合理利用多线程、协程等技术来提高系统的性能和处理能力。 1.1. 常见业务流程首先看以下流程: 图片 这是一个聚合根更新操作,包括:
也许还没有使用DDD,对聚合根不太熟悉,那再看一个流程: 图片 这是一个更为通用的数据编辑流程,包括:
仔细对比这两张图,其实他们都在做同样的事情:
在这里便存在并发问题。 1.2. 并发问题上面所提到的流程是否存在并发问题,仔细看下图: 图片 同一个流程,操作同一数据,只是操作顺序不同,也会出现并发安全问题:
看起来没什么问题,但 V3 是业务期望的吗?V2 的变更又去哪里了呢? 此时,V2 被 V3 覆盖,V2 的变更丢失了。 如果还不清楚,明确业务操作为 count++,如下图所示: 图片 对数据库的 count 进行累加操作
操作完成后,最终结果为2。实际期望结果为3,Action2 的修改被 Action1 覆盖,导致一次累加操作被覆盖。 当然,这仅仅是同一流程下的并发问题,多流程间也存在并发问题: 图片 对于同一记录,自增流程和设置流程并发执行,同样发生了写覆盖。 2. 局部串行
2.1. 线程方案如下图所示: 图片 订单流程中的核心操作:
由于多个订单间不存在关系,可以并发执行;但同一订单,必须保障业务执行顺序。 什么是“局部串行”:
其中分发器是核心,它连接订单事件和后台线程:
这样,相同订单号的订单事件均由同一个线程处理,从而保证局部串行化。不同订单之间,不存在相互影响,可以在多个线程中并行执行。 2.2. MQ 方案当然,内存操作存在数据安全问题(重启任务会丢失),不少MQ也提供了相关功能,以 RocketMQ 的顺序消息为例,如下图所示: 图片
局部串行对性能存在一定影响,系统最大的并发量为 partition 数量。如果出现增加 Worker 节点无法提升系统吞吐时,需要扩展 partition 数量。 【备注】在系统做 rebalance 时,可能会出现短暂的消息混乱,通常情况下,业务是可接受的。如果必须保障强顺序,如 binlog 场景,只能使用一个 partition,但会极大的影响性能。 3. 最后写胜出
如下图所示: 图片
此时,不会出现并发问题。但由于时序问题,数据的最终状态以“最后更新”为准。 4. 原子指令
比如在库存扣减的场景,可以使用 Redis 或 DB 的原子指令进行操作。 4.1. Redis使用 Redis 的 incr 指令: 图片 由于 redis 指令是单线程处理不存在并发问题,直接使用 incr key -1 质量对数量进行扣减。当然,这样可能会出现数量为负值情况,此时可以引入 LUA 脚本进行保障: -- KEYS[1]: 库存键的名称,例如 stock:1001 -- ARGV[1]: 要扣减的数量 local stock = tonumber(redis.call('GET', KEYS[1])) -- 判断扣减的数量是否大于库存数量 if stock < tonumber(ARGV[1]) then return -1 end -- 扣减库存,并返回剩余的库存数量 stock = stock - tonumber(ARGV[1]) redis.call('SET', KEYS[1], stock) -- 返回剩余的库存数量 return stock1.2.3.4.5.6.7.8.9.10.11.12.13.14.15. 4.2. MySQL同样的操作也可以在 MySQL 中操作,如下图所示: 图片 也可避免扣减为 负值的情况,如下图所示: 图片 新增对 count 的条件判断,通过操作结果控制不同的流程:
5. 乐观锁
业务中使用最多的场景仍旧是 读-改-写,此时最佳处理方案便是乐观锁。 图片 相对于数据更新,乐观锁方案只是增加了 version 判断,并未引入其他复杂性,对性能影响非常小。
对于聚合根来说,这是数据更新最常见的并发保障机制。 6. 悲观锁当一个事务(线程)正在使用某个数据时,其他事务(线程)就不能访问该数据,必须等待锁释放后才能访问。悲观锁能够保证数据的一致性,但是对并发性能影响比较大。 悲观锁是最后的办法,由于其对性能冲击较大,不到万不得已不要随便使用。 6.1. 数据库悲观锁
使用 for update 加载数据,操作如下: 图片 for update 语句将对数据进行强制加锁,只有在事务提交后,锁才会释放。如图所示,for update 会对操作进行强制排序,最终使单条操作变成串行化,从而影响并发度最终影响系统性能。 6.2. 分布式锁
比如,在订单系统中,对于特价商品一个用户只能购买一次,如下图所示: 图片 该流程存在并发问题,可能导致一个用户下单多次:
由于是新增场景,没有什么资源可锁定,所以乐观锁方案无法落地,此时就需要引入分布式锁,如下图所示: 图片 以 user 为单位申请分布式锁,保证同一用户只有一个线程能进行被保护流程,从而保证同一用户不会购买多次。 4. 小结并发管理是一个高级话题,也是设计中的难点,一不小心就会出问题。让每个开发人员都成为并发高手又是一件不太现实的事,但,好在存在很多并发管理的成熟方案,业务开发者按照场景进行落地即可:
责任编辑:武晓燕来源: geekhalo
该文章在 2023/10/28 10:37:26 编辑过 |
关键字查询
相关文章
正在查询... |