背景
当我们多个java客户端并非操作mysql中某一批数据CURD时,在java客户端层面不加锁的情况下,mysql可能会出现脏写,脏读,不可重复读,幻读的问题,为此mysql设计了事务隔离机制、锁机制、MCVCC多版本控制并发隔离机制一整套机制来解决多事务并发的问题。
事务及其ACID属性
- 事务是由一组SQL语句组成的逻辑处理单元,事务具有以下4个属性,通常简称为事务的ACID属性。
- 原子性(Atomicity) :事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。例如:一个方法开启事务 下单 减库存 增加积分 在操作层面不可分开 ,也就是同时成功,或者者同时失败;
- 一致性(Consistent):在事务开始和完成时,数据都必须保持一致状态。这意味着所有相关的数据规则都必须应用于事务的修改,以保持数据的完整性。例如: 一个方法开启事务 下单 减库存 增加积分 , 数据在一致的状态。
- 隔离性(Isolation):数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的“独立”环境执行。这意味着事务处理过程中的中间状态对外部是不可见的,反之亦然。例如:在 RR(可重复读)级别 下(user表中有id、name字段),A session开启事务去查询user表 ,B session开启事务去更新user表中id为1的name,然后再在A session中再次查询user表,发现id为1的name依然跟第一次查询相同(在下面隔离级别有例子)
- 持久性(Durable) :事务完成之后,它对于数据的修改是永久性的,即使出现系统故障也能够保持。例如:数据已持久化硬盘
并发事务带来哪些问题
- 更新丢失(Lost Update)或脏写(Dirty Writes)
当两个及以上的事务选择同一行,然后基于最初选定的(原)值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题– 最后的更新覆盖了由其他事务所做的更新 。 - 脏读(Dirty Reads)
一个事务正在对一条记录做修改,在这个事务完成并提交前,这条记录的数据就处于不一致的状态;这时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”数据,并据此作进一步的处理,就会产生未提交的数据依赖关系。这种现象被形象的叫做“脏读”。
总结:事务A读到事务B已修改未提交的数据(事务A读取到了事务B已经修改但尚未提交的数据,还在这个数据基础上做了操作。此时,如果B事务回滚,A读取的数据无效,不符合一致性要求。) - 不可重读(Non-Repeatable Reads)
一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变、或某些记录已经被删除了!这种现象就叫做“不可重复读”。
总结:事务A读到事务B已提交的数据,导致事务A内部的相同查询语句在不同时刻读出的结果不一致,不符合隔离性 - 幻读(Phantom Reads)
一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读”。
总结:事务A读取到了事务B提交的新增数据,不符合隔离性
如何解决脏读、不可重复读、幻读的问题
脏读、不可重复读、幻读,其实都是数据库读一致性问题,为此数据库提供一定的事务隔离机制来解决(可参考上图)。
下面分析一下各个隔离级别下可能产生的问题
读未提交
客户端A 和 客户端B都设置隔离级别为读未提交
set tx_isolation='read-uncommitted';
客户端A:
- 第一把读(事务不提交)
- 第二把读(读到客户端B事务未提交插入的数据)
第二把查询到的客户端B新增的数据后,这时客户端B回滚了,那此时客户端A查询到的数据就是脏数据,这时要想解决这个问题就要采用读已提交的隔离级别
客户端B
- 第一把写(事务不提交)
- 第二把回滚事务
读已提交
客户端A和客户端B都设置隔离级别读已提交
set tx_isolation='read-committed';
-
客户端A:
第一把查
第二把查(等客户端B写入后),发现与第一把查的一样,这时解决了脏读的问题。
第三把查(客户端Bcommit后查),这时发现与之前查的新增一条数据,即产生了不可重复读的问题
-
客户端B:
第一把写
第二把执行commit操作
可重复读
客户端A和客户端B都设置隔离级别为可重复读
set tx_isolation='repeatable-read';
-
客户端A
第一把查
第二把查(在客户端B 事务commit之后),可以发现与第一次读的一样,因此解决了不可重复读的问题。
第三把更新 客户端B新增的id为12的数据(更新前后对比一下),通过对比发现,更新id为12的数据后,再次查询多了一条id为12的数据,这时产生了幻读的问题
![在这里插入图片描述](https://img-blog.csdnimg.cn/18d97e743af04628ba682543665edc90.png
- 客户端B
第一把写和commit(待客户端A开启事务查完)
可串行化
客户端A和客户端B都设置隔离级别为串行化
set tx_isolation='serializable';
- 客户端A
第一把开启事务查
- 客户端B
第一把更新id为1的发现被阻塞了(在客户端A开启事务之后),这种虽然解决了幻读问题,但是并发度性极低
锁
表锁:
好处: 每次操作锁住整张表。开销小,加锁快;不会出现死锁。
不足: 锁定粒度大,发生锁冲突的概率最高,并发度最低;一般用在整表数据迁移的场景。
- 手动加表锁
lock table tab_name01 [read | write],tab_name02 [read | write];
- 查看表上加过的锁
show open tables WHERE In_use > 0;
- 删除表锁
unlock tables;
- 案例(加读锁)
从动图上可以看出,对user表手动加读锁之后,其他进程是能直接查询user表的。
从动图上可以看出,对user表手动加读锁之后,其他进程不能直接对表进去写请求。
一句话:手动给表加读锁不阻塞其他进程对user表的读请求,但会阻塞写请求。
- 案例(加写锁)
从动图中可以看出,给user表手动加写锁,其他进程的读请求会被阻塞。
从动图中可以看出,给user表手动加写锁,其他进程的写请求也会被阻塞。
总结:不管是对MyISAM表还是InnoDB表加读锁时,不会阻塞其他进程读请求,但是会阻塞写请求;
加写锁时会阻塞其他进程读写请求。(读不阻塞写,写阻塞读写)
行锁:
好处:每次操作锁住一行数据。开销大,加锁慢;会出现死锁。
不足: 锁定粒度最小,发生锁冲突的概率最低,并发度最高。
- 案例
一个session开启事务更新不提交,另一个session更新同一条记录会阻塞,更新不同记录不会阻塞
间隙锁(Gap Lock)
间隙锁顾名思义就是锁两个索引记录之间的空隙。
案例:
按照表中的数据,间隙就有id为(3,10),(12,正无穷)
当执行SQL:
UPDATE account
SET money = money + 88
WHERE
id > 3
AND id < 8;
那么此时就有间隙锁(3,10),还有行锁 锁住 id为10的这行记录,因此在(3,10]之间的数据不能insert、update,而(3,20]就是临键锁
临键锁(Next-key Locks)
临键锁是行锁和间隙锁的组合。