一、定义
一组操作要么全部成功,要么全部失败,目的是为了保证数据的最终一致性。
二、特性
- 原子性:当前事务的操作要么同时成功,要么同时失败。原子性由undo log日志来实现
- 一致性:使用事务的最终目的,由其他3个特性和业务代码正确逻辑实现
- 隔离性:在事务并发执行时,它们内部的操作不能互相干扰,由锁和MVCC机制来实现
- 持久性:一旦提交了事务,它对数据库的改变是永久性的。持久性由redo log来实现
三、隔离性
- read uncommit(读未提交): 脏读
- read commit(读已提交): 不可重复读
- repeatable read(可重复读): 幻读
- serializable(串行): 读写都串行化执行(读到的数据不允许其他事物修改),解决所有的问题但效率最低。实现原理(select+读锁)
四、持久性
执行顺序如上,redo log可以磁盘顺序写而ibd文件不支持,从而提高性能。
五、并发事务处理带来的问题及解决方案
- 更新丢失或脏写
- 描述:当两个或多个事务选择同一行数据修改,最后的更新覆盖了其他事务的更新。
- 解决1:悲观锁方案,数据库在作数量变更时,不要去到值在java层面运算后重新将运算后的值写入数据库,要直接在数据库层面运算作变更(example: set balance = balance + 500)。
- 解决2:乐观锁方案,加上version版本号。
- 脏读:事务A读取到了事务B已经修改但未提交的数据。
- 不可重复读:事务A内部的相同查询语句在不同时刻读取的结果不一致。
- 幻读
- 描述:事务A读取到了事务B的新增数据。
- 解决:间隙锁。
六、锁机制
读锁(共享锁、S锁)
它是共享的,多个事务可以同时读取一个资源,单不允许其他事务修改(select ... lock in share mode)
写锁(排它锁、X锁)
写锁是排它的,会阻塞其他的写锁和读锁,增删改都会加写锁
七、MVCC机制
多版本并发控制(可提升并发),可以做到读写不阻塞,且避免了脏读问题,通过undo日志链实现
特点
- select操作是快照读(历史版本)
- insert/update/delete 是当前读(当前版本)
- read commit(读已提交),语句级快照
- reapetable read(可重复读),事务级快照
undo日志链
不同的记录都有对应的版本链(写操作),为了回滚数据。
上图中是对同一条数据执行多次修改操作(不同事务间),trx_id表示对应的事务id。roll_pointer是回滚指针,初始指向的是undo日志
可见性算法
解决RC和RR读写同一份数据并发冲突的问题。 注意:begin/start transaction命令不是一个事务的起点,在执行到它们的第一个修改操作或加排它锁操作(selet... for update)的语句,事务才真正启动,才会向mysql申请真正的事务id。
我们可以认为事务id是递增的。
在可重复读隔离级别下,一致性视图是不变的。
当然如果是当前事务肯定是可见的。
在读已提交隔离级别下,一致性视图是变化的。
八、大事务的影响
- 并发情况下,数据库连接池容易被撑爆
- 锁定太多的数据,造成大量的阻塞和超时
- 执行时间长,容易造成主从延迟
- 回滚所需要的时间比较长
- undo log膨胀
- 容易造成死锁
九、事务优化实践原则
- 将查询等数据准备操作放到事务外(RC级别)
- 事务中避免远程调用,远程调用要设置超时,防止事务等待时间太久
- 事务中避免一次性处理太多数据,可以拆分成多个事务分次处理(业务允许)
- 更新等涉及加锁的操作尽可能放在事务靠后的位置(update放在insert后)
- 能异步处理的尽量异步处理(设置超时时间)
- 应用侧(业务代码)保证数据一致性,非事务执行(性能很高,容易出bug,适合简单业务)
十、面试题
1. 读已提交和可重复读要怎么选择?
答:传统的软件公司偏向于OA等系统,对性能要求并不高,且报表场景较多,选择可重复读更合适。而在大型互联网公司更多的要求性能就只有选择读已提交才能把性能最大化。
2. 查询操作方法需要事务吗?
答:只有一个select当然不需要事务了,但涉及到多个select需要考虑业务场景,如果是类似报表的场景,在可重复读隔离级别下,最好加上一个读事务,保证读取到的数据都是同一时刻的数据,一定要根据公司实际业务做选择。