今天蹦出一个很蠢的问题,来记录一下
MySQL在可重复读事务隔离级别下,通过版本号实现的乐观锁可以生效吗?
举个例子:开启了两个事务一和二,在事务一中对某条数据进行了修改,版本号发生变化。但是在事务二中,查询这条记录的版本号,并没有发生变化,怎么确定这条数据是否被其他事务修改了呢?
原因是在可重复读隔离级别下,分为快照读和当前读,update是当前读,而不是快照读,因而可以读取到最新的版本号。
快照读,读取的是开启事务一瞬间快照的数据,当前读可以读取到其他事务已经提交的数据。在可重复读隔离级别下,select使用的是快照读,当其他事务已经提交时,当前事务还是读取的快照数据,update更新时 通过where选中数据时,使用是当前读,读取的是当前的数据。所以在多个事务中,当其他事务修改了数据,版本号发生了变化,即使当前事务读取不到版本号的变化,但是在更新的时候,根据版本号更新数据时,选不到数据,会更新失败。
为什么要引入锁?肯定是避免多个线程修改一个资源,引发的并发问题,破坏数据一致性问题。下面看个例子,money记录一个存钱罐金额,存钱罐中的钱可以多个用户取出。下面是初数据:
mysql> select * from money;
+----+--------+
| id | money |
+----+--------+
| 1 | 100.00 |
+----+--------+
1 row in set (0.00 sec)
开启两个事务,每个事务判断余额充足的情况下,取出金额:
事务一
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from money;
+----+--------+
| id | money |
+----+--------+
| 1 | 100.00 |
+----+--------+
1 row in set (0.00 sec)
事务二
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from money;
+----+--------+
| id | money |
+----+--------+
| 1 | 100.00 |
+----+--------+
1 row in set (0.00 sec)
每个事务判断金额都充足,事务一取出90
事务一
mysql> update money set money = money-90 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from money;
+----+-------+
| id | money |
+----+-------+
| 1 | 10.00 |
+----+-------+
1 row in set (0.00 sec)
事务二再次查询数据库,发现金额并没有减少,原因就是Select读取的是快照数据,然后事务二也开始取钱,
事务二
mysql> select * from money;
+----+--------+
| id | money |
+----+--------+
| 1 | 100.00 |
+----+--------+
1 row in set (0.00 sec)
mysql> update money set money = money-90 where id = 1;
但是update时,数据库阻塞,原因是事务一并没有提交事务,在事务二中拿不到行级锁。下面我们提交事务一,释放锁
事务一
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
事务二再次查询数据库,金额还是100,再次取钱试试
事务二
mysql> select * from money;
+----+--------+
| id | money |
+----+--------+
| 1 | 100.00 |
+----+--------+
1 row in set (0.00 sec)
mysql> update money set money = money-90 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
数据更新成功。提交事务吧
事务二
mysql> commit;
Query OK, 0 rows affected (0.01 sec)
让我们查询一下数据最后的结果是怎样的
mysql> select * from money;
+----+--------+
| id | money |
+----+--------+
| 1 | -80.00 |
+----+--------+
1 row in set (0.00 sec)
意料之中,并发问题,数据一致性被破坏。
下面让我们增加一个乐观锁试试吧。增加version版本号字段,记录数据版本
mysql> alter table money add version int(11) after money;
Query OK, 0 rows affected (0.06 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> update money set version = 0 , money = 100 where id =1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from money;
+----+--------+---------+
| id | money | version |
+----+--------+---------+
| 1 | 100.00 | 0 |
+----+--------+---------+
1 row in set (0.00 sec)
同样开启两个事务
事务一
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
事务二
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
下面事务一开始取钱,更新数据时根据快照数据版本号更新,并且更改数据时顺带更新数据版本号。
事务一
mysql> select * from money;
+----+--------+---------+
| id | money | version |
+----+--------+---------+
| 1 | 100.00 | 0 |
+----+--------+---------+
1 row in set (0.00 sec)
mysql> update money set money = money-90,version = version+1 where id = 1 and version = 0;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from money;
+----+-------+---------+
| id | money | version |
+----+-------+---------+
| 1 | 10.00 | 1 |
+----+-------+---------+
1 row in set (0.00 sec)
此时如果事务二也想修改数据是修改不成功的,因为事务一没有释放锁,事务二阻塞
事务二
mysql> select * from money;
+----+--------+---------+
| id | money | version |
+----+--------+---------+
| 1 | 100.00 | 0 |
+----+--------+---------+
1 row in set (0.00 sec)
mysql> update money set money = money-90,version = version+1 where id = 1 and version = 0;
事务一提交事务
事务一
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
此时在事务二中更新数据,更改行数为0,找不到id=1,version=0 的记录
mysql> update money set money = money-90,version = version+1 where id = 1 and version = 0;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 0 Changed: 0 Warnings: 0
原因就是update是当前读,读取的是最新的数据,version已经变成了1。
select读取的是快照读,读取的是version还是0。
数据版本号发生了变化,更新失败,事务二回滚,再次查询数据,读取到新数据
mysql> rollback;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from money;
+----+-------+---------+
| id | money | version |
+----+-------+---------+
| 1 | 10.00 | 1 |
+----+-------+---------+
1 row in set (0.00 sec)
在MybatisPlus中,乐观锁实现原理同样是给数据库增加version版本号,我们修改数据时,会自动在update语句set后添加version = version + 1 ,where后面增加version = 旧版本号。