
mysql 事物篇
一. 事物的定义
以一种可控的方式来访问数据库资源的一组操作, 事物应该满足 ACID 原子性 一致性 隔离性 永久性
原子性: 要么全部成功 要么全部失败
一致性: 执行前后, 整体结果的一致的, A转100到B A要扣去100 B要增加100 执行前后两个账户总和是一致的!
隔离性: 主要是针对并发时, 多个事务之间应该有隔离程度. 事物隔离度越高, 系统并发性越差, 数据一致性越能保证. 总的来说, 一致性的考虑应该优于系统性能
对于数据库来说, 不一定支持所有的事级别, 默认的事物级别也不同. 例如Oracle 只支持 Read Committed 和 Serializable . EJB Spring jdbc 可以指定四种隔离级别, 但是最终使用什么级别还是得根据使用的数据库来决定.持久性: 事物一旦提交成功, 那么本次操作将被记载不可逆转.
二. 四种事物隔离级别
主要面临的问题
脏读 (Dirty Read)会读取到别的事物尚未提交的数据, 如果别的事物回滚, 那么自身事物将会读取到脏数据 (开启读已提交解决) 不可重复读(Non-repeatable Read) 不可重复读读取是值同一个事物在读取同一条数据的时候, 多次读取到的结果都不一样
例如: A事物读取到 B事物尚未开始更新前是1, B事物完成更新后了是2 , A读取的都是未提交的事物, 但是结果都不同 (开启可重复读解决)幻读(Phantom Read) 在查询整体结果集的时候, 两次读取的结果不一样. 也就是针对多条数据的读取结果不一致 (间隙锁或串行化解决)
对应的解决办法 四种隔离级别
Read committed(读提交) 隔离性最低的策略 , 三种问题都会出现 Read Uncommitted (读未提交) 大多数数据库采用的策略 ,读取已提交的数据, 可避免 脏读 Repeatable Read (可重复读 Mysql使用MVCC) 保证在本次事物内, 针对一条数据, 怎么读取都是一致的. 但是针对整体的数据集就没法保证了(无法避免幻读) Servilazible 串行化 最严格的级别 , 每个事物都是串行化依次进行, 可避免所有问题, 但是性能最差 .
三. MVCC底层实现
1. 问题: 读已提交的级别下出现不可重复读
大多数数据库默认在不同事物下, 读取都是读取已提交的数据.
当对同一条数据重复读取两次时, 中间对此数据进行了修改, 则前后两次读取(读取已提交)的结果将会出现不同, 这就是不能对同一条事物重复读取问题.
2. 解决思路: 目的是为了确保同一事务的多个实例在并发读取数据时, 能看到同样的数据.
解决方案1:LBCC 采用读写锁思想进行读写分离(即串行化), 保证当前事物如果对本条数据进行加锁, 则其他事物无法进行修改。
解决方案2:Mysql-innoDB的解决方式: - MVCC 多版本并发控制 MVCC使得数据库读不会对数据加锁,普通的SELECT请求不会加锁,提高了数据库的并发处理能力
MVCC只是一个概念而不是具体的实现, 其核心理念就是数据快照,不同的事务访问不同版本的数据快照,从而实现不同的事务隔离级别。
3. InnoDB 的MVCC实现
InnoDB 的 MVCC 通过在每行记录后面保存两个隐藏的列, 一列存行事物ID, 一列存行回滚指针. 每个事物只要影响到此行的话, 都会将当前事物id 放入影响行的事物id列中
这样我们就能通过事物id来比较当前行的最新事物状态
同时也会将旧记录叠加到一个undo log 版本链中

4. 回滚段 undo log
MVCC 的实现, 通过undo log 来回溯本次事物可见的版本.
根据行为的不同,undo log分为两种:insert undo log 和 update undo log
insert undo log
在inset 操作时, 将会产生一条 undo log. 由于插入操作对其他事物本来就是不可见的, 所以这条记录将会在事物提交后马上删去

update undo log
当执行 delete 和 update 操作的时候将会产生update undo log. 这种类型的undo log 会被MVCC使用, 所以提交后并不会马上删除, 会有一个purge线程单独进行回收

update 的时候, 首先会进行排它锁进行加锁, 在对行进行复制到undo log 更新事物id和回滚指针再释放锁
为了保证并发的操作, 每条语句都有自己独立的undo log回滚组, 每次事物要对某行进行update都会进行行锁保证当前行只会有一个事务进行产生update undo log的产生
多个事务参与过后, 就会形成一条有顺序的版本链

5. ReadView 活跃事物列表
读未提交:直接读最新版本的数据, serializable串行化直接加悲观锁. 而对于 读已提交和可重复度的级别, 则需要使用ReadView来判断当前事物的可见版本是哪一个
ReadView 中主要包含当前系统中还有哪些活跃的读写事务,把它们的事务id放到一个列表中,我们把这个列表命名为为m_ids(如下图)

判断当前事物可见性
如果被访问版本的 trx_id 属性值小于 m_ids 列表中最小的事务id,表明生成该版本的事务在生成ReadView 前已经提交,所以该版本可以被当前事务访问。 如果被访问版本的 trx_id 属性值大于 m_ids 列表中最大的事务id,表明生成该版本的事务在生成ReadView 后才生成,所以该版本不可以被当前事务访问。 如果被访问版本的 trx_id 属性值在 m_ids 列表中最大的事务id和最小事务id之间, 那就需要判断一下 trx_id 属性值是不是在 m_ids 列表中如果在,说明创建 ReadView 时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。
READ COMMITTED 每次读取数据前都生成一个ReadView快照,记录此刻的ReadView
ReadView快照 最小值还小的trx_id 是可见的, 意味着这条undo log之前的事物了 ReadView快照 记录中存在trx_id 则不可见, 意味着这条undo log的事物还未提交
REPEATABLE READ 在事务开始后 第一次读取数据时生成一次ReadView快照, 此后一直沿用次快照
ReadView快照 最小值还小的trx_id 是可见的, 意味着在第一次查询开始前就已经提交了 ReadView快照 记录中存在trx_id 则不可见, 意味着在第一次查询开始的时候这条undo log的事物还未提交
总结: 每次读取一次生成一次ReadView快照的时候, 就会定格这一刻的所有活跃事物, 追溯undo log 中的trx_id就能判断版本链中的可见性了
对于 READ COMMITTED 每次查询都重新生成快照, 意味着每次都读取最新提交的数据; 对于 REPEATABLE READ在第一次读取的时候生成, 意味着只读取一开始完成事物的数据;





