一、事物的四个性质
- 原子性( Atomicity):一组操作要么全部执行,要么全部都不执行;如果一组操作中的一个操作执行失败,那么所有的操作都要回滚
- 一致性(Consistence):数据的状态在执行一组操作以后,数据状态在执行操作之前和执行操作之后应该是一致的。例子,假如账户A中有3000元,账户B中有2000元,那么在执行操作之前数据的状态是5000元,那么执行账户A向账户B转1000以后,A中的钱是2000,B中的钱是3000,账户的数据状态是5000和事务执行之前相同,因此,我们说这个事务是一致性的。
- 隔离性(Isolation):事物的隔离性表示事物之间相互影响的程度,主要有以下中程度:READ_UNCOMMITED、READ_COMMITED、REPEATABLE_READ、SERIALIZABLE;事物的隔离级别越高,系统越安全,但是系统的并发性能就会下降。
- 持久性(Durability):数据一旦执行了commit,那么就不能通过回滚操作撤销之前的操作。
二、事物的隔离性
2.1、事物的隔离性造成的不同的问题
问题 | 问题描述 |
---|---|
脏读 | 表示一个事务还没有提交,但是另一个事务就可以看到该事物没有提交的更新结果,这样就会造成如果该事物执行回滚,那么第二个事务在此之前看到的就是一个”脏数据”. |
不可重复读 | 同一个事务过程中对同一笔数据进行读取,每次读取的结果都不相同 |
幻读 | 在同一个事务中执行多次查询,多次查询的结果是不一样的。 |
2.2、事务隔离级别
事务隔离级别 | 描述 | 脏读 | 不可重复读 | 幻读 | 安全性 | 并发性 |
---|---|---|---|---|---|---|
READ_UNCOMMITED | 一个事务可以读取另一个事物未提交的数据,这样会造成脏读、不可重复读、幻读的问题出现 | 是 | 是 | 是 | 极低 | 极高 |
READ_COMMITED | 一个事务的更新操作只能在提交以后,才能被其他的事物读取到。这样避免了脏读,无法避免不可重复读和幻读 | 否 | 是 | 是 | 低 | 高 |
READ_REPEATABLE | 保证在同一个事务过程中,同一笔数据读取的结果是相同的;当另一个事物插入数据或这删除数据时,这个对这个事务是不可见的 | 否 | 否 | 是 | 高 | 低 |
SERIALIZABLE | 所有事务按顺序执行 | 否 | 否 | 否 | 极高 | 极低 |
从上面我们也可以看到事务的隔离级别和安全性是成正比的,但是和并发性是成反比的,所以,如果我们的系统在实现的时候需要衡量一下安全性和并发性。
这里需要注意的是,不可重复读和幻读之间的区别,幻读是因为另一个事物对数据表做删除(delete)或者插入(insert)数据,当一个事务插入或者删除数据时会,另一个事物在二不可重复读主要是侧重数据行中信息的修改在另一个事物中可见。
2.3、局部事务
如果当前事务只有一个RM参与,那么我们就称这个事务为局部事务
2.4、全局事务
如果当前事务与多个RM参与,那么这个事务就是全局事务
三、数据库锁
数据库锁一般可以分为两类,一个是悲观锁,一个是乐观锁,悲观锁一般就是我们通常说的数据库锁机制,乐观锁一般是指用户自己实现的一种锁机制,比如hibernate实现的乐观锁甚至编程语言也有乐观锁的思想的应用。
3.1、悲观锁
顾名思义,就是很悲观,它对于数据被外界修改持保守态度,认为数据随时会修改,所以整个数据处理中需要将数据加锁。悲观锁一般都是依靠关系数据库提供的锁机制,事实上关系数据库中的行锁,表锁不论是读写锁都是悲观锁。
3.2、悲观锁按照使用性质划分:
共享锁(Share locks简记为S锁):也称读锁,事务A对对象T加s锁,其他事务也只能对T加S,多个事务可以同时读,但不能有写操作,直到A释放S锁。
排它锁(Exclusivelocks简记为X锁):也称写锁,事务A对对象T加X锁以后,其他事务不能对T加任何锁,只有事务A可以读写对象T直到A释放X锁。
更新锁(简记为U锁):用来预定要对此对象施加X锁,它允许其他事务读,但不允许再施加U锁或X锁;当被读取的对象将要被更新时,则升级为X锁,主要是用来防止死锁的。因为使用共享锁时,修改数据的操作分为两步,首先获得一个共享锁,读取数据,然后将共享锁升级为排它锁,然后再执行修改操作。这样如果同时有两个或多个事务同时对一个对象申请了共享锁,在修改数据的时候,这些事务都要将共享锁升级为排它锁。这些事务都不会释放共享锁而是一直等待对方释放,这样就造成了死锁。如果一个数据在修改前直接申请更新锁,在数据修改的时候再升级为排它锁,就可以避免死锁。
3.3、悲观锁按照作用范围划分:
行锁:锁的作用范围是行级别,数据库能够确定那些行需要锁的情况下使用行锁,如果不知道会影响哪些行的时候就会使用表锁。举个例子,一个用户表user,有主键id和用户生日birthday当你使用update … where id=?这样的语句数据库明确知道会影响哪一行,它就会使用行锁,当你使用update … where birthday=?这样的的语句的时候因为事先不知道会影响哪些行就可能会使用表锁。
表锁:锁的作用范围是整张表。
3.4乐观锁
顾名思义,就是很乐观,每次自己操作数据的时候认为没有人回来修改它,所以不去加锁,但是在更新的时候会去判断在此期间数据有没有被修改,需要用户自己去实现。既然都有数据库提供的悲观锁可以方便使用为什么要使用乐观锁呢?对于读操作远多于写操作的时候,大多数都是读取,这时候一个更新操作加锁会阻塞所有读取,降低了吞吐量。最后还要释放锁,锁是需要一些开销的,我们只要想办法解决极少量的更新操作的同步问题。换句话说,如果是读写比例差距不是非常大或者你的系统没有响应不及时,吞吐量瓶颈问题,那就不要去使用乐观锁,它增加了复杂度,也带来了额外的风险。
3.4、乐观锁实现方式:
版本号(记为version):就是给数据增加一个版本标识,在数据库上就是表中增加一个version字段,每次更新把这个字段加1,读取数据的时候把version读出来,更新的时候比较version,如果还是开始读取的version就可以更新了,如果现在的version比老的version大,说明有其他事务更新了该数据,并增加了版本号,这时候得到一个无法更新的通知,用户自行根据这个通知来决定怎么处理,比如重新开始一遍。这里的关键是判断version和更新两个动作需要作为一个原子单元执行,否则在你判断可以更新以后正式更新之前有别的事务修改了version,这个时候你再去更新就可能会覆盖前一个事务做的更新,造成第二类丢失更新,所以你可以使用update … where … and version=”old version”这样的语句,根据返回结果是0还是非0来得到通知,如果是0说明更新没有成功,因为version被改了,如果返回非0说明更新成功。
时间戳(timestamp):和版本号基本一样,只是通过时间戳来判断而已,注意时间戳要使用数据库服务器的时间戳不能是业务系统的时间。
待更新字段:和版本号方式相似,只是不增加额外字段,直接使用有效数据字段做版本控制信息,因为有时候我们可能无法改变旧系统的数据库表结构。假设有个待更新字段叫count,先去读取这个count,更新的时候去比较数据库中count的值是不是我期望的值(即开始读的值),如果是就把我修改的count的值更新到该字段,否则更新失败。java的基本类型的原子类型对象如AtomicInteger就是这种思想。
所有字段:和待更新字段类似,只是使用所有字段做版本控制信息,只有所有字段都没变化才会执行更新。
3.5、乐观锁几种方式的区别:
新系统设计可以使用version方式和timestamp方式,需要增加字段,应用范围是整条数据,不论那个字段修改都会更新version,也就是说两个事务更新同一条记录的两个不相关字段也是互斥的,不能同步进行。旧系统不能修改数据库表结构的时候使用数据字段作为版本控制信息,不需要新增字段,待更新字段方式只要其他事务修改的字段和当前事务修改的字段没有重叠就可以同步进行,并发性更高。
四、实验
4.1、使用下面的语句查看Mysql的事物隔离级别
查看事务
mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
设置事务隔离级别
mysql> set session transaction isolation level read uncommitted;
Query OK, 0 rows affected (0.00 sec)
下面的实验的实验环境都是打开两个PowerShell中的窗口,并连接了MySQL的环境下执行的,所说的ClientA和ClientB 都是在两个PowerShell中执行的意思。
4.2、验证READ_ COMMITTED
第一步
Client A 使用下面的语句设置隔离级别
mysql> set session transaction isolation level read uncommitted;
Query OK, 0 rows affected (0.00 sec)
Client B 设置事物隔离级别
mysql> set session transaction isolation level read uncommitted;
Query OK, 0 rows affected (0.00 sec)
第二步
Client A 开启事务并执行一次查询
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select *from stu;
+----+------------+-----------+------------+---------+
| id | first_name | last_name | phone | address |
+----+------------+-----------+------------+---------+
| 1 | Ha | NULL | 1345678912 | HeNan |
| 2 | YU | Delaing | 1239877531 | HeNan |
| 3 | YU | Delaing | 1345678912 | HeNan |
| 4 | YU | Delaing | 1345678912 | HeNan |
| 5 | YU | Delaing | 1345678912 | HeNan |
| 6 | YU | Delaing | 1345678912 | HeNan |
+----+------------+-----------+------------+---------+
6 rows in set (0.00 sec)
ClientB 开启事务并执行一个Update操作
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> update stu set first_name='Ha' where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
第三步
Client A在ClientB没有提交事务之前执行一次查询,从结果我们可以看到,ClientA查询到了Client B 尚未提交的结果
mysql> select *from stu;
+----+------------+-----------+------------+---------+
| id | first_name | last_name | phone | address |
+----+------------+-----------+------------+---------+
| 1 | Ha | NULL | 1345678912 | HeNan |
| 2 | YU | Delaing | 1239877531 | HeNan |
| 3 | YU | Delaing | 1345678912 | HeNan |
| 4 | YU | Delaing | 1345678912 | HeNan |
| 5 | YU | Delaing | 1345678912 | HeNan |
| 6 | YU | Delaing | 1345678912 | HeNan |
+----+------------+-----------+------------+---------+
6 rows in set (0.00 sec)
总结:从实验我们就可以看出这里对于事务隔离属性如果设置为READ_UNCOMMITTED,那么就会发生脏读的情况。
4.3、验证READ_COMMITTED
第一步
Client A 使用下面的语句设置隔离级别
mysql> set session transaction isolation level read committed;
Query OK, 0 rows affected (0.00 sec)
Client B 设置事物隔离级别
mysql> set session transaction isolation level read committed;
Query OK, 0 rows affected (0.00 sec)
第二步
ClientA开启事务并执行一次查询
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select *from stu;
+----+------------+-----------+------------+---------+
| id | first_name | last_name | phone | address |
+----+------------+-----------+------------+---------+
| 1 | Yang | NULL | 1345678912 | HeNan |
| 2 | YU | Delaing | 1239877531 | HeNan |
| 3 | YU | Delaing | 1345678912 | HeNan |
| 4 | YU | Delaing | 1345678912 | HeNan |
| 5 | YU | Delaing | 1345678912 | HeNan |
| 6 | YU | Delaing | 1345678912 | HeNan |
+----+------------+-----------+------------+---------+
Client B 开启事务并执行一次update操作
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> update stu set last_name='Jiajiang' where id=1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
第三步
Client A执行一条查询语句,从查询结果我们可以看到,Client A并没有查询到Client B修改且未提交的事物,所以在这种隔离级别下是不会发生脏读的。
mysql> select *from stu;
+----+------------+-----------+------------+---------+
| id | first_name | last_name | phone | address |
+----+------------+-----------+------------+---------+
| 1 | Yang | Qianhua | 1345678912 | HeNan |
| 2 | YU | Delaing | 1239877531 | HeNan |
| 3 | YU | Delaing | 1345678912 | HeNan |
| 4 | YU | Delaing | 1345678912 | HeNan |
| 5 | YU | Delaing | 1345678912 | HeNan |
| 6 | YU | Delaing | 1345678912 | HeNan |
+----+------------+-----------+------------+---------+
第四步
Client B 执行提交事务操作
mysql> commit;
Query OK, 0 rows affected (0.03 sec)
Client A 执行查询操作,从这里我们可以看到,在同一个事务中多次执行查询操作的结果因为另一个事物对表进行修改且提交了事务而导致不可重复读取同一行的数据,造成了不可重复读取的现象。
mysql> select *from stu;
+----+------------+-----------+------------+---------+
| id | first_name | last_name | phone | address |
+----+------------+-----------+------------+---------+
| 1 | Yang | Jiajiang | 1345678912 | HeNan |
| 2 | YU | Delaing | 1239877531 | HeNan |
| 3 | YU | Delaing | 1345678912 | HeNan |
| 4 | YU | Delaing | 1345678912 | HeNan |
| 5 | YU | Delaing | 1345678912 | HeNan |
| 6 | YU | Delaing | 1345678912 | HeNan |
+----+------------+-----------+------------+---------+
4.4、验证 REPEATABLE READ
第一步
Client A设置属性隔离级别
mysql> set session transaction isolation level repeatable read;
Query OK, 0 rows affected (0.00 sec)
Client B设置属性隔离级别
mysql> set session transaction isolation level repeatable read;
Query OK, 0 rows affected (0.00 sec)
第二步
Client A执行开启事务并执行查询
mysql> select *from stu;
+----+------------+-----------+------------+---------+
| id | first_name | last_name | phone | address |
+----+------------+-----------+------------+---------+
| 1 | Yang | Jiajiang | 1 | HeNan |
| 2 | YU | Delaing | 1239877531 | HeNan |
| 3 | YU | Delaing | 1345678912 | HeNan |
| 4 | YU | Delaing | 1345678912 | HeNan |
| 5 | YU | Delaing | 1345678912 | HeNan |
| 6 | YU | Delaing | 1345678912 | HeNan |
+----+------------+-----------+------------+---------+
Client B 开启事务并执行update操作
mysql> update stu set address='AnHui' where id = 1;
第三步
Cliet B执行update后,我们在Client A执行查询操作发现我们Client A并没有读取到Client B修改的数据(在Client B没有提交事务之前)
mysql> select *from stu;
+----+------------+-----------+------------+---------+
| id | first_name | last_name | phone | address |
+----+------------+-----------+------------+---------+
| 1 | Yang | Jiajiang | 1 | HeNan |
| 2 | YU | Delaing | 1239877531 | HeNan |
| 3 | YU | Delaing | 1345678912 | HeNan |
| 4 | YU | Delaing | 1345678912 | HeNan |
| 5 | YU | Delaing | 1345678912 | HeNan |
| 6 | YU | Delaing | 1345678912 | HeNan |
+----+------------+-----------+------------+---------+
6 rows in set (0.00 sec)
第四步
在Client B中执行commit就会提交事务
mysql> commit;
Query OK, 0 rows affected (0.08 sec)
然后在ClientA中再一次执行查询,我们发现Client A中仍然不会读取到Client B修改的数据信息(在Client A 未提交的情况下)
mysql> select *from stu;
+----+------------+-----------+------------+---------+
| id | first_name | last_name | phone | address |
+----+------------+-----------+------------+---------+
| 1 | Yang | Jiajiang | 1 | HeNan |
| 2 | YU | Delaing | 1239877531 | HeNan |
| 3 | YU | Delaing | 1345678912 | HeNan |
| 4 | YU | Delaing | 1345678912 | HeNan |
| 5 | YU | Delaing | 1345678912 | HeNan |
| 6 | YU | Delaing | 1345678912 | HeNan |
+----+------------+-----------+------------+---------+
最后Client A提交事务以后
总结:我们发现REAPEATABLE_READ这个隔离级别避免了脏读和不可重复读取的问题。
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
4.5、验证Serializable
Client A 设置事务隔离级别和开启事务并执行一个查询操作,不提交事务。
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 stu;
+----+------------+-----------+------------+---------+
| id | first_name | last_name | phone | address |
+----+------------+-----------+------------+---------+
| 1 | Yang | Jiajiang | 1 | AnHui |
| 2 | YU | Delaing | 1239877531 | HeNan |
| 3 | YU | Delaing | 1345678912 | HeNan |
| 4 | YU | Delaing | 1345678912 | HeNan |
| 5 | YU | Delaing | 1345678912 | HeNan |
| 6 | YU | Delaing | 1345678912 | HeNan |
+----+------------+-----------+------------+---------+
6 rows in set (0.00 sec)
Client B执行事务开启和查询update操作
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> update stu set first_name where id = 2;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'where id = 2' at line 1
mysql> update stu set first_name='Li' where id = 2;
发现执行一直在等待,当等待一段时间以后出现下面的错误
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
结论:从这里我们可以发现如果事务隔离级别设置成了SERIALIZABLE,那么最终只能有一个事务执行,其他事务都需要等待当前事务执行完毕,否则将会出现超时等待而放弃执行事务。因此这种方式解决了所有的问题,没有脏读、不可重复读、幻读的情况。
五、参考文章
https://www.cnblogs.com/huanongying/p/7021555.html
https://blog.youkuaiyun.com/aluomaidi/article/details/52460844