乐观锁和悲观锁:并发冲突了怎么办
多个用户同时改同一条数据,谁的算?这是所有并发系统都要面对的问题。乐观锁和悲观锁是两种截然不同的解决思路。
悲观锁:先锁再改
悲观锁的假设是"冲突一定会发生",所以操作之前先把资源锁住,别人只能等。数据库里的SELECT ... FOR UPDATE就是典型的悲观锁——事务一开始就把行锁住,其他事务读这行数据会被阻塞,直到当前事务提交。Java的synchronized关键字也是悲观锁。优点是数据安全,不会出现脏写。缺点是并发性能差,锁的粒度越大、持有时间越长,等待的线程越多。
乐观锁:先改再验
乐观锁的假设是"冲突很少发生",所以不加锁,而是在提交时检查数据有没有被别人改过。最常见的实现方式是版本号:每次更新时带上WHERE version = 旧版本号,如果版本号已经被别人改了,更新影响行数为0,说明有冲突,需要重试。UPDATE SET stock = stock - 1, version = version + 1 WHERE id = 1 AND version = 5,这就是乐观锁的典型写法。
怎么选
读多写少的场景用乐观锁——大部分时候不会冲突,没必要加锁浪费性能。典型场景:商品详情页、用户信息修改、配置管理。写多读少的场景用悲观锁——冲突频繁,乐观锁不断重试反而更浪费。典型场景:秒杀扣库存、银行转账、票务系统。实际项目中,秒杀场景往往两者结合:先用Redis做乐观锁预扣库存,再用数据库悲观锁做最终确认。
乐观锁的坑
乐观锁最大的问题是ABA问题:数据从A改成B再改回A,版本号变了但值没变,乐观锁检测不到这种中间变化。解决方案:不用版本号,改用时间戳或者直接比较完整数据。另外,乐观锁的重试次数要设上限,否则高并发下线程会一直自旋空转,CPU直接打满。
Redis里的实现
Redis没有原生的锁表机制,但可以用WATCH+MULTI实现乐观锁。WATCH一个key后,如果在EXEC之前这个key被别人改了,整个事务会失败。Redisson等客户端库则提供了基于Lua脚本的分布式悲观锁(RedissonLock),支持可重入、看门狗续期,在微服务场景下用得比较多。




提供云计算服务