MySQL可重复读隔离级别下,乐观锁为什么可以生效?

今天蹦出一个很蠢的问题,来记录一下
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 = 旧版本号。

你可能感兴趣的:(数据库,mysql,事务隔离级别,乐观锁)