事务就是保证一组数据库操作,要么全部成功,要么全部失败。事务的实现是在引擎层, 因此我们说的是InnoDB的事务。
为何需要事务?
比如有一个转钱的业务,A给B转100, 那么就是两条sql语句,一个是A的钱包减去100元,一个是B的钱包加100元。如果没有事务,A减少100元以后,mysql机器断电了,B加钱的操作没有执行,那么就会出现A少了钱而B没有加钱的情况。如果用了事务,在mysql重启的时候,通过log就会把A这100元加回去,保证了数据的可靠性。
事务的特点
ACID (Atomicity, Consistency, Isolation, Durability) 原子性,一致性,隔离性,持久性。
原子性: 多个数据库操作要么全部成功,要么全部失败。
一致性: 数据库在执行事务前后数据都是具有一致性的。比如A 300 元 B 300 元,这是一个一致性的数据,执行了转账后A 200 元 B 400元也是一致性数据, 但是A 200元 B 300元就不是一致性的,这是要避免的。
隔离性: 并发执行的事务不会相互影响,这个会受到不同隔离级别设置而产生影响。
持节性:事务提交后对数据库的更新是持久的,不能回滚,系统故障后也不会丢失数据。
实现事务上述能力的主要技术就是日志文件(redo log, undo log),锁和MVCC。
事务的原子性是通过undo log(回滚日志)实现的
事务的持久性是通过redo log 来实现的
事务的隔离性是通过(读写锁+MVCC)来实现的
事务的一致性是通过原子性,持久性,隔离性来实现的。
Redo log
mysql不是每次都把修改实时同步到磁盘,因为这会很慢,mysql会先把修改存到缓冲池里面(内存),然后才会用后台线程去把缓冲池的东西刷到磁盘里面。这就是WAL技术。
redo log记录的是新数据的备份,记录的是事务对数据页的修改,在事务提交以前,把Redo log持久化了就行,就算commit以后系统立刻断电,有redo log就可以恢复数据。
Undo Log
回滚日志,记录数据被修改前的信息,记录数据的逻辑变化,这样事务出现错误要回滚的时候就可以通过回滚日志来回滚之前的操作。
锁
表锁,行锁,读写锁。
这个和多线程中概念是一样的。
两个事务同时修改同一行记录,获得锁的才能改,没获得锁的就得等获得锁的事务执行完成(commit 或者rollback)。
MVCC
事务开始的时候会有一个事务号,这个号是唯一存在的,叫做transaction Id, 严格递增。
表里面的每一行数据也是有多个版本的,每次事务更新数据的时候,都会生成一个新的数据版本。
并且把 transaction id 赋值给这个数据版本的事务 ID,记为 row trx_id
举个例子
一开始k的值是1,三个事务1,2,3分别修改k为2,3,4这时这三个事务还都没提交,row上也会先把这三次修改记录下来,箭头反着画是因为undo log记录的是修改之前的记录。
确定一下现在的状态是事务1,2,3依次修改k, 然后都没有提交。隔离级别是可重复读。
在这之后事务1去读k, 首先看到的是k=4 trx_id=3这个记录,发现trx_id比自己大,这就是不可读的,然后通过undo log读到k=3这次记录,也是不可以读的,再往前知道发现k=2这一行和自己的事务id一致,就读到了。隔离性就是这么做的。
原子性
简单来说原子性就是对了继续,错了回滚。
每条数据变更操作(insert/update/delete)操作都伴随一条undo log的生成,回滚日志必须先于数据持久化到磁盘上。
回滚就是在调用一条反向的sql语句,delete变insert, insert变delete, update的逆向update。
持久性
提交事务时,数据先写缓冲池,然后后台线程才会写到磁盘上。如果事务提交后缓冲池数据还没有写到磁盘,mysql断电了,那么数据就丢了。
所以mysql在提交事务前就要把redo log写到磁盘上,这样才能保证持久性。
这里有一点说明一下,事务没提交,也会把数据写到缓冲区然后刷到磁盘,就是上面那个图花的那种结构。
隔离性和隔离级别
当数据库上多个事务同时执行的时候,可能出现脏读,不可重复读,幻读的问题。隔离级别就可以解决这些问题。隔离的越强,效率就越低。
SQL的事务隔离级别包括了:
读未提交: 事务没提交,变更也能被其他事务看到。
读提交: 事务提交了,变更才会被其他事务看到。
可重复读: 一个事务执行过程中看到的数据,和这个事务启动时看到的数据是一致的,未提交的数据其他事务也是看不见的。
串行化: 读写锁,阻塞了就得等持有锁的事务执行完成。
举个例子, 假设这两个事务访问的都是同一个数据表的同一行数据的k1。k1的初始值是1
事务A | 事务B |
启动事务 | 启动事务 |
查询值k1,得到值1 | 查询k1,得到了值1 |
把k2改成2 | |
查询k1 值v1 | |
提交事务 | |
查询k1 值v2 | |
提交事务 | |
查询k1 值v3 |
在不同的隔离级别下,V1,V2,V3的值是不同的。
读未提交:v1=2, v2=2, v3=2
读提交: v1=1 v2=2 v3=2 b事务提交了以后A事务才能看到。
可重复读: v1=1 v2=1 v2=2 事务A读到的数据是事务开始时候的数据。
串行化:事务B执行将1改成2时,这一行记录已经被A事务加锁 (行锁,读锁),B要修改需要得到写锁,所以就阻塞了,直到提交了以后A才会继续执行。v1=v2=1 v3=2
一致性
上面的原子性,持久性,隔离性做到了,一致性也就做到了。
事务如何保证转账业务
这里说一下我们开头提到的业务。流程是
start 事务
set A.balance = A.balance -100
set B.balance = B.balance +100
commit
在运行第二句的时候,mysql会先把redo log写到磁盘上,然后写缓冲区,缓冲区再往磁盘上刷数据。
在第三句之前第二句之前mysql断电了,这时要分情况
redo log还没写,这就等于第二句没跑,不用恢复。
redo log有了,缓冲区还没写断电了,这时这个redo log里面没有commit标记,说明这个事务没有正常完成,要回滚,根据redo log把数据改回去就可以了。