事务
一、概述
- 事务是一组不可被分割执行的SQL语句集合
- 事务成功整体提交 commit,失败整体回滚 rollback
- 事务应该尽可能短,因为长事务会导致长时间无法释放表内行级锁,从而降低系统并发的性能
- 其他相关
- autocommit 自动提交
- MVCC:set autocommit = 1 ; 操作事务完毕后再打开自动提交操作, 避免一致性非阻塞读;即当前select的结果是上一次查询结果的快照
二、事务相关操作命令
begin || start transaction ;
commint ;
rollback ;
set autocommit = 0 ;
commit ; 或 rollback ;
SELECT @@global.tx_isolation; // 全局
SELECT @@session.tx_isolation; // 当前会话
SELECT @@tx_isolation; // 会话中下一个事务的隔离级别
1 row in set
// 设置事务的隔离级别
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}
// 举例:设置当前会话中下一个(未开始的)事务的隔离级别为可重复读
mysql> SET TRANSACTION ISOLATION LEVEL REPEATABLE READ ;
Query OK, 0 rows affected
// 命令参数说明如下:
命令参数 | 解释 | 注意事项 |
---|
GLOBAL | 在全局对从那点开始创建的所有新连接(除了不存在的连接)设置默认事务级别 | 需要SUPER权限 |
SESSION | 为将来在当前连接上执行的事务设置默认事务级别 | 任何客户端都能自由改变会话隔离级别(甚至在事务的中间) |
默认(无参数) | 设置当前会话中下一个(未开始的)事务的隔离级别 | 任何客户端都能自由为下一个事务设置隔离级别 |
三、事务性质
- 原子性(Atomicity):构成事务的的所有操作必须是一个逻辑单元,要么全部执行,要么全部不执行
- 一致性(Consistency):在事务开始和完成时,数据都必须保持一致状态;这意味着所有相关的数据规则都必须应用于事务的修改,以保持数据的完整性;事务结束时,所有的内部数据结构(如B树索引或双向链表)也都必须是正确的;即数据库在事务执行前后状态都必须是稳定的;无非法数据操作,如:外键约束、事务回滚、唯一性
- 隔离性((Isolation):事务之间不会相互影响;事务是独立运行的,一个事务的操作如果影响了另一个事物,那么另一个事务就会撤回执行,要做到事务100%的隔离,需要牺牲速度和性能
- 持久性(Durability):事务执行成功后必须全部写入磁盘;当数据库崩溃之后,InnoDB数据库表驱动会利用日志文件进行数据的重构修改,需要注意的是:安全性和性能速度不可兼得。
四、事务性质的实现原理
- undo 日志:记录数据未更新操作前的数据
与redo log相反,undo log是为回滚而用,具体内容就是copy事务前的数据库内容(行)到undo buffer,在适合的时间把undo buffer中的内容刷新到磁盘。undo buffer与redo buffer一样,也是环形缓冲,但当缓冲满的时候,undo buffer中的内容会也会被刷新到磁盘;与redo log不同的是,磁盘上不存在单独的undo log文件,所有的undo log均存放在主ibd数据文件中(表空间),即使客户端设置了每表一个数据文件也是如此。 - redo 日志:记录数据更新后的数据
redo log就是保存执行的SQL语句到一个指定的Log文件,当Mysql执行recovery时重新执行redo log记录的SQL操作即可。当客户端执行每条SQL(更新语句)时,redo log会被首先写入log buffer;当客户端执行COMMIT命令时,log buffer中的内容会被视情况刷新到磁盘。redo log在磁盘上作为一个独立的文件存在,即Innodb的log文件。 - 原子性、一致性、隔离性,通过 undo 日志实现
- 持久性通过 redo 日志实现
五、多事务运行并发问题常见分类
- 第一类丢失更新(Lost Update):撤销一个事务时, 把其他事务已经提交的更新数据覆盖
时间 | 事务一 | 事务二 |
---|
T1 | 开启事务 | - |
T2 | - | 开启事务 |
T3 | 查询当前账户余额为1000元 | - |
T4 | - | 查询当前账户余额为1000元 |
T5 | - | 存入100元,当前账户余额为1100元 |
T6 | - | 提交事务 |
T7 | 取出100,当前账户余额为900元 | - |
T8 | 撤销事务,数据回滚,当前账户余额恢复1000元 | - |
示例二
- 第二类丢失更新(Lost Update):当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题–最后的更新覆盖了由其他事务所做的更新。
时间 | 事务一 | 事务二 |
---|
T1 | 开启事务 | - |
T2 | - | 开启事务 |
T3 | 查询当前账户余额为1000元 | - |
T4 | - | 查询当前账户余额为1000元 |
T5 | 取出100,当前账户余额为900元 | - |
T6 | 提交事务 | - |
T7 | - | 存入100元,当前账户余额为1100元 |
T8 | - | 提交事务 |
示例七
- 脏数据(Dirty Reads):一个事务正在对一条记录做修改,在这个事务完成并提交前,这条记录的数据就处于不一致状态;这时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”数据,并据此做进一步的处理,就会产生未提交的数据依赖关系。这种现象被形象地叫做”脏读”。即读未提交:事务还没提交,他的修改已经被其他事务看到;
时间 | 事务一 | 事务二 |
---|
T1 | 开启事务 | - |
T2 | - | 开启事务 |
T3 | 查询当前账户余额为1000元 | - |
T4 | 存入100元,当前账户余额为1100元 | - |
T5 | - | 查询当前账户余额为1100元 |
T6 | - | 取出100,当前账户余额为1000元 |
T7 | - | 提交事务 |
T8 | 撤销事务 | - |
示例三
- 不可重复读(Non-Repeatable Reads):同一事务中两个相同SQL读取的内容可能不同。两次读取之间其他事务提交了修改可能会造成读取数据不一致
时间 | 事务一 | 事务二 |
---|
T1 | 开启事务 | - |
T2 | - | 开启事务 |
T3 | 查询当前账户余额为1000元 | - |
T4 | 存入100元,当前账户余额为1100元 | - |
T5 | - | 查询当前账户余额为1100元 |
T6 | - | 提交事务 |
T7 | 查询当前账户余额为1100元 | - |
T8 | 提交事务 | - |
示例四
- 幻读(Phantom Reads):同一个事务突然发现他以前没发现的数据。和不可重复读很类似,不过修改数据改成增加数据。
时间 | 事务一 | 事务二 |
---|
T1 | 开启事务 | - |
T2 | - | 开启事务 |
T3 | 查询当天财务流水:无 | - |
T4 | - | 存入100元,当前账户余额为1100元 |
T5 | - | 查询当前账户余额为1100元 |
T6 | - | 提交事务 |
T7 | 查询当天财务流水:一笔存款100元 | - |
T8 | 提交事务 | - |
// 事务的隔离级别:可重复读,事务1读取指定的where子句所返回的一些行。然后,事务2插入一个新行,这个新行也满足事务1使用的查询where子句。然后事务1再次使用相同的查询读取行,但是现在它看到了事务2刚插入的行。这个行被称为幻象,因为对事务1来说,这一行的出现是不可思议的。详情见示例五
示例五
- 不可重复读与幻读的区别:前者是同一个事务中的两次查询的结果中某一属性字段的值前后不一致,update操作;后者是同一个事务中的两次查询的结果中数据的条数不一致,insert/delete操作
六、事务的隔离级别
概述:
- 为解决多事务运行并发问题,设置事务的隔离级别
- 事务的隔离性是通过锁机制实现的
- 锁机制是通过锁定索引实现,如果查询条件中有主键则锁定主键,如果有索引则先锁定对应索引然后再锁定对应的主键(可能造成死锁),如果连索引都没有则会锁定整个数据表
- 有索引则先锁定对应索引然后再锁定对应的主键:InnoDB的索引实现方式,辅助索引存储的内容是主键索引,主键索引与数据存放在同一个文件中,以主键索引构建成一个B+树,数据存放B+树的叶子终端节点上。
四种隔离级别从低到到依次为:
- 未提交读(READ UNCOMMIT):允许某个事务看到其他事务并没有提交的数据。可能会导致脏读、不可重复读、幻影数据。
- 实现原理:READ UNCOMMIT不会采用任何锁。一般不推荐使用
示例三
- 提交读(READ COMMIT):READ COMMIT允许某个事务看到其他事务已经提交的数据。可能会导致不可重复读和幻影数据。
- 实现原理:数据的读是不加锁的,但是数据的写入、修改、删除加锁,避免了脏读。
示例四
- 可重复读(REPEATABLE READ):确保同一事务的多个实例在并发读取数据时,会看到同样的数据行
- 实现原理:数据的读、写都会加锁,当前事务如果占据了锁,其他事务必须等待本次事务提交完成释放锁后才能对相同的数据行进行操作。
- mysql的InnoDB存储引擎通过多版本并发控制(Multi_Version Concurrency Control, MVCC)机制来解决该问题。在该机制下,事务每开启一个实例,都会分配一个版本号给它,如果读取的数据行正在被其它事务执行DELETE或UPDATE操作(即该行上有排他锁),这时该事物的读取操作不会等待行上的锁释放,而是根据版本号去读取行的快照数据(记录在undo log中),这样,事务中的查询操作返回的都是同一版本下的数据,解决了不可重复读问题。
示例五
- 串行(SERIALIZABLE):这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。只要操作产生了锁,就不允许其他事务读取和修改
- 实现原理:通过强制事务排序,使之不可能相互冲突,就是在每个读的数据行加上共享锁来实现。在该隔离级别下,可以解决前面出现的脏读、不可重复读和幻读问题,但也会导致大量的超时和锁竞争现象,一般不推荐使用
示例六
不同分离级别可能产生的问题
事务隔离级别 | 第一类更新丢失 | 脏读 | 不可重复读 | 幻读 | 第二类更新丢失 |
---|
未提交读 | 不会 | 会 | 会 | 会 | 会 |
提交读 | 不会 | 不会 | 会 | 会 | 会 |
可重复读 | 不会 | 不会 | 不会 | 会 | 不会 |
串行 | 不会 | 不会 | 不会 | 不会 | 不会 |
事务的隔离性越强,并发度越差
- 低级别的隔离级一般支持更高的并发处理,并拥有更低的系统开销
并发控制:
- 服务器级别控制、存储器级别控制
- 方式:加锁(行级别锁、表级别锁)、MVCC机制多版本两阶段封锁协议(Multiversion two-phrase locking protocal)
参考资料