事务是什么
事务比较常见数据库操作,例如银行金融业务,涉及多个操作,并保证操作要么全部成功要么全部失败,这样保证数据的一致性。事务支持是在引擎层实现的,事务存在以下特性:原子性、一致性、隔离性、持久性;
- 原子性:原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚
- 一致性:一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态
- 隔离性:隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离
- 持久性:持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作
当数据库上有多个事务同时执行的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题,接下来我们看下不同的事务隔离级别以及每种隔离级别的作用
事务隔离级别有哪些
读未提交
Read uncommmited:读未提交,顾名思义,就是一个事务可以读取另一个未提交事务的数据。
● 事例:老板要给程序员发工资,程序员的工资是3.6万/月。但是发工资时老板不小心按错了数字,按成3.9万/月,该钱已经打到程序员的户口,但是事务还没有提交,就在这时,程序员去查看自己这个月的工资,发现比往常多了3千元,以为涨工资了非常高兴。但是老板及时发现了不对,马上回滚差点就提交了的事务,将数字改成3.6万再提交。
● 分析:实际程序员这个月的工资还是3.6万,但是程序员看到的是3.9万。他看到的是老板还没提交事务时的数据。这就是脏读。
● 问题:这种事务隔离级别可导致脏读,使用读提交,能解决脏读问题。
读已提交
Read committed:读提交,顾名思义,就是一个事务要等另一个事务提交后才能读取数据
● 事例:程序员拿着信用卡去享受生活(卡里当然是只有3.6万),当他买单时(程序员事务开启),收费系统事先检测到他的卡里有3.6万,就在这个时候,程序员的妻子要把钱全部转出充当家用,并提交。当收费系统准备扣款时,再检测卡里的金额,发现已经没钱了(第二次检测金额当然要等待妻子转出金额事务提交完)。程序员就会很郁闷,明明卡里是有钱的…
● 分析:这就是读提交,若有事务对数据进行更新(UPDATE)操作时,读操作事务要等待这个更新操作事务提交后才能读取数据,可以解决脏读问题。但在这个事例中,出现了一个事务范围内两个相同的查询却返回了不同数据,这就是不可重复读
● 问题:这种隔离级别可导致不可重复读;那怎么解决可能的不可重复读问题?Repeatable read
可重复读
Repeatable read:重复读,就是在开始读取数据(事务开启)时,不再允许修改操作
● 事例:程序员拿着信用卡去享受生活(卡里当然是只有3.6万),当他买单时(事务开启,不允许其他事务的UPDATE修改操作),收费系统事先检测到他的卡里有3.6万。这个时候他的妻子不能转出金额了。接下来收费系统就可以扣款了。
● 分析:可重复读隔离级别可以解决不可重复读问题。写到这里,应该明白的一点就是,不可重复读对应的是修改,即UPDATE操作。但是可能还会有幻读问题。因为幻读问题对应的是插入INSERT操作,而不是UPDATE操作。
● 问题:导致幻读问题;什么时候会出现幻读?事例:程序员某一天去消费,花了2千元,然后他的妻子去查看他今天的消费记录(全表扫描FTS,妻子事务开启),看到确实是花了2千元,就在这个时候,程序员花了1万买了一部电脑,即新增INSERT了一条消费记录,并提交。当妻子打印程序员的消费记录清单时(妻子事务提交),发现花了1.2万元,似乎出现了幻觉,这就是幻读。
串行
Serializable 序列化:是最高的事务隔离级别,在该级别下,事务串行化顺序执行,可以避免脏读、不可重复读与幻读。“写”会加“写锁”,“读”会加“读锁”,当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行,这种事务隔离级别效率低下,比较耗数据库性能,一般不使用
【注意】大多数数据库默认的事务隔离级别是Read committed,比如Sql Server , Oracle。Mysql的默认隔离级别是Repeatable read
事务隔离之可重复读如何实现
这里主要说下可重复读的实现,首先可重复读是利用undo log来记录,不同时刻启动的事务会有不同的 read-view,Undo log是InnoDB MVCC事务特性的重要组成部分。当我们对记录做了变更操作时就会产生undo记录,Undo记录默认被记录到系统表空间(ibdata)中,但从5.6开始,也可以使用独立的Undo 表空间。
案例分析:事务 A 第一次读完之后,事务 B 要修改这行数据了。undo log 会为所有写操作生成日志,所以就会生成一条 undo log 日志,并且它的 roll_pointer 会指向上一条 undo log 日志。
紧接着,事务 A 第二次去读这行数据了,情况如下图所示:
第一次读的时候,开启事务 A 的时候就生成了一个 ReadView-R,此时事务 A 第二次去查询的时候,先查到的是 trx_id = 18 的那条数据,它会发现 18 比最小的事务编号 10 大。那就说明事务编号为 18 的事务,有可能它是读不到的。
接着就要去 m_ids 里确认是否有 18 这条数据了。发现有 18,那就说明在事务 A 开启事务的时候,这个事务是没有提交的,它修改的数据就不应该被读到。事务 A 就会顺着 roll_pointer 指针继续往下找,找到了 trx_id = 8 这条日志,发现这条能读,读到的值任然是 x,与第一次读到的结果一致。成功实现可重复读!
常见问题
1.可重复读的使用场景举例? 对账的时候应该很有用?
对于已经开启的事务,读视图已经建立,这时对账过程中出现更新不影响对账;
2.并发版本控制(MCVV)的概念是什么, 是怎么实现的?
是通过保存数据在某个时间点的快照来实现并发控制的。也就是说,不管事务执行多长时间,事务内部看到的数据是不受其它事务影响的,根据事务开始的时间不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的;
可以认为 多版本并发控制(MVCC) 是行级锁的一个变种,但是它在很多情况下避免了加锁操作,因此开销更低。虽然实现机制有所不同,但大都实现了非阻塞的读操作,写操作也只锁定必要的行;
3.使用长事务的弊病? 为什么使用常事务可能拖垮整个库?
长事务意味着系统里面会存在很老的事务视图,由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就导致了大量占用内存。在MySQL5.5以前的版本,回滚日志是和数据字典一起放在ibdata文件里的,即使长事务提交,回滚段被清理,文件也不会变小,最终往往为了清理回滚段而重建整个库。除了影响回滚段,长事务还会占用锁资源,也有可能拖垮整个库。
4.commit work and chain的语法是做什么用的?
提交当前事务,并开始下一个事务
5.怎么查询各个表中的长事务?
select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60;
7.如何避免长事务的出现?
在一个事务里面, 避免一次处理太多数据
在一个事务里面,尽量避免不必要的查询
在一个事务里面, 避免耗时太多的操作,造成事务超时。一些非DB的操作,比如rpc调用,消息队列的操作尽量放到事务之外操作
参考文献
https://www.cnblogs.com/cnbk/p/13043872.html
http://mysql.taobao.org/monthly/2015/04/01/
https://time.geekbang.org/column/article/68963
https://blog.csdn.net/itworld123/article/details/115207304
https://segmentfault.com/a/1190000037557620
https://www.cnblogs.com/trunks2008/p/15213918.html
https://segmentfault.com/a/1190000037557620
https://segmentfault.com/a/1190000023273980