目录
多事务并发执行带来的问题
首先来看一下没有事务隔离的情况下,事务并发执行可能会带来的问题.
- 脏读(一个事务读取了其它事务未提交的数据,然后这个数据后面又北回滚了)
- 不可重复读(一个事务对同样查询条件的数据进行多次查询时,得到的结果不一致)
- 幻读(一个读取到的数据可能是表中不存在的数据)
脏读
即事务1读取了事务2刚刚插入的数据,而事务2并没有commit提交,被中途中断了,那么事务2的事务管理会进行回滚,这时事务1读取的数据在数据库中其实不存在,而事务1确实读到了那一行数据,这行数据就被称为脏数据,这种读取也被称作脏读.
事务1 | 事务2 |
---|---|
set autocommit=0//关闭自动提交 | set autocommit=0//关闭自动提交 |
set tx_isolation='read-uncommitted'//设置事务隔离级别为读未提交,即最低,关闭隔离级别(注:mysql版本8以上的,改为transaction_isolation) | set tx_isolation='read-uncommitted'//设置事务隔离级别为读未提交,即最低,关闭隔离级别 |
begin//开始事务 | begin//开始事务 |
select * from regions; | 不做事情 |
可以看见第一次查询的数据 | 不做事情 |
不做事情 | insert into regions(region_name) values ('上海');//添加一条上海数据 |
再次执行上面的查询语句,发现上海已经有了.此时事务2的事务并没有提交. | rollback;//执行回滚 |
再次执行,发现上海消失了,也就是说数据库中并不存在上海这条数据,被回滚了.那么对于第二次查询而言,这条数据就是脏数据,第二次这个操作也就是脏读! |
不可重复读
即同样的查询语句查询两次,结果是不一样的.
事务1 | 事务2 |
set transaction_isolation='read-commit'; //设置隔离级别为读已提交,可以防止上面脏 //读的操作,只读已经被其他事务提交的数据 | set transaction_isolation='read-committed'; |
set autocommit=0; | set autocommit=0; |
begin; | begin; |
select * from regions; | |
查询到regions表的内容如图 | |
insert into regions(region_name) values('shanghai');//更新update操作也可以,总之去修改数据 | |
commit;//提交 | |
select * from regions;//再次查询 | |
可以看见,shanghai已经出现了,那么事务1,并没有提交,是同一个事务,一次事务里面两次查询操作读取到的数据不同,就是不可重复读 | |
commit; |
幻读
这是一个解决了不可重复读之后出现的一个问题!
为了解决不可重复读我们会将隔离级别设置为'repeatable-read',可重复读的,这种可重复读保证了我们在一个事务中第二次读取到的数据和第一次读取到的数据是同一个数据,相当于缓存了一波.这样就会带来一个问题,在第一次查询或者第二次查询中间,别的事务删除了数据,那么就会导致读取到一条不存在的数据.
事务1 | 事务2 |
set transaction_isolation='repeatable-read'; | set transaction_isolation='repeatable-read'; |
set autocommit=0; | set autocommit=0; |
begin; | begin; |
select * from regions; | |
查询得到regions表结果 | |
delete from regions where region_name='shanghai'; | |
commit;//删除sahnghai并提交 | |
select * from regions;//再次查询 | |
会发现上海依旧存在 | select * from regions; |
commit;//提交后续再进行查询,会发现shanghai已经不存在 | 事务2查询会发现,上海实际上已经不存在了. |
上海不存在 |
表中所示的就是所谓的幻读,读出来的数据是虚幻的,不存在的数据.
那么要解决幻读就需要将事务隔离提高到
serializable
串行化的.
至此,事务隔离级别以及会导致的问题都已经讲完,那么是不是无脑的使用串行就可以了呢?
并不是!串行导致并发性能极低,应该根据自己数据库情况使用,在并发读取量大的数据库使用高级别的隔离,在读取少写入多的数据库使用低隔离级别,参考读写分离数据库.其次就是设高级别隔离,也不适合设置的过高,一般读已提交就行,不建议串行化,并发效率太低了.其他问题完全可以交给业务去处理,比如读取到的数据不存在,前端对该数据进行修改删除等,先去判断一下数据存不存在,不存在就返回不存在信息,并且自动刷新一下界面,删除该数据就行了.
事务实现原理 (数据库锁)
事务隔离在底层主要通过数据库读写锁与一致性快照读(mvcc多版本并发控制)来实现.读写锁用来实现读已提交,和串行化,一致性快照读用于实现可重复读.
MVCC即多版本并发控制,通过读以前的版本获得数据,来降低并发的冲突,主要是重复读取!尽量减少阻塞,提高并发效率!
数据库锁
锁的分类
1.从性能上讲可以分为乐观锁和悲观锁。
2. 从操作类型上讲可以读锁(共享锁)和写锁(排它锁)
3. 从数据操作粒度上来讲可以分为全局锁、表锁、行锁、间隙锁(between 100 and 200)、...
乐观锁与悲观锁
乐观锁即乐观地认为不存在多个线程同时操作一个数据,因此默认是不上锁.每次读数据都乐观认为别人不会修改.只在每次读数据的时候判断一下而已,判断一下别人有没有改.适用于多读数据库,即读的并发量很大,使用乐观锁利于提高吞吐量!
悲观锁即悲观的认为,一定有人同时在操作数据,默认就要上锁!每次读数据都认为别人会改,因此为了避免这种情况,加锁,知道我读完了,别人才能拿到这个锁,进行他们的操作.传统数据库种的锁大部分都是这种悲观锁,比如行锁,表锁,读锁,写锁等等,java中synchronized关键字采用的也是悲观锁!
读锁与写锁(共享锁与排他锁)
读锁也叫共享锁,即当前事务可以读不可以写,但是别人只能读,写的话会被阻塞.写锁也叫排他锁,即当前事务可以读写,别人读都不行!
可以这么理解---加了读锁,就是告诉别人 我在读数据,你们写的话先等等,读的话就读吧.加了写锁就是说,我在写写数据,你们先别读,可能会错的!所以先别读,写的话也先等等,等我先写完读完!
全局锁
全局锁是要关闭所有打开的表。在MySQL可以使用全局读锁锁定所有表。
相对应的也有全局写锁.对应到读锁定义,效果是一样的只是相当于作用域不一样,全局读锁,就为整个数据库所有表加上读锁.
需要注意全局锁就是全局的读锁,没有全局的写锁,只能让数据库变只读,不能变只写!不能读的数据库,失去意义了!只写数据库纯属瞎扯淡!
1. 加全局读锁 (flush tables with read lock): 只能读库中所有表的数据,但是不能写。
2. 解锁 unlock tables
可以看见加了读锁后读操作并没有被限制
可以看见更新数据的操作被阻止了.
表锁
表锁即施加在单个表上的读/写锁.当然你可以对多个表加多个锁!
读锁(lock table regions read):所有线程可以读,当先线程写会出错,其它线程写会阻塞。
事务1 对regions加表读锁.
可以看见事务1对regions表的读没有问题,写就报错了.当前线程只能读不能写,因为别人可能在读,当然不允许写.
再看看事务2
事务2的读操作也是正常的.看看写
敲下回车后发现没有操作成功,没有行受影响.这个操作被阻塞的,但是没有报错!只是阻塞,当事务1的锁解开,事务2马上就可以进行这个更新语句了.
可以看到,事务1解锁之后,事务2马上就执行了更新操作!
写锁(lock table regions write):当前线程可以读写,其它线程读写要阻塞。
写锁类似,类比一下.也就是事务1如果对regions加了写锁,事务1可以对regions进行读写,而事务二的读写都会被阻塞.但是不会报错!
行锁
行锁就是针对表中的某一行进行锁定。
对行加行读锁(共享锁,s锁)
共享锁,又称为S锁,允许当前事务读取一行,阻止其它事务获取相同数据集的排它锁。
sql语句(注意只是mysql,InnoDB中select默认不加锁)
select * from regions where id=13 lock in share mode
加了行读锁之后,其他线程事务将不能对该行再进行更改,也不能加写锁,但是其他事务线程可以读,也可以同时加读锁.
对行加行写锁
排它锁又称为X锁,允许当前事务更新数据,阻止其它事务获取相同数据集的共享锁和排它锁。
sql语句
select * from regions where id=13 for update
事务1加了行写锁之后,当前事务1可以读写,事务2的读写都会被阻塞,并且无法加读/写锁.
MVCC(多版本并发控制)
前面已经讲过.
MVCC即多版本并发控制,通过读以前的版本获得数据,来降低并发的冲突,主要是重复读取!尽量减少阻塞,提高并发效率!
那么为什么使用mvcc能提高并发效率?
就是因为解决可重复读需要添加排他锁,也就是事务1在写的时候,事务2就不能读,来保证读取数据的一致性,但是排他锁会大大降低并发效率,那么我们就采取了mvcc,读取历史版本的内容,这样就能保证数据一致性,增大并发量,这未尝是一个好办法,但是也因此产生了幻读的问题.(幻读不知道的翻前面去看看).
MVCC的底层逻辑是如何实现的呢?
MVCC的实现原理主要依赖于记录中的undolog(回滚日志),ReadView(读视图),三个隐藏字段来实现的.
mvcc三个隐藏字段
1. DB_TRX_ID:记录创建这条记录或者最后一次修改该记录的事务id
2. DB_ROLL_PTR:回滚指针,指向这条记录的上一个版本,用于配合undolog实现数据的回滚.
3. DB_ROW_ID:隐藏的主键,如果数据表没有主键,那么innodb会自动生成一个row_id。
-
什么是ReadView?
对于Read Committed和Repeatable Read的隔离级别,都要读取已经提交的事务数据,也就
是说如果版本链中的事务没有提交,该版本的记录是不能被读取的,那哪个版本的事务是可以读取
的,此时就引入了ReadView.
-
ReadView中包含什么?
-
1. m_ids: 截止到当前事务id之前,所有活跃的事务id(还没有commit的事务。)。例如m_ids[1,2,3,4] 2. min_trx_id: 记录以上活跃事务id中的最小值。例如 1; 3. max_trx_id: 保存当前事务结束后应分配的下一个id值。例如 5; 4. creator_trx_id: 保存创建ReadView的当前事务id。例如 4;
主要通过 DB_ROLL_PTR指向undolog(回滚日志)中的版本链中的上一个版本.
当db_trx_id等于Readview中的creator_trx_id,说明他在读的是直接修改的数据,当然可以正常读取.
当db_trx_id小于Readview中的min_trx_id说明,修改该数据的事务早在创建该readview的事务之前就已经提交了,可以正常读.
当db_trx_id大于Readview中的max_trx_id说明,修改该数据的事务在创建该readview的事务之后才提交,所以该readview的事务都不应该读取该数据.应该通过DB_ROLL_PTR和undolog日志找寻上一个版本数据,再次执行判断,直到可以读取.
如果访问的版本的db_trx_id属性值在min_trx_id和max_trx_id之间 ,就需要判断一下db_trx_id的值是不是在m_ids列表中,如果在,说明创建ReadView时,生成的该版本的事务还是活跃的,该版本不可以访问,如果不存在,则说明创建ReadView时,生成该版本的事务已经提交则可以读取.