事务由事务开始(begin transaction)和事务结束(end transaction)之间执行的全体操作组成。(执行都执行,失败就回滚,对数据库数据没有影响)

MYSQL事务并处理

关于mysql事务的基本概念请查看数据库部分的mysql基础中的mysql高级了解章节,这里不再叙述

1)事务的并发问题

脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据

不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。

幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。

2)事务隔离级别

MYSQL为了解决并发问题,其数据库底层设计了几种隔离级别来解决存在的事务并发问题。

事务隔离级别脏读不可重复读幻读

读未提交(read-uncommitted)

不可重复读(read-committed)

可重复读(repeatable-read)

串行化(serializable)

mysql默认的事务隔离级别为repeatable-read

mysql> select @@tx_isolation;

+-----------------+

| @@tx_isolation |

+-----------------+

| REPEATABLE-READ |

+-----------------+

3)实例分析几种隔离级别的含义

在实例分析前,我们创建了一个名为money的数据表,如下:

mysql> select * from money;

+----+--------+--------+

| id | name | number |

+----+--------+--------+

| 1 | 张三 | 500 |

| 2 | 李四 | 1000 |

| 3 | 王无 | 1200 |

+----+--------+--------+

1、read-uncommitted

打开两个客户端A和B,并设置其当前事务模式为read uncommitted,然后查询该表,可以看到二者查询到的数据和原始数据相同。

mysql> set session transaction isolation level read uncommitted;

Query OK, 0 rows affected (0.00 sec)

mysql> start transaction;

Query OK, 0 rows affected (0.00 sec)

mysql> select * from money;

+----+--------+--------+

| id | name | number |

+----+--------+--------+

| 1 | 张三 | 500 |

| 2 | 李四 | 1000 |

| 3 | 王无 | 1200 |

+----+--------+--------+

3 rows in set (0.00 sec)

然后通过客户端B来更改张三的钱为400

mysql> update money set number = 400 where id = 1;

Query OK, 1 row affected (0.00 sec)

Rows matched: 1 Changed: 1 Warnings: 0

mysql> select * from money;

+----+--------+--------+

| id | name | number |

+----+--------+--------+

| 1 | 张三 | 400 |

| 2 | 李四 | 1000 |

| 3 | 王无 | 1200 |

+----+--------+--------+

3 rows in set (0.00 sec)

此时,客户端B的事务还未提交,由客户端A再来查询一下表数据,可以看到从客户端A中立马就可以查询到客户端B所做的修改。

mysql> select * from money;

+----+--------+--------+

| id | name | number |

+----+--------+--------+

| 1 | 张三 | 500 |

| 2 | 李四 | 1000 |

| 3 | 王无 | 1200 |

+----+--------+--------+

3 rows in set (0.00 sec)

-----------------------------------

mysql> select * from money;

+----+--------+--------+

| id | name | number |

+----+--------+--------+

| 1 | 张三 | 400 |

| 2 | 李四 | 1000 |

| 3 | 王无 | 1200 |

+----+--------+--------+

3 rows in set (0.00 sec)

此时,我们回滚客户端B中的操作

mysql> select * from money;

+----+--------+--------+

| id | name | number |

+----+--------+--------+

| 1 | 张三 | 400 |

| 2 | 李四 | 1000 |

| 3 | 王无 | 1200 |

+----+--------+--------+

3 rows in set (0.00 sec)

mysql> rollback;

Query OK, 0 rows affected (0.00 sec)

mysql> select * from money;

+----+--------+--------+

| id | name | number |

+----+--------+--------+

| 1 | 张三 | 500 |

| 2 | 李四 | 1000 |

| 3 | 王无 | 1200 |

+----+--------+--------+

3 rows in set (0.00 sec)

再在客户端A中查询,数据又恢复为500。此时客户端A的用户肯定会疑惑的,自己并未修改此数据,怎么发生了变化。

mysql> select * from money;

+----+--------+--------+

| id | name | number |

+----+--------+--------+

| 1 | 张三 | 400 |

| 2 | 李四 | 1000 |

| 3 | 王无 | 1200 |

+----+--------+--------+

3 rows in set (0.00 sec)

mysql> select * from money;

+----+--------+--------+

| id | name | number |

+----+--------+--------+

| 1 | 张三 | 500 |

| 2 | 李四 | 1000 |

| 3 | 王无 | 1200 |

+----+--------+--------+

3 rows in set (0.00 sec)

我们称这一例子中客户端A所获取到的数据400为脏数据。要想解决这一问题,我们必须保证其他客户端正在修改但并未提交的数据不应该被本客户端获取,这就需要上升事务隔离级别为read-committed。

2、read-committed

同样,打开两个客户端A和B,并设置其当前事务模式为read committed,然后查询该表,可以看到二者查询到的数据和原始数据相同。

mysql> set session transaction isolation level read committed;

Query OK, 0 rows affected (0.00 sec)

mysql> start transaction;

Query OK, 0 rows affected (0.00 sec)

mysql> select * from money;

+----+--------+--------+

| id | name | number |

+----+--------+--------+

| 1 | 张三 | 500 |

| 2 | 李四 | 1000 |

| 3 | 王无 | 1200 |

+----+--------+--------+

3 rows in set (0.00 sec)

然后通过客户端B来更改张三的钱为400

mysql> update money set number = 400 where id = 1;

Query OK, 1 row affected (0.00 sec)

Rows matched: 1 Changed: 1 Warnings: 0

mysql> select * from money;

+----+--------+--------+

| id | name | number |

+----+--------+--------+

| 1 | 张三 | 400 |

| 2 | 李四 | 1000 |

| 3 | 王无 | 1200 |

+----+--------+--------+

3 rows in set (0.00 sec)

此时,客户端B的事务还未提交,由客户端A再来查询一下表数据,可以看到从客户端A中没有看到客户端B中的修改,解决了脏数据读取问题

mysql> select * from money;

+----+--------+--------+

| id | name | number |

+----+--------+--------+

| 1 | 张三 | 500 |

| 2 | 李四 | 1000 |

| 3 | 王无 | 1200 |

+----+--------+--------+

3 rows in set (0.00 sec)

mysql> select * from money;

+----+--------+--------+

| id | name | number |

+----+--------+--------+

| 1 | 张三 | 500 |

| 2 | 李四 | 1000 |

| 3 | 王无 | 1200 |

+----+--------+--------+

3 rows in set (0.01 sec)

然后从客户端B中提交刚才的事务

mysql> update money set number = 400 where id = 1;

Query OK, 1 row affected (0.00 sec)

Rows matched: 1 Changed: 1 Warnings: 0

mysql> select * from money;

+----+--------+--------+

| id | name | number |

+----+--------+--------+

| 1 | 张三 | 400 |

| 2 | 李四 | 1000 |

| 3 | 王无 | 1200 |

+----+--------+--------+

3 rows in set (0.00 sec)

mysql> commit;

Query OK, 0 rows affected (0.00 sec)

mysql> select * from money;

+----+--------+--------+

| id | name | number |

+----+--------+--------+

| 1 | 张三 | 400 |

| 2 | 李四 | 1000 |

| 3 | 王无 | 1200 |

+----+--------+--------+

3 rows in set (0.00 sec)

再在客户端A的当前事务中查询表数据,发现数据发生了变化,但客户端A的用户未在当前事务中修改该数据为400 。这就引发了同一事务中的数据重复读取不一致问题,即不可重复读的问题。

mysql> select * from money;

+----+--------+--------+

| id | name | number |

+----+--------+--------+

| 1 | 张三 | 500 |

| 2 | 李四 | 1000 |

| 3 | 王无 | 1200 |

+----+--------+--------+

3 rows in set (0.01 sec)

mysql> select * from money;

+----+--------+--------+

| id | name | number |

+----+--------+--------+

| 1 | 张三 | 400 |

| 2 | 李四 | 1000 |

| 3 | 王无 | 1200 |

+----+--------+--------+

3 rows in set (0.00 sec)

如何解决事务并发时的不可重复读问题呢?我们需要把事务隔离级别进一步上升为MYSQL默认的事务隔离级别repeatable read。

3、repeatable-read

同样,打开两个客户端A和B,并设置其当前事务模式为repeatable-read,然后查询该表,可以看到二者查询到的数据和原始数据相同。

mysql> set session transaction isolation level repeatable read;

Query OK, 0 rows affected (0.00 sec)

mysql> start transaction;

Query OK, 0 rows affected (0.00 sec)

mysql> select * from money;

+----+--------+--------+

| id | name | number |

+----+--------+--------+

| 1 | 张三 | 400 |

| 2 | 李四 | 1000 |

| 3 | 王无 | 1200 |

+----+--------+--------+

3 rows in set (0.00 sec)

然后通过客户端B来更改张三的钱为450

mysql> update money set number = 450 where id = 1;

Query OK, 1 row affected (0.01 sec)

Rows matched: 1 Changed: 1 Warnings: 0

mysql> select * from money;

+----+--------+--------+

| id | name | number |

+----+--------+--------+

| 1 | 张三 | 450 |

| 2 | 李四 | 1000 |

| 3 | 王无 | 1200 |

+----+--------+--------+

此时,客户端B的事务还未提交,由客户端A再来查询一下表数据,仍为400

mysql> select * from money;

+----+--------+--------+

| id | name | number |

+----+--------+--------+

| 1 | 张三 | 400 |

| 2 | 李四 | 1000 |

| 3 | 王无 | 1200 |

+----+--------+--------+

3 rows in set (0.00 sec)

然后从客户端B中提交刚才的事务

mysql> update money set number = 450 where id = 1;

Query OK, 1 row affected (0.01 sec)

Rows matched: 1 Changed: 1 Warnings: 0

mysql> select * from money;

+----+--------+--------+

| id | name | number |

+----+--------+--------+

| 1 | 张三 | 450 |

| 2 | 李四 | 1000 |

| 3 | 王无 | 1200 |

+----+--------+--------+

3 rows in set (0.00 sec)

mysql> commit;

Query OK, 0 rows affected (0.01 sec)

mysql> select * from money;

+----+--------+--------+

| id | name | number |

+----+--------+--------+

| 1 | 张三 | 450 |

| 2 | 李四 | 1000 |

| 3 | 王无 | 1200 |

+----+--------+--------+

3 rows in set (0.00 sec)

再在客户端A的当前事务中查询表数据,仍旧为400 。解决了同一事务的数据不可重复读问题。

mysql> select * from money;

+----+--------+--------+

| id | name | number |

+----+--------+--------+

| 1 | 张三 | 400 |

| 2 | 李四 | 1000 |

| 3 | 王无 | 1200 |

+----+--------+--------+

3 rows in set (0.00 sec)

从客户端A提交当前事务,并查询表数据,会发现张三的钱变成了450 。这会让客户端A的用户认为刚才事务中操作时读取的数据不对,即产生幻觉。我们称之为幻读。

mysql> select * from money;

+----+--------+--------+

| id | name | number |

+----+--------+--------+

| 1 | 张三 | 400 |

| 2 | 李四 | 1000 |

| 3 | 王无 | 1200 |

+----+--------+--------+

3 rows in set (0.00 sec)

mysql> commit;

Query OK, 0 rows affected (0.00 sec)

mysql> select * from money;

+----+--------+--------+

| id | name | number |

+----+--------+--------+

| 1 | 张三 | 450 |

| 2 | 李四 | 1000 |

| 3 | 王无 | 1200 |

+----+--------+--------+

3 rows in set (0.00 sec)

如何解决事件并发的幻读问题呢?那就需要进一步提升事件隔离级别为serializable。

4、serializable

同样,打开两个客户端A和B,并设置其当前事务模式为serializable,然后查询该表,可以看到二者查询到的数据和原始数据相同。

mysql> set session transaction isolation level serializable;

Query OK, 0 rows affected (0.00 sec)

mysql> start transaction;

Query OK, 0 rows affected (0.00 sec)

mysql> select * from money;

+----+--------+--------+

| id | name | number |

+----+--------+--------+

| 1 | 张三 | 450 |

| 2 | 李四 | 1000 |

| 3 | 王无 | 1200 |

+----+--------+--------+

3 rows in set (0.00 sec)

然后通过客户端B来更改张三的钱为500

mysql> select * from money;

+----+--------+--------+

| id | name | number |

+----+--------+--------+

| 1 | 张三 | 450 |

| 2 | 李四 | 1000 |

| 3 | 王无 | 1200 |

+----+--------+--------+

3 rows in set (0.00 sec)

mysql> update money set number = 500 where id = 1;

Query OK, 1 row affected (47.01 sec)

Rows matched: 1 Changed: 1 Warnings: 0

mysql> select * from money;

+----+--------+--------+

| id | name | number |

+----+--------+--------+

| 1 | 张三 | 500 |

| 2 | 李四 | 1000 |

| 3 | 王无 | 1200 |

+----+--------+--------+

3 rows in set (0.00 sec)

再在客户端A中修改数据,会报错,提示表被锁,无法插入

mysql> update money set number = 600 where id = 1;

ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

这就可以解决数据读取重复性的问题了。

4)总结:

mysql中默认事务隔离级别是可重复读时并不会锁住读取到的行

事务隔离级别为读提交时,写数据只会锁住相应的行

事务隔离级别为可重复读时,写数据会锁住整张表

事务隔离级别为串行化时,读写数据都会锁住整张表

隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大,鱼和熊掌不可兼得。

对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为Read Committed,它能够避免脏读取,而且具有较好的并发性能。但是它会导致不可重复读、幻读这些并发问题。这些问题我们可以由应用程序等采用悲观锁或乐观锁来控制。接下来我们介绍悲观锁和乐观锁。

数据库系统的自定义隔离级别配置:

linux:

打开mysql配置文件: sudo vi /etc/mysql/mysql.conf.d/mysqld.cnf,

增加:

transaction-isolation = READ-COMMITTED

例如:

# The MySQL server

[mysqld]

# 设置隔离级别

transaction-isolation = READ-COMMITTED

default-storage-engine=INNODB

character-set-server=utf8

collation-server=utf8_general_ci

port = 3306

pid-file = /var/run/mysqld/mysqld.pid

socket = /var/run/mysqld/mysqld.sock

mac:

As of MySQL 5.7.18, my-default.ini is no longer included in or installed by distribution packages

所以需要在/etc/目录下自建名为my.cnf的 文件,配置内容与linux相同。

配置成功后需查看事务隔离级别是否修改ok:

mysql> select @@tx_isolation;

+----------------+

| @@tx_isolation |

+----------------+

| READ-COMMITTED |

+----------------+

隔离级别知识点补充

mysql 备注颜色 mysql红色_红锁mysql

mysql 备注颜色 mysql红色_红锁mysql_02

什么是事务隔离?

任何支持事务的数据库,都必须具备四个特性,分别是:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability),也就是我们常说的事务ACID,这样才能保证事务((Transaction)中数据的正确性。

而事务的隔离性就是指,多个并发的事务同时访问一个数据库时,一个事务不应该被另一个事务所干扰,每个并发的事务间要相互进行隔离。

如果没有事务隔离,会出现什么样的情况呢?

假设我们现在有这样一张表(T),里面记录了很多牛人的名字,我们不进行事务的隔离看看会发生什么呢?

第一天,事务A访问了数据库,它干了一件事情,往数据库里加上了新来的牛人的名字,但是没有提交事务。

insert into T values (4, '牛D');

这时,来了另一个事务B,他要查询所有牛人的名字。

select NamefromT;

这时,如果没有事务之间没有有效隔离,那么事务B返回的结果中就会出现“牛D”的名字。这就是“脏读(dirty read)”。

第二天,事务A访问了数据库,他要查看ID是1的牛人的名字,于是执行了

select Namefrom T where ID = 1;

这时,事务B来了,因为ID是1的牛人改名字了,所以要更新一下,然后提交了事务。

update T set Name= '不牛' where ID = 1;

接着,事务A还想再看看ID是1的牛人的名字,于是又执行了

select Namefrom T where ID = 1;

结果,两次读出来的ID是1的牛人名字竟然不相同,这就是不可重复读(unrepeatable read)。

第三天,事务A访问了数据库,他想要看看数据库的牛人都有哪些,于是执行了

select* fromT;

这时候,事务B来了,往数据库加入了一个新的牛人。

insert into T values(4, '牛D');

这时候,事务A忘了刚才的牛人都有哪些了,于是又执行了。

select* fromT;

结果,第一次有三个牛人,第二次有四个牛人。

相信这个时候事务A就蒙了,刚才发生了什么?这种情况就叫“幻读(phantom problem)”。

为了防止出现脏读、不可重复读、幻读等情况,我们就需要根据我们的实际需求来设置数据库的隔离级别。

数据库都有哪些隔离级别呢?

一般的数据库,都包括以下四种隔离级别:

读未提交(Read Uncommitted)

读提交(Read Committed)

可重复读(Repeated Read)

串行化(Serializable)

如何使用这些隔离级别,那就需要根据业务的实际情况来进行判断了。

我们接下来就看看这四个隔离级别的具体情况

读未提交(Read Uncommitted)

读未提交,顾名思义,就是可以读到未提交的内容。

因此,在这种隔离级别下,查询是不会加锁的,也由于查询的不加锁,所以这种隔离级别的一致性是最差的,可能会产生“脏读”、“不可重复读”、“幻读”。

如无特殊情况,基本是不会使用这种隔离级别的。

读提交(Read Committed)

读提交,顾名思义,就是只能读到已经提交了的内容。

这是各种系统中最常用的一种隔离级别,也是SQL Server和Oracle的默认隔离级别。这种隔离级别能够有效的避免脏读,但除非在查询中显示的加锁,如:

select* from T where ID=2 lock inshare mode;

select* from T where ID=2 forupdate;

不然,普通的查询是不会加锁的。

那为什么“读提交”同“读未提交”一样,都没有查询加锁,但是却能够避免脏读呢?

这就要说道另一个机制“快照(snapshot)”,而这种既能保证一致性又不加锁的读也被称为“快照读(Snapshot Read)”

假设没有“快照读”,那么当一个更新的事务没有提交时,另一个对更新数据进行查询的事务会因为无法查询而被阻塞,这种情况下,并发能力就相当的差。

而“快照读”就可以完成高并发的查询,不过,“读提交”只能避免“脏读”,并不能避免“不可重复读”和“幻读”。

可重复读(Repeated Read)

可重复读,顾名思义,就是专门针对“不可重复读”这种情况而制定的隔离级别,自然,它就可以有效的避免“不可重复读”。而它也是MySql的默认隔离级别。

在这个级别下,普通的查询同样是使用的“快照读”,但是,和“读提交”不同的是,当事务启动时,就不允许进行“修改操作(Update)”了,而“不可重复读”恰恰是因为两次读取之间进行了数据的修改,因此,“可重复读”能够有效的避免“不可重复读”,但却避免不了“幻读”,因为幻读是由于“插入或者删除操作(InsertorDelete)”而产生的。

串行化(Serializable)

这是数据库最高的隔离级别,这种级别下,事务“串行化顺序执行”,也就是一个一个排队执行。

这种级别下,“脏读”、“不可重复读”、“幻读”都可以被避免,但是执行效率奇差,性能开销也最大,所以基本没人会用。

总结一下

为什么会出现“脏读”?因为没有“select”操作没有规矩。

为什么会出现“不可重复读”?因为“update”操作没有规矩。

为什么会出现“幻读”?因为“insert”和“delete”操作没有规矩。

“读未提(Read Uncommitted)”能预防啥?啥都预防不了。

“读提交(Read Committed)”能预防啥?使用“快照读(Snapshot Read)”,避免“脏读”,但是可能出现“不可重复读”和“幻读”。

“可重复读(Repeated Red)”能预防啥?使用“快照读(Snapshot Read)”,锁住被读取记录,避免出现“脏读”、“不可重复读”,但是可能出现“幻读”。

“串行化(Serializable)”能预防啥?排排坐,吃果果,有效避免“脏读”、“不可重复读”、“幻读”,不过效果谁用谁知道。

View Code

19.2 悲观锁与乐观锁

1) 悲观锁

正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。

悲观锁实例

首先,将事务隔离级别设置为READ-COMMITTED,然后通过客户端A和客户端B进入事务,查询数据

mysql> begin;

Query OK, 0 rows affected (0.00 sec)

mysql> select * from money;

+----+--------+--------+

| id | name | number |

+----+--------+--------+

| 1 | 张三 | 500 |

| 2 | 李四 | 1000 |

| 3 | 王无 | 1200 |

+----+--------+--------+

3 rows in set (0.00 sec)

在客户端A中对数据张三进行查询。

此次采用数据库本身的悲观锁命令 for update

mysql> select number from money where id=1 for update;

+--------+

| number |

+--------+

| 500 |

+--------+

1 row in set (0.00 sec)

在客户端B中对数据张三进行修改300操作,可以看到被锁了,修改失败。这和serializable的效果完全一样。

mysql> update money set number = 300 where id = 1;

ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

在客户端A中提交事务

mysql> select number from money where id=1 for update;

+--------+

| number |

+--------+

| 500 |

+--------+

1 row in set (0.00 sec)

mysql> commit;

Query OK, 0 rows affected (0.00 sec)

再在客户端B中对数据张三进行修改300操作,操作成功

mysql> update money set number = 300 where id = 1;

Query OK, 1 row affected (0.00 sec)

Rows matched: 1 Changed: 1 Warnings: 0

mysql> select * from money;

+----+--------+--------+

| id | name | number |

+----+--------+--------+

| 1 | 张三 | 300 |

| 2 | 李四 | 1000 |

| 3 | 王无 | 1200 |

+----+--------+--------+

3 rows in set (0.00 sec)

通过实例可以看出,悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但是如果加锁的时间过长,其他用户长时间无法访问,影响了程序的并发访问性,同时这样对数据库性能开销影响也很大,特别是对长事务而言,这样的开销往往无法承受。因此便产生了乐观锁。

Django中的模型管理器悲观锁方法:

select_for_update(nowait=False, skip_locked=False, of=())

等价于: SELECT ... FOR UPDATE

例如:entries = Entry.objects.select_for_update().filter(author=request.user)

2)乐观锁

乐观锁( Optimistic Locking ) 相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回用户错误的信息,让用户决定如何去做。那么我们如何实现乐观锁呢,一般来说有以下2种方式:

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

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

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

乐观锁实例

为了演示模拟乐观锁,我们向之前的money表中添加字段version,并定义初始值为1

mysql> select * from money where id=1;

+----+--------+--------+---------+

| id | name | number | version |

+----+--------+--------+---------+

| 1 | 张三 | 300 | 1 |

+----+--------+--------+---------+

我们约定当事务对表进行修改时,将版本version设置自动加一version+1操作。

update money set version=version+'1' where id=1 and version=操作前本客户端此时的版本号;

可以看到此更新命令可以完成我们的约定,那么我们就在修改数据时以此命令来操作数据。

mysql> select * from money;

+----+--------+--------+---------+

| id | name | number | version |

+----+--------+--------+---------+

| 1 | 张三 | 300 | 1 |

| 2 | 李四 | 1000 | 1 |

| 3 | 王无 | 1200 | 1 |

+----+--------+--------+---------+

3 rows in set (0.00 sec)

mysql> update money set version=version+'1' where id=1 and version=1;

Query OK, 1 row affected (0.00 sec)

Rows matched: 1 Changed: 1 Warnings: 0

mysql> select * from money;

+----+--------+--------+---------+

| id | name | number | version |

+----+--------+--------+---------+

| 1 | 张三 | 300 | 2 |

| 2 | 李四 | 1000 | 1 |

| 3 | 王无 | 1200 | 1 |

+----+--------+--------+---------+

3 rows in set (0.00 sec)

目前张三的数据版本为2,我们在客户端A、B分别修改:

在客户端B对该张三数据进行更新为500,其版本号变更为3 。

mysql> update money set number=500,version=version+'1' where id=1 and version=2;

Query OK, 1 row affected (0.01 sec)

Rows matched: 1 Changed: 1 Warnings: 0

mysql> select * from money;

+----+--------+--------+---------+

| id | name | number | version |

+----+--------+--------+---------+

| 1 | 张三 | 500 | 3 |

| 2 | 李四 | 1000 | 1 |

| 3 | 王无 | 1200 | 1 |

+----+--------+--------+---------+

3 rows in set (0.00 sec)

在客户端A中对该张三数据进行更新为600(此时客户端A与客户端B同时操作,因此操作时的版本号为2),版本冲突(客户端B操作后将版本号修改为了3)导致不能更新。

mysql> update money set number=600,version=version+'1' where id=1 and version=2;

Query OK, 0 rows affected (0.00 sec)

Rows matched: 0 Changed: 0 Warnings: 0

当以版本号3进行更新时,客户端A就可以更新数据为600了。

mysql> update money set number=500,version=version+'1' where id=1 and version=2;

Query OK, 0 rows affected (0.00 sec)

Rows matched: 0 Changed: 0 Warnings: 0

mysql> update money set number=600,version=version+'1' where id=1 and version=3;

Query OK, 1 row affected (0.00 sec)

Rows matched: 1 Changed: 1 Warnings: 0

mysql> select * from money;

+----+--------+--------+---------+

| id | name | number | version |

+----+--------+--------+---------+

| 1 | 张三 | 600 | 4 |

| 2 | 李四 | 1000 | 1 |

| 3 | 王无 | 1200 | 1 |

+----+--------+--------+---------+

3 rows in set (0.00 sec)

在库存问题上,我们可以以库存这一字段作为版本标记,直接进行乐观锁确认。