基本特性
事务的基本特性,概括说来就4点:ACID
A:atomicity事务的原子性,保证了一个事务要不全部成功,要不回滚,全部失败
C:consistency事务的一致性,保证了数据库总是从一个一致性的状态转换到另一个一致性的状态
I:isolation事务的隔离性,保证了一个事务执行完毕前,对其他事务不可见
D:durability事务的持久性,保证事务一旦提交,做的修改会永久保存在数据库之中
举几个栗子:
比如小胡打算给阿伟转账100块软妹币,不巧的是:当小胡账户刚减少100元时,而这100元还没有到阿伟账户时,系统不争气的崩溃了。小胡并不用急,这时,事务的原子性会使得事务进行回滚,回到事务未开始的状态,在这个转账的过程中,将会回到小胡账户未更改的时刻;
小胡仍然不死心,再次向阿伟进行转账,在同一时刻阿伟等不及了,去看了一下自己的账户余额,发现账户金额还没有增长,便去质问小胡,此时小胡去看了一下自己的账户,发现余额确实减少了,两人就此争论了起来,但事务的隔离性就能避免这种情况的发生;
在转账完成后,系统又一次的宕机了,这时候,阿伟也并不用担心转账会消失,因为事务的持久性保证了转账是永久有效的,不会因为故障导致数据丢失。
隔离级别
当然,虽然说事务拥有隔离性,但是并不是说所有的事务都是完成隔离的,隔离是有四个不同的级别的:
1读未提交(read uncommit):能读到其他事务未提交的数据,也叫脏读
时间点 | 事务1 | 事务2 |
---|---|---|
1 | 开始事务 | 开始事务 |
2 | 账户开始为0元,加100元 | |
3 | 查询账户,结果为100元 | |
4 | 提交 |
比如在该表中,事务1未提交的数据被事务2所读取到,如果在事务1提交之前,系统崩溃,事务进行回滚,事务2读取到的显然是错误的信息,这就是脏读
2读已提交(read commit):只能读取其他事务已提交的数据,但会导致多次读取到的结果不一样,叫做不可重复读
时间点 | 事务1 | 事务2 |
---|---|---|
1 | 开始事务 | 开始事务 |
2 | 查询账户,为0元 | |
3 | 账户加100元 | |
4 | 提交事务 | |
5 | 查询账户余额,为100元 |
如上表,事务1读取了两次数据,很明显这两次的结果是不同的,说明事务1在执行过程中有其它事务对该数据的修改,这样肯定也是不合适的结果,需要对这种不可重复读的机制进行进一步的改善。
3可重复读(repeatable read):多次读取的数据一致,这是mysql的默认级别,但会导致另一个问题:幻读。幻读是指事务在前后读取一个数据时,两次查询的行数不同
时间点 | 事务1 | 事务2 |
---|---|---|
1 | 开始事务 | 开始事务 |
2 | 查询账户余额为100的账户,为1 | |
3 | 增加一个新账户,余额为100 | |
4 | 提交事务 | |
5 | 再次查询账户余额为100的账户,为2 |
4串行(serializable):也叫序列化,是最极端的一种隔离机制,所有事务会一个一个的按顺序执行,对每一个数据进行加锁,会导致大量的超时问题和锁竞争的问题
那么,事务是如何保证自己的几个特性呢
ACID的实现
原子性:由undo log日志保证,undo log记录了需要回滚的日志信息,事务回滚时撤销已经执行成功的sql语句
undo log:名为回滚日志,是实现原子性的关键,当事务回滚时能够撤销所有已经成功执行的sql语句,他需要记录你要回滚的相应日志信息。
例如
当插入时需要记录这条数据,回滚时删除这条数据
当删除时记录删除的该条数据,回滚时进行插入
当修改时记录之前的旧值,回滚时修改为原值
隔离性:MVCC保证
首先,了解MVCC是什么东西:MVCC全称:Multi-Version Concurrency Control,多版本并发控制。能够很好的处理读写冲突。
其主要依赖于每行记录的隐藏字段:
自增ID,若记录没有主键,将会将其作为记录的主键
最近修改事务ID:记录了最后修改该事务的ID
回滚指针:记录这条记录的上一个版本
所以,对于每一条记录,完整的表格应该是这样:
name | money | DB_ROW_ID主键 | DB_TRX_ID事务ID | DB_ROLL_PTR回滚指针 |
---|---|---|---|---|
小胡 | 100 | 1 | null | null |
阿伟 | 100 | 2 | null | null |
除此之外,还需要undo log(日志)和read view(读视图)
我们用一次转账来解释undo日志的使用:
按照上述表格,如果小胡账户增加100时:
首先数据库会对该行加锁
然后把该行数据拷贝到undo log中,作为旧记录,既在undo log中有当前行的拷贝副本
拷贝完毕后,修改该行money为200,并且修改隐藏字段的事务ID为当前事务1的ID, 我们默认从1开始,之后递增,回滚指针指向拷贝到undo log的副本记录,既表示上一个版本就是它
事务提交后,释放锁
name | money | DB_ROW_ID主键 | DB_TRX_ID事务ID | DB_ROLL_PTR回滚指针 |
---|---|---|---|---|
小胡 | 100 | 1 | 1 | * |
阿伟 | 100 | 2 | null | null |
而undo log中表为:
name | money | DB_ROW_ID主键 | DB_TRX_ID事务ID | DB_ROLL_PTR回滚指针 |
---|---|---|---|---|
小胡 | 100 | 1 | null | null |
而如果该行记录再次进行修改时,undo log会继续增加
name | money | DB_ROW_ID主键 | DB_TRX_ID事务ID | DB_ROLL_PTR回滚指针 |
---|---|---|---|---|
小胡 | 100 | 1 | 1 | * |
name | money | DB_ROW_ID主键 | DB_TRX_ID事务ID | DB_ROLL_PTR回滚指针 |
---|---|---|---|---|
小胡 | 100 | 1 | null | null |
事务对同一条记录的更改,在undo日志中会是一条链表的形式储存undo log的链首就是最新的旧记录,链尾就是最早的旧记录。
read view:
read view就是事务进行快照读操作的时候生产的读视图,在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID
read view拥有三个全局属性
一个list列表,储存系统正活跃着的ID
up_limit_id,记录列表中事务ID最小的ID
low_limit_id,readview生成时刻系统尚未分配的下一个事务ID
read view可以判断事务的可见性,对于任意一个事务,将会判断其能见到哪一个版本的记录,为此,执行以下判断:
1、判断DB_TRX_ID是否小于up_limit_id,小于则表示当前事务能看到DB_TAX_ID所表示的该版本的记录,否则进入下一步
2、判断DB_TRX_ID是否大于low_limit_id,大于或等于则说明比当前系统中事务的最大ID都要大,表示事务无法观测到当前记录,否则进入下一步
3、如果DB_TRX_ID在当前列表中,表示记录还未提交,事务也是无法看到该记录,反之,说明该记录所在事务已提交,可以观测到记录
持久性:由内存和redo log来保证,mysql修改数据时会在内存和redo log中记录这次操作,事务提交redo log进行记录,宕机时从redo log中恢复。
一致性:由代码层面保证,和其他三者都有一定关系