🍰 个人主页:不摆烂的小劉
🍞文章有不合理的地方请各位大佬指正。
🍉文章不定期持续更新,如果我的文章对你有帮助➡️ 关注🙏🏻 点赞👍 收藏⭐️
《redis数据一致性》讨论了加分布式锁几种方式,保证了数据一致性。
本文继续讨论并发场景下的数据一致性问题,以及常见的解决方式——加锁。而常见的锁又分为乐观锁和悲观锁,如何使用?使用时有什么注意事项?
文章目录
并发场景下的数据一致性问题
1.读异常(脏读、不可重复读、幻读)
由事务间的未提交读、更新或插入操作导致
-
脏读
定义:事务 A 读取到事务 B 未提交的中间数据,若 B 回滚,A 读到的是无效数据;
典型场景:转账时,收款方在转账事务提交前查询余额,若转账回滚则显示错误金额。
解决:设置数据库隔离级别为「读已提交」或更高 -
不可重复读
定义:同一事务内两次读取同一数据,结果不同(因其他事务在期间执行了更新操作)
典型场景:报表统计事务中,两次查询订单总金额时,中间有订单金额被修改。
解决:提升隔离级别至「可重复读」(通过行锁或 MVCC 保证事务内数据版本不变);对读取数据加共享锁(如SELECT ... FOR SHARE
),阻止其他事务修改。 -
幻读:
定义:同一事务内两次执行相同范围查询(如WHERE status=0
),结果集数量或内容不同(因其他事务在期间执行了插入 / 删除操作)
典型场景:库存统计时,两次查询「库存 > 0」的商品,中间有新品上架或商品下架。
解决:使用「可重复读」隔离级别(如MySQL
的RR
级别,通过间隙锁锁定数据范围);对查询加范围排它锁(SELECT ... FOR UPDATE
),阻止并发插入 / 删除。
2.写异常(丢失更新)
由并发修改未加控制导致。
- 丢失更新
两个事务同时读取同一数据并修改,后提交的事务覆盖先提交的事务的修改,导致前者的更新被「丢失」
典型场景:多人同时编辑文档、商品库存扣减(如两人同时购买同一商品,库存未正确递减)
解决:悲观锁,更新前加排它锁(SELECT ... FOR UPDATE
),确保同一时间仅一个事务修改;
乐观锁,通过版本号(version
字段)或CAS
(比较并交换)校验,如UPDATE ... WHERE id=1 AND version=旧值
;
原子操作,将更新逻辑简化为数据库内置函数(如UPDATE stock SET count=count-1 WHERE count>0
)
3.应用层竞争(超卖)
由业务逻辑未原子化或分布式系统特性导致
- 超卖:
定义:高并发下,多个请求同时判定资源充足(如库存 > 0),但实际扣减后资源为负(如卖出101
件库存100
的商品)。上面只是其中一种超卖的情况,业务逻辑的错误等其他情况导致超卖这里不再讨论
典型场景:秒杀活动中,多用户同时下单,库存校验与扣减未严格同步
解决:原子化操作,将库存扣减逻辑写为数据库单行原子语句,如UPDATE stock SET count=count-1 WHERE count>0
; 分布式锁,通过Redis
分布式锁保证同一时间仅一个节点处理扣减逻辑
悲观锁
定义:在访问共享资源前会先获取锁,确保同一时间只有一个线程能操作该资源。例Mysql的SELECT ... FOR UPDATE
,Java的synchronized
- 适合场景:
高冲突场景:数据争用频繁的场景(如库存扣减、订单处理);需要严格保证数据原子性的操作(如银行转账)
低并发环境:并发量较小的系统,锁竞争压力小,适合使用悲观锁 - 优点:简单直观;适用于对数据准确性要求极高的场景
- 缺点:高并发场景下,锁竞争严重,可能导致大量事务阻塞甚至死锁; 加锁和解锁操作会增加系统开销,降低吞吐量
乐观锁
CAS
1.什么是CAS
先检查某个键的值是否符合预期,如果符合则更新为新值
2.CAS原理
- 读取当前值V。
- 比较V与预期值A:
若相等,将V更新为B(原子操作)。
若不等,说明其他线程已修改V,当前线程需重试或放弃
以Sql
修改库存为例
3.CAS解决什么问题
- 并发修改时的数据一致性问题,替代传统锁(如
synchronized
)悲观锁,尤其是丢失更新和原子性更新需求 - 避免线程因竞争锁而阻塞,减少上下文切换开销
- 适用于单变量的原子更新(如计数器、单例模式),典型场景包括:
无锁计数器:AtomicInteger的incrementAndGet()
。
并发容器:ConcurrentHashMap
中使用CAS
实现无锁数据结构更新
4.CAS带来什么风险
- ABA 问题:这是指在 CAS 操作期间,如果一个值 A 被修改为另一个值 B 然后再变回 A,那么 CAS 操作会误以为没有任何变化而继续执行。
- 只能保证一个共享变量的原子性:对于多个共享变量的操作,CAS 不能保证原子性。
版本号机制
1.版本号机制原理
添加一个version
字段(版本号),记录数据的修改次数,每次更新数据时,需检查当前版本号是否与读取时一致,若一致则允许更新并递增版本号,否则拒绝操作
tip:其中Mysql
的多版本并发控制(MVCC
)与其类似,实现:数据库InnoDB
为每个事务维护快照,读写互不阻塞。例如:Mysql
的READ COMMITTED
隔离级别下,写操作自动获取并递增version
2.版本号机制解决什么问题
和CAS类似
- 保证数据一致性,替代传统锁(如
synchronized
)悲观锁 - 避免ABA问题,通过版本号递增确保状态变化可追溯
3.版本号机制带来什么风险
- 高冲突场景下,大量事务因版本不匹配回滚
参考:
[1]使用select for share,for update的场景及死锁陷阱
[2]全方位探究似懂非懂的 CAS 机制~
[3]深入理解 CAS 原理 | Java
[4]深入理解CAS机制
[5]CAS是什么?彻底搞懂CAS
[6]乐观锁常见的两种实现方式
🍉文章不定期持续更新,如果我的文章对你有帮助➡️ 关注🙏🏻 点赞👍 收藏⭐️