(一)事务四大属性
分别是原子性、一致性、隔离性、持久性。
1、原子性(Atomicity)
原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚,因此事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响。
2、一致性(Consistency)
一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。举例来说,假设用户A和用户B两者的钱加起来一共是1000,那么不管A和B之间如何转账、转几次账,事务结束后两个用户的钱相加起来应该还得是1000,这就是事务的一致性。
3、隔离性(Isolation)
隔离性是当多个用户并发访问数据库时,比如同时操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。关于事务的隔离性数据库提供了多种隔离级别,稍后会介绍到。
4、持久性(Durability)
持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。例如我们在使用JDBC操作数据库时,在提交事务方法后,提示用户事务操作完成,当我们程序执行完成直到看到提示后,就可以认定事务已经正确提交,即使这时候数据库出现了问题,也必须要将我们的事务完全执行完成。否则的话就会造成我们虽然看到提示事务处理完毕,但是数据库因为故障而没有执行事务的重大错误。这是不允许的。
(二)事务的并发问题
脏读:事务A读取到了事务B中未提交的数据,当事务B回滚后,事务A读取到的数据都是脏数据。
不可重复读:事务A多次读取同一数据,但每次读取的值不同。这是因为事务B多次对数据进行更新和提交。
幻读:事务A在操作过程中,表数据多次新增或删除,并且新增或删除的数据影响到事务A的操作结果。
不可重复读侧重的是数据的修改,幻读侧重的是数据的新增或删除。解决不可重复读,只有锁住行,而解决幻读,需要锁住整张表。
(三)mysql的隔离级别
数据库事务的隔离级别有4个,由低到高依次为Read uncommitted(读未提交)、Read committed(读提交)、Repeatable read(可重复读取)、Serializable(可串行化),这四个级别可以逐个解决脏读、不可重复读、幻象读这几类问题。
事务隔离级别 |
脏读 |
不可重复读 |
幻读 |
未提交读 |
√ |
√ |
√ |
提交读 |
× |
√ |
√ |
可重复读 |
× |
× |
√ |
串行化 |
× |
× |
× |
隔离级别的实现:
未提交读(RU:read-uncommitted):在RU级别中,事务读到的所有数据都是最新的数据,可能是事务提交后的数据,也可能是事务执行中的数据(可能会被回滚)。
当隔离级别为RU时:
-
所有的读不加锁,读到的数据都是最新的数据,性能最好。
-
所有的写加行级锁,写完释放。
提交读(RC:read-committed):使用MVCC技术,在每一行加入隐藏的字段(DB_TRX_ID:修改该行的最后一个事务的id,DB_ROLL_PTR:指向当前行的undo log日志,DB_ROW_ID:行标识,DELETE_BIT:删除标志),它实现了不加锁的读操作。
当隔离级别为RC时:
-
写操作:加行级锁。事务开始后,会在UNDO日志中写入修改记录,数据行中的隐藏列DATA_POLL_PTR存储指向该行的UNDO记录的指针。
-
读操作:不加锁。在读取时,如果该行被其它事务锁定,则顺着隐藏列DATA_POLL_PTR指针,找到上一个有效的历史记录(有效的记录:该记录对当前事务可见,且DELETE_BIT=0)。
可重复读(RR:repeatable-read):使用MVCC技术来实现不加锁的读操作。
当隔离级别为RR时:
-
写操作:加行级锁。事务开始后,会在UNDO日志中写入修改记录,数据行中的隐藏列DATA_POLL_PTR存储指向该行的UNDO记录的指针。
-
读操作:不加锁。在读取时,如果该行被其它事务锁定,则顺着隐藏列DATA_POLL_PTR指针,找到上一个有效的历史记录(有效的记录:该记录对当前事务可见,且DELETE_BIT=0)。
从上面可以知道:实际上RC和RR级别的操作基本相同,而不同之处在于:行记录对于当前事务的可见性(可见性:即哪个版本的行记录对这个事务是可见的)。RC级别对数据的可见性是该数据的最新记录,RR基本对数据的可见性是事务开始时,该数据的记录。
1) 行记录的可见性(read_view)的实现
在innodb中,创建一个事务的时候,会将当前系统中的活跃事务列表创建一个副本(read_view),里面存储着的都是在当前事务开始时,还没commit的事务,这些事务里的值对当前事务不可见。read_view中有两个关键值 up_limit_id(当前未提交事务的最小版本号-1,在up_limit_id之前的事务都已经提交,在up_limit_id之后的事务可能提交,可能还没提交) 和 low_limit_id(当前系统尚未分配的下一个事务id,也就是目前已出现过的事务id的最大值+1。注意:low_limit_id不是最大的活跃事务的id。)
注意:当前事务和正在commit的事务是不在read_view中的。
2)无论是RC级别还是RR级别,其判断行记录的可见性的逻辑是一样的。
当该事务要读取undo中的行记录时,会将行记录的版本号(DB_TRX_ID)与read_view进行比较:
1. 如果DB_TRX_ID小于up_limit_id,表示该行记录在当前事务开始前就已经提交了,并且DELETE_BIT=0,则该行记录对当前事务是可见的。
2. 如果DB_TRX_ID大于low_limit_id,表示该行记录在所在的事务在本次事务创建后才启动的,所以该行记录的当前值不可见。
3. 如果up_limit_id< = DB_TRX_ID <= low_limit_id,判断DB_TRX_ID是否在活跃事务链中,如果在就是不可见,如果不在就是可见的。
4. 如果上面判断都是不可见的,则读取undo中该行记录的上一条行记录,继续进行判断。
而对于RC级别的语句级快照和RR级别的事务级快照的之间的区别,其实是由read_view生成的时机来实现的。RC级别在执行语句时,会先关闭原来的read_view,重新生成新的read_view。而RR级别的read_view则只在事务开始时创建的。所以RU级别每次获取到的都是最新的数据,而RR级别获取到的是事务开始时的数据。
3)值得注意的是: 在上面的可见性判断中,虽然逻辑是一样的,但是实际意义上是有区别的:
在第二步中,对于RC级别来说,low_limit_id是执行语句时已出现的最大事务id+1,可以认为在执行语句时,是不存在事务会比low_limit_id要大,所以大于low_limit_id的事务都是不可见的。而对于RR级别来说,low_limit_id是当前事务开始时已出现的最大事务+1(也可以认为是当前事务的id+1,因为在创建当前事务时,当前事务的id最大),大于low_limit_id的事务表示是在该事务开始后创建的,所以对RR级别是不可见。
在第三步中,对于RC级别来说,只要DB_TRX_ID不在活跃链表中,则无论DB_TRX_ID是否大于事务id,RC都是可见的。而对于RR级别来说,因为low_limit_id就是当前事务id+1,可以认为小于low_limit_id的事务都是在当前事务创建前出现的,所以也只需要简单判断DB_TRX_ID是否在活跃链表中。
串行化(serializable):读写都会加锁。
(四)Mysql中的锁
锁分类
Mysql为了解决事务并发,数据安全问题,使用了锁机制。
按照锁的粒度可以把锁分为表级锁和行级锁:
1) 表级锁
Mysql中粒度最大的一种锁,会锁住当前操作的整张表,并发性能低,但表锁的实现简单,耗费资源少,加锁快,不会出现死锁。
2) 行级锁
Mysql中粒度最小的一种锁,只会锁住当前操作的数据行。行锁极大地提高了Mysql的并发性能,但行锁的开销较大,速度较慢,会出现死锁。
按照锁的性质可以把锁分为共享锁和排它锁:
1) 共享锁(S锁)
其他事务可以读取被共享锁锁住的数据行,不能修改该数据行,并且也只能对该数据行加共享锁,而不能加排它锁。
2) 排它锁(X锁)
当一个事务对数据行加上排他锁,那么该事务可以读取和修改该数据行,而其他事务不允许对该数据行加任何锁。
表锁
Mysql中表锁除了共享锁和排他锁之外,还存在着两种锁:意向共享锁(IS),意向排他锁(IX)。
意向锁的作用是表明该事务想对该表加一个共享/排他锁,但并没有真正把锁加上去。比如,当事务想对一个被排他锁锁住的表加上共享锁/排他锁时,必须先在该表上添加一个意向共享锁/意向排他锁,直到锁住表的排他锁被释放。
事务在给一个数据行加共享锁前必须先取得该表的IS锁,在加排它锁前必须先取得该锁的IX锁。并且意向共享锁可以同时并存多个,但是意向排他锁同时只能有一个存在。
行锁
InnoDB行锁是通过给索引上的索引项加锁来实现的,这意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁。
Innodb中的行级锁有以下几种:
1) Record Lock: 对索引项加锁,锁定符合条件的行。其他事务不能修改和删除加锁项;
2) Gap Lock: 对符合条件范围的“间隙”加锁,锁定记录的范围,不包含索引项本身。其他事务不能在锁范围内插入数据。“间隙(GAP)”是指 键值在条件范围内但并不存在的记录。
3) Next-key Lock: 锁定索引项本身和间隙。Record Lock和Gap Lock的结合,Next-key Lock就是我们所说的间隙锁,可解决幻读问题。
举例来说,假如emp表中只有101条记录,其empid的值分别是 1,2,...,100,101,下面的SQL:
Select * from emp where empid > 100 for update;
是一个范围条件的检索,InnoDB不仅会对符合条件的empid值为101的记录加锁,也会对empid大于101(这些记录并不存在)的“间隙”加锁。
在使用范围条件检索并锁定记录时,InnoDB这种加锁机制会阻塞符合条件范围内键值的并发插入。
注意
1) InnoDB 存储引擎在 REPEATABLE-READ(可重读) 事务隔离级别下使用的是Next-Key Lock 锁,解决的幻读的问题,因此InnoDB 引擎的默认的隔离级别 REPEATABLE-READ(可重读) 已经达到了SQL标准的 SERIALIZABLE(可串行化) 隔离级别,并且事务不需要串行化。
2) 当查询的索引含有唯一属性时,将next-key lock降级为record key。
3) Innodb中行级锁是加在索引上,所以只有使用索引时,才会加行锁,否则只会加表锁。但这并不意味着,只要使用了索引就会加行级锁,如果MySQL认为全表扫描效率更高,比如对一些很小的表或者索引范围包括大部分表数据,它就不会使用索引,这种情况下InnoDB将使用表锁,而不是行锁。
死锁
不同于MyISAM总是一次性获得所需的全部锁,InnoDB的锁是逐步获得的,当两个事务都需要获得对方持有的锁,导致双方都在等待,这就产生了死锁。 发生死锁后,InnoDB一般都可以检测到,并使一个事务释放锁回退,另一个则可以获取锁完成事务,我们可以采取以上方式避免死锁:
1) 通过表级锁来减少死锁产生的概率;
2) 多个程序尽量约定以相同的顺序访问表;
3 )同一个事务尽可能做到一次锁定所需要的所有资源。