MySQL悲观锁与乐观锁的实现方案
导读
前言
悲观锁和乐观锁是用来解决并发问题的两种思想,在不同的平台有着各自的实现。例如在Java中,synchronized就可以认为是悲观锁的实现(不严谨,有锁升级的过程,升级到重量级锁才算),Atomic***原子类可以认为是乐观锁的实现。
悲观锁
具有强烈的独占和排他特性,在整个处理过程中将数据处于锁定状态,一般是通过系统的互斥量来实现。当其他线程想要获取锁时会被阻塞,直到持有锁的线程释放锁。
乐观锁
对数据的修改和访问持乐观态度,假设不会发生冲突,只有当数据提交更新时才会对数据冲突与否进行检测,如果没有冲突则顺利提交更新,否则快速失败,返回一个错误给用户,让用户选择接下来该如何去做,一般来说失败后会继续重试,直到提交更新成功为止。
MySQL本身就支持锁机制,例如我们有一个「先查再写」的需求,我们希望整个流程是一个原子操作,中间不能被打断,这时候就可以通过给查询的数据行加「排他锁」来实现。只要当前事务不释放锁,其他事务要想获得排他锁,MySQL就会将其阻塞,直到当前事务释放锁。这种MySQL底层的排他锁就称作「悲观锁」。
MySQL本身不提供乐观锁的功能,需要开发者自己实现。普遍的做法是在表中加一个version列,用来标记数据行的版本,当我们需要更新数据时,必须比对version版本,version一致说明这个期间数据没有被其他事务修改过,否则说明数据已经被其他事务修改,需要自旋重试了。
实战
假设数据库有两张表:商品表和订单表。
用户下单后需要执行两个操作:
- 商品表减去库存。 订单表创建一条记录。
初始数据:ID为1的商品有100的库存,订单表数据为空。
客户端启动10个线程并发下单,分别在无锁、悲观锁、乐观锁的场景下有哪些表现。
如下是创建表的sql语句:
-- 商品表 CREATE TABLE `goods` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `goods_name` varchar(50) NOT NULL, `price` decimal(10,2) NOT NULL, `stock` int(11) DEFAULT '0', `version` int(10) unsigned NOT NULL DEFAULT '0', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 -- 订单表 CREATE TABLE `t_order` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `goods_id` bigint(20) NOT NULL, `order_time` datetime NOT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8
1、无锁
不做任何处理。
// 下单 private boolean order(){ Goods goods = goodsMapper.selectById(1L); boolean success = false; if (goods.getStock() > 0) { goods.setStock(goods.getStock() - 1); // 更新库存 goodsMapper.updateById(goods); // 创建订单 orderMapper.save(goods.getId()); success = true; } return success; }
控制台输出结果:
2、悲观锁
查询商品时加FOR UPDATE,给数据行加排他锁,这样其他线程再查询时就会被阻塞,直到当前线程的事务提交并释放锁,其他线程才能继续下单。这种方式并发性能不高。
sql语句
@Select("SELECT * FROM goods WHERE id = #{id} FOR UPDATE") Goods selectForUpdate(Long id);
控制台输出结果:
注意:FOR UPDATE必须在事务中才有效,查询和更新必须在同一个事务中!!!
3、乐观锁
实现思路是:每次更新时校验版本号,如果版本号一致说明期间数据没有被其他线程改过,当前线程可以正常提交更新,否则说明数据已经被其他线程改过了,当前线程需要自旋重试,直到业务成功为止。
更新数据的同时版本号必须自增!!!
@Update("UPDATE goods SET stock = #{stock},version = version+1 WHERE id = #{id} AND version = #{version}") int updateByVersion(Long id, Integer stock, Integer version);
业务代码
boolean order(){ Goods goods = goodsMapper.selectById(1L); boolean success = false; if (goods.getStock() > 0) { goods.setStock(goods.getStock() - 1); // 更新库存,带上版本号 int result = goodsMapper.updateByVersion(goods.getId(), goods.getStock(), goods.getVersion()); if (result <= 0) { // 更新失败,说明期间数据已经被其他线程修改,需要递归重试 return order(); } // 创建订单 orderMapper.save(goods.getId()); success = true; } return success; }
控制台输出结果: