MySQL悲观锁和乐观锁的实现 - Sanarous的博客

MySQL悲观锁和乐观锁的实现

悲观锁和乐观锁

不清楚悲观锁和乐观锁定义的可以看这里

MySQL 中悲观锁的实现

在 MySQL 中一般指排他锁,当事务在操作数据时将这部分数据进行锁定,直到操作完毕后再解锁,解锁后才能够由其它事务操作该部分数据。

MySQL 中的悲观锁大部分情况下依赖数据库的锁机制实现,一般使用SELECT ... FOR UPDATE对选择的数据进行加锁处理,例如:

1
SELECT * FROM account WHERE name = "MAX" for update

这条 SQL 语句锁定了 account 表中所有符合检索条件(name=”MAX”)的记录。本次事务提交之前(事务提交时会释放事务过程中的锁),外界无法修改这些记录。

MySQL 中乐观锁的实现

乐观锁相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以数据进行提交更新的时候,才会对数据是否冲突进行检测,如果冲突了,就返回用户错误的信息,让用户决定如何去做。一般都是使用版本号机制来实现 MySQL 数据库中的乐观锁。

版本号的实现方式有两种,一个是数据版本机制,一个是时间戳机制。

数据版本实现乐观锁

使用数据版本机制实现是 MySQL 乐观锁实现的最常用的方式,何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 version 字段来实现。当读取数据时,将 version 字段的值一同读出,数据每更新一次,对 version 的值加 1。当我们提交更新时,判断数据库表对应的当前版本信息与第一次取出来的 version 值进行比对,如果数据库表当前版本号与第一次取出来的 version 值相等,则予以更新,否则认为是过期数据。

如上图所示,如果更新操作顺序执行,则数据的版本(version)依次递增,不会产生冲突。但是如果有发生不同的业务操作对同一版本的数据进行修改,那么先提交的操作(图中 B)会把数据 version 更新为 2,当 A 在 B 之后提交更新时发现数据的 version 已经被淘汰了,那么 A 的更新操作就会失败。

时间戳机制实现乐观锁

使用时间戳机制实现乐观锁原理类似,同样是在需要乐观锁控制的 table 中增加一个字段,名称无所谓,字段类型时间 timestamp,和上面的 version 类似,也是在更新提交时检查当前数据库中的时间戳和自己更新前提取到的时间戳进行对比,如果一致则 OK,否则就是版本冲突。

使用举例

以 MySQL InnoDB 为例

还是拿之前的实例来举:商品 t_goods 表中有一个字段 status,status 为 1 代表商品未被下单,status 为 2 代表商品已经被下单,那么我们对某个商品下单时必须确保该商品 status 为 1。假设商品的 id 为 1。

下单操作包括 3 个步骤:

  1. 查询出商品信息:select (status,status,version) from t_goods where id=#{id}

  2. 根据商品信息生成订单

  3. 修改商品 status 为 2:update t_goods set status=2,version=version+1 where id=#{id} and version=#{version};

那么为了使用乐观锁,我们首先修改 t_goods 表,增加一个 version 字段,数据默认 version 值为 1。

t_goods 表初始数据如下:

1
2
3
4
5
6
7
8
9
10
mysql> select * from t_goods;  
+----+--------+------+---------+
| id | status | name | version |
+----+--------+------+---------+
| 1 | 1 | 道具 | 1 |
| 2 | 2 | 装备 | 2 |
+----+--------+------+---------+
2 rows in set

mysql>

对于乐观锁的实现,我使用 MyBatis 来进行实践,具体如下:

Goods实体类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Goods implements Serializable {  

/**
* serialVersionUID:序列化ID.
*/
private static final long serialVersionUID = 6803791908148880587L;

/**
* id:主键id.
*/
private int id;

/**
* status:商品状态:1未下单、2已下单.
*/
private int status;

/**
* name:商品名称.
*/
private String name;

/**
* version:商品数据版本号.
*/
private int version;

@Override
public String toString(){
return "good id:"+id+",goods status:"+status+",goods name:"+name+",goods version:"+version;
}

//setter and getter

}

GoodsDao 代码:

1
int updateGoodsUseCAS(Goods goods);

Mapper.xml 代码:

1
2
3
4
5
6
7
<update id="updateGoodsUseCAS" parameterType="Goods">  
<![CDATA[
update t_goods
set status=#{status},name=#{name},version=version+1
where id=#{id} and version=#{version}
]]>
</update>

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test  
public void goodsDaoTest(){
int goodsId = 1;
//根据相同的id查询出商品信息,赋给2个对象
Goods goods1 = this.goodsDao.getGoodsById(goodsId);
Goods goods2 = this.goodsDao.getGoodsById(goodsId);

//打印当前商品信息
System.out.println(goods1);
System.out.println(goods2);

//更新商品信息1
goods1.setStatus(2);//修改status为2
int updateResult1 = this.goodsDao.updateGoodsUseCAS(goods1);
System.out.println("修改商品信息1"+(updateResult1 == 1?"成功":"失败"));

//更新商品信息2
goods1.setStatus(2);//修改status为2
int updateResult2 = this.goodsDao.updateGoodsUseCAS(goods1);
System.out.println("修改商品信息2"+(updateResult2 == 1?"成功":"失败"));
}

输出结果:

1
2
3
4
good id:1,goods status:1,goods name:道具,goods version:1  
good id:1,goods status:1,goods name:道具,goods version:1
修改商品信息1成功
修改商品信息2失败

在 GoodsDaoTest 测试方法中,我们同时查出同一个版本的数据,赋给不同的 goods 对象,然后先修改 good1 对象然后执行更新操作,执行成功。然后我们修改 goods2,执行更新操作时提示操作失败。此时 t_goods 表中数据如下:

1
2
3
4
5
6
7
8
9
10
mysql> select * from t_goods;  
+----+--------+------+---------+
| id | status | name | version |
+----+--------+------+---------+
| 1 | 2 | 道具 | 2 |
| 2 | 2 | 装备 | 2 |
+----+--------+------+---------+
2 rows in set

mysql>

我们可以看到 id 为 1 的数据 version 已经在第一次更新时修改为 2 了。所以我们更新 good2 时 update where 条件已经不匹配了,所以更新不会成功,具体 sql 如下:

1
2
3
update t_goods   
set status=2,version=version+1
where id=#{id} and version=#{version};

这样我们就实现了乐观锁。以上内容有问题欢迎指正!

参考文章

  1. mysql乐观锁总结与实战
  2. 使用mysql乐观锁解决并发问题
  3. mysql对乐观锁的支持
如果这篇文章对您很有帮助,不妨
-------------    本文结束  感谢您的阅读    -------------
0%