一、并发带来的问题
在多个线程同时开启事务后对数据库进行访问时,不可避免就会出现多个线程之间交叉访问而导致数据的不一致,通常存在以下问题:
1.1 脏读(读取未提交数据)
如下图,事务2读取了事务1未提交的数据。
举个栗子:
假设一个秒杀火车票的场景,目前上海到新化的火车票库存只有1张票了,张三(事务1)和李四(事务2)同时发起抢票,张三抢到了1张票,事务1正在扣除库存还没有提交入库,事务2此时查询发现还有余票(读到库存仍然为1),李四也可能买到了1张票,导致库存与销量不一致。
1.2 不可重复读(前后多次读取,数据内容不一致)
在同一事务中,两次读取同一数据,得到内容不同。
例如事务1读取了某个数据,然后事务2更新了这个数据并提交,然后事务1又来读取了一次,那这两次读取的结果就会不一样。
注意:不可重复读与幻读的区别在于:
不可重复读是针对于多次读取同一条数据出现不同结果,幻读是多次读取而产生的记录数不一样。
1.3 幻读(前后多次读取,数据总量不一致)
在一个事务的两次查询中数据记录数不一致,例如有一个事务1查询了几行数据,而事务2在此时插入了新的几行数据,事务1在接下来的查询中,就会发现有几行数据是它先前所没有的。
1.4 更新丢失
当两个事务选择同一行,然后更新数据,由于每个事务都不知道其他事务的存在,就会发生丢失更新的问题。
举个栗子:
比如一个公司账号下有两个员工子账号,张三和李四分别登录后,两人同时发起修改公司名称(分别对应事务1和事务2),两个事务同时提交时就发生了覆盖,张三就会发现刚才修改的公司名称xx公司变成了yy公司(李四的提交结果)。
二、解决方案——设置隔离级别
为解决上述问题,可以给数据库设置相应的隔离级别,保证各线程数据获取的准确性。
有以下4种隔离级别:
|隔离级别|读数据一致性|脏读|不可重复读|幻读
|:—:|:—:|:—:|:—:|:—:|
|未提交读|最低级别,只保证不读取物理上损坏的数据|有|有|有|
|已提交读|语句级|无|有|有|
|可重复读|事务级|无|无|有|
|可串行化|最高级别,事务级|无|无| 无|
- READ_UNCOMMITTED :未提交读,允许脏读,不可重读,幻读。
- READ_COMMITTED:已提交读,仅允许读取已提交的数据,即不能脏读,但是可能发生不可重读和幻读。
- REPEATABLE_READ :可重复读,不可读脏,可重复读(即多次读取数据一致),但是可能发生幻读。
- SERIALIZABLE :串行化事务,保证不读脏,不幻读,可重复读,事务隔离级别最高,但是并发冲突最高。
MySQL默认的隔离级别为:repeatable-read
(可重复读),不允许脏读,但允许幻读。
隔离级别越高越严格,内部工作机制越复杂,较低的隔离级别通常支持更高的并发。
在MySQL数据库中查看当前事务的隔离级别,使用命令select @@tx_isolation
,也可以使用命令show variables like "%tx_isolation%"
,如下:
1 | mysql> select @@tx_isolation; |
在MySQL数据库中设置事务的隔离级别:
1 | set tx_isolation='read-uncommitted'; //允许脏读,不可重读,幻读。 |
凡是DDL语句(涉及到表结构的语句),都是自带commit,比如create table 、alter table、truncate,皆不受事务控制。
三、隔离级别演示
创建测试数据库,添加测试数据
1 | SET NAMES utf8mb4; |
3.1、未提交读 READ_UNCOMMITTED
session 1 | session 2 |
---|---|
set session transaction isolation level read uncommitted; | set session transaction isolation level read uncommitted; |
select @@tx_isolation; | select @@tx_isolation; |
select * from tb_account; | select * from tb_account; |
update tb_account set balance=balance-10 where id = 1; | |
select * from tb_account; | select * from tb_account; |
commit; |
【小结】
由上面的操作可以得出,事务2在更新后commit前,事务1可以实时查询到数据的变化,称之为事务2未提交读。
一旦事务2因为某种原因回滚了,更新的操作被撤销,那事务1查询到的数据,就为脏数据,所以未提交读(READ_UNCOMMITTED)会导致脏读。在实际项目中,有多个事务在同时更新同一个数据,而且事务之间是不知道对方有没有回滚的,很容易导致脏读,所以这个时候可以采用“已提交读(READ_COMMITTED)”隔离级别。
3.2、已提交读 READ_COMMITTED
session 1 | session 2 |
---|---|
set session transaction isolation level read committed; | set session transaction isolation level read committed; |
select @@tx_isolation; | select @@tx_isolation; |
select * from tb_account; | select * from tb_account; |
update tb_account set balance=balance-10 where id = 1; | |
select * from tb_account; | select * from tb_account; |
commit; | |
select * from tb_account; | |
commit; |
【小结】
由上面的操作可以得出,事务1读到的数据,始终是事务2更新后commit后的数据,不会脏读,解决了“未提交读 READ_UNCOMMITTED”带来的脏读问题,但又产生了新的问题:在事务1的commit操作之前,导致了事务1的不可重复读(也就是在事务1范围内(提交前),多次读取的数据不一样)。
3.3、可重复读 REPEATABLE_READ
session 1 | session 2 |
---|---|
select @@tx_isolation; | select @@tx_isolation; |
start transaction; | start transaction; |
select * from tb_account; | select * from tb_account; |
update tb_account set balance=balance-10 where id = 1; | |
select * from tb_account; | select * from tb_account; |
commit; | |
select * from tb_account; | |
commit; | |
select * from tb_account; |
【小结】
由上面的操作可以得出,事务1在开启事务后,就打上版本号了,在事务1做commit操作前,事务1中所有的查询操作都是快照数据,不受事务2的更新影响,解决了前面两个级别的缺陷:脏读和不可重复读,做到了可重复读。但还有一个问题未解决:幻读,如果此时事务2插入一条新记录,那么事务1多次读取时,记录条数就不一样,MySQL默认的隔离级别就是“可重复读 REPEATABLE_READ ”,可见这种情况还是能被容忍的。如果脏读、不可重复读、幻读要全部解决,只能采用最高级别了——SERIALIZABLE 串行化事务。
可重复读,是由MVVC (Multi-Version Concurrency Control) 基于多版本的并发控制协议,只有在InnoDB引擎下存在。https://www.cnblogs.com/hirampeng/p/9944200.html
3.4、串行化事务 SERIALIZABLE
session 1 | session 2 |
---|---|
set session transaction isolation level serializable; | set session transaction isolation level serializable; |
start transaction; | start transaction; |
select * from tb_account; | |
(已锁表) | insert into tb_account values (3, ‘zhangsan’, 100);, 100); |
commit; | 等待 |
执行成功 Query OK, 1 row affected (0.00 sec) | |
commit; |
【小结】
由上面的操作可以得出,事务1查询数据时,先锁住了表,事务2插入一条记录只能等待,如果等待时间过长,会提示ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
,MySQL中事务隔离级别为serializable时做任何操作都会先锁表,因此不会出现幻读的情况,这种隔离级别并发性极低,往往一个事务霸占了一张表,其他成千上万个事务只能干瞪眼,得等它用完(提交后)才可以使用,开发中很少会用到。
思考:
已提交读和可重复读是通过什么协议做到的?
答:是通过MVVC (Multi-Version Concurrency Control)协议做到的,中文名为:基于多版本的并发控制协议,只有在InnoDB引擎下存在。MVCC只工作在REPEATABLE READ和READ COMMITED隔离级别下。InnoDB在每行数据都增加两个隐藏字段,一个记录创建的版本号,一个记录删除的版本号。
参考:https://www.cnblogs.com/hirampeng/p/9944200.html
参考:
https://www.cnblogs.com/fengyumeng/p/9852735.html
https://blog.csdn.net/qq_34975710/article/details/81077178
https://www.jb51.net/article/116477.htm
https://www.jianshu.com/p/081a3e208e32 例子举的很通俗
https://www.cnblogs.com/wyc1994666/p/11367051.html mysql 事务的实现原理,讲得非常细致
https://blog.csdn.net/w_linux/article/details/79666086 MySQL——事务(Transaction)详解