事务是数据库执行的最小单位,如果一个包含多个步骤的业务操作,被事务管理,那么这些操作要么同时成功,要么同时失败
开启事务:begin / start transaction
提交:commit
回滚:rollback
设置保存点:savepoint(配合rollback使用)
自动提交(autocommit = 1)
手动提交(autocommit = 0)
MySQL中事务默认自动提交:MySQL一条DML语句就提交一次事务
Oracle中事务默认手动提交:Oracle执行完DML语句要commit
-- 查看事务的默认提交方式 1--自动提交 0--手动提交 select @@autocommit; -- 修改事务的默认提交方式 set @@autocommit=0;
start transaction; delete from account where id = 25; savepoint a;-- 设置保存点 delete from account where id = 28; rollback to a;-- 回滚到保存点 # 回滚到保存点,保存点搭配rollback使用
ACID | 事务的四大特性 |
---|---|
原子性(atomicity) | 一个事务是一个不可分割的最小工作单元,事务中的所有操作要么全部执行成功,要么全部失败,不会出现有部分操作执行成功的情况 |
一致性(consistency) | 事务必须让数据库从一个一致性状态到另一个一致性状态,从两个人都是1000块钱的状态,到一个是500一个人是1500的状态,不会出现中间状态 |
隔离性(isolation) | 事务在提交之前的所有操作对其它事务是不可见的,一个人是500块另一个人1500块,并且未提交之前,别人看到的还是两人都是1000块 |
持久性(durability) | 事务一旦提交,事务里所有对数据的修改会持久的保存到数据库中(此时即使系统崩溃,修改的数据也不会丢失) |
多个事务操作同一批数据,则会引发一些问题,设置不同的隔离级别可以解决这些问题
事务的隔离级别设置不当会导致事务事务失去隔离性
丢失更新(脏写):简单说就是 交叉修改同一个数据,没有加写锁
第一类丢失更新: A事务撤销事务,覆盖了B事务提交的事务(现代关系型数据库中已经不会发生)
第二类丢失更新: A事务提交事务,覆盖了B事务提交的事务(是不可重复读的特殊情况)
脏读:一个事务读取到另一个事务中没有提交的数据
不可重复读(虚读): 两次读取同一数据,得到内容不同(强调内容不同),不包括被本身事务自己所修改
幻读:同一事务中,用同样的操作读取两次,得到的记录数不相同(强调数量不同)
read uncommitted:读未提交
产生的问题:脏读、不可重复读、幻读
read committed:读已提交(Oracle默认)
产生的问题:不可重复读、幻读
repeated read:可重复读(Mysql默认)
产生的问题:幻读
serializable:串行化: 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰
可以解决所有问题
隔离级别 | 脏写 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|
Read uncommitted | × | √ | √ | √ |
Read committed(Oracle默认) | × | × | √ | √ |
Repeatable read(MySQL默认) | × | × | × | √ |
Serializable | × | × | × | × |
备注 : √ 代表可能出现 , × 代表不会出现
注意: 隔离级别从小到大安全性越来越高,但效率越来越低,要选择合适的隔离级别保证相对安全的同时效率较高
-- 查询数据库的隔离级别 select @@transaction_isolation; -- 设置数据库的隔离级别,设置之后,要重新打开mysql才会生效 set GLOBAL transaction isolation level Read uncommitted; SET session TRANSACTION ISOLATION LEVEL Serializable;
Mysql数据库在可重复读的隔离级别下没有幻读,通过行锁 + 间隙锁
因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是READ-COMMITTED(读取提交内容)
但是InnoDB 存储引擎默认使用 REPEATABLE-READ(可重读)并不会有任何性能损失。
InnoDB 存储引擎在 分布式事务 的情况下一般会用到SERIALIZABLE(可串行化)隔离级别。
事务隔离级别的实现基于锁机制 和 MVCC
数据库事务有不同的隔离级别,不同的隔离级别对锁的使用是不同的,锁的应用最终导致不同事务的隔离级别
问题1:如果一个进程在读某一行的数据的过程中,另一个在进程又往这一行里面写数据(改、删),那结果会是怎样?
问题2:如果两个进程都同时对某一行数据进行修改,以谁的更改为准?
解决方案:在开启事务后,如果是写操作,给写操作的这一行数据加一个写锁,如果是读,就给该行数据一个读锁,读完或者写完之后立马释放锁。这样之后,在一个线程修改该行数据的时候,不让其他进程对该行数据有任何操作,解决了多个进程操作数据库出现的丢失更新问题
这个解决冲突的方案叫做: 读未提交,这也就是事务的第一个隔离级别。
缺点:事务未提交就释放了锁,其他进程在修改数据的进程释放写锁之后读到的数据就是未提交的数据,如果事务回滚的话,刚才读的数据就是无效数据,也就是脏数据
问题3: 如何解决读取脏数据的问题?
解决方案:把释放锁的时机调整到事务提交之后,此时在事务提交前(整个过程都有排它锁-->行级锁),其他进程是无法对该行数据进行任何操作 ,包括读,效率极低,于是在这里Mysql使用了一个并发版本控制机制,叫做MVCC
这个解决冲突的方案叫做:读已提交,这也是事务的第二个隔离级别。
MVCC通俗的也就是说:mysql为了提高系统的并发量,在事务未提交前,虽然事务内操作的数据是锁定状态,但是另一个事务仍然可以读取数据的 快照版本。
缺点:Mysql虽然锁住了正在操作的数据行,但它仍然不会阻止另一个事务往表的其他行插入数据。
问题4:为了防止在同一事务中两次读取数据不一致,(包括不可重读和幻读),接下来该如何继续做呢?
解决方案:mysql依然采取的是MVCC并发版本控制来解决这个问题,还是读取的快照数据 ,具体是:如果事务中存在多次读取同样的数据,MySQL第一次读的时候仍然会保持选择读最新提交事务的数据,当第一次之后,之后再读时,mysql会取第一次读取的数据作为结果。这样就保证了同一个事务多次读取数据时数据的一致性。
Mysql把这种解决方案叫做:可重复读 (Repeatable-Read),也就是事务的第三个隔离级别
Mysql在可重复读的隔离级别下可以解决幻读(Mysql的默认隔离级别是可重复读,它默认开启了间隙锁,所以可以解决幻读问题)
注意:事务的隔离级别降到Read Commited就没有间隙锁了,只有可重复读隔离级别下才有间隙锁
产生幻读的原因是,行锁只能锁住行,但是新插入记录这个动作,要更新的是记录之间的“间隙”。因此,为了解决幻读问题,InnoDB 只好引入新的锁,也就是间隙锁 (Gap Lock)。 间隙锁是Innodb在可重复读提交下为了解决幻读问题时引入的锁机制
在可重复读隔离级别下,数据库是通过next-key lock(行锁 + 间隙锁)来实现的
当前读通过间隙锁的方式解决了幻读。间隙锁是锁住一段范围。比如id大于2,那间隙锁会把id大于2的这个范围全部都上锁。这个时候别人就不能往id大于2这个范围里插入数据了。从而解决了幻读。
innodb自动使用间隙锁的条件 (1)必须在Repeatable Read级别下
(2)检索条件必须有索引(没有索引的话,mysql会全表扫描,那样会锁定整张表所有的记录,包括不存在的记录,此时其他事务不能修改不能删除不能添加)
next-key lock加锁规则有以下特性
加锁的基本单位是next-key lock,他是前开后闭原则
插入过程中访问的对象会增加锁
索引上的等值查询--给唯一索引加锁的时候,next-key lock升级为行锁
索引上的等值查询--向右遍历时最后一个值不满足查询需求时,next-key lock 退化为间隙锁
唯一索引上的范围查询会访问到不满足条件的第一个值为止
因为在第一个事务中进行更新数据时,会使用 行锁 + 间隙锁 将表锁住了,所以在第二个事务中插入操作被阻塞;只有当第一个事务提交后,第二个事务中的插入操作才能被执行;
最高隔离级别:串行化
该隔离级别会自动在锁住你要操作的整个表的数据(加表级锁),如果另一个进程事务想要操作表里的任何数据就需要等待获得锁的进程操作完成释放锁(事务与事务之间完全串行执行)。可避免脏读、不可重复读、幻读的发生。
当然性能会下降很多,会导致很多的进程相互排队竞争锁。
多版本并发控制( Multi-Version Concurrency Control )
解读: 多版本的意思就是数据库中同时存在多个版本的数据,即某一条记录的多个版本同时存在,在某个事务对其进行操作的时候,需要查看这一条记录的隐藏列事务版本id,比对事务id并根据事物隔离级别去判断读取哪个版本的数据。
MVCC只在可重复读 和 读已提交这两个隔离级别下工作,其他的两个隔离级别和MVCC不兼容
读未提交总是读取最新行,而不是符合当前事务版本的数据行(读到的是未提交的数据)
而串行化则会对所有的读取行都加锁(不需要MVCC,同一个时刻只有一个事务能操作表,非常安全)
MVCC的实现原理依赖于记录中的三个隐藏字段、undo log和read view来实现
DB_ROW_ID:隐含的自增ID(隐藏主键),当我们没有显示设置主键,将会以这个隐藏列作为主键
DB_TRX_ID:用来存储每次对某条聚集索引记录进行修改的时候的事务id(就是最近的对它做了修改的那个事务的事务id)
DB_ROLL_PTR:roll_point就是存储了一个指针,配合undo log指向这条索引记录上一个版本的位置
对记录每次更新后,都会将旧值放到一条undo日志中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被DB_ROLL_PTR属性连接成一个链表,我们把这个链表称之为版本链,版本链的头节点就是当前记录最新的值(使用头插法插入数据)。另外,每个版本中还包含生成该版本时对应的事务id (注意插入操作的undo日志没有这个属性,因为他没有老版本)
被称为回滚日志
undo log主要分为两种
insert undo log 代表事务在insert
新记录时产生的undo log
,只在事务回滚时需要,并且在事务提交后可以被立即丢弃
update undo log 事务在进行update
或delete
时产生的undo log
; 不仅在事务回滚时需要,在快照读时也需要;所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被purge
线程统一清除
说白了Read View就是事务进行快照读操作的时候生产的读视图
读取数据时通过一种类似快照的方式将数据保存下来,这样读锁和写锁就不冲突了,不同的事务session会看到自己特定版本的数据
当每个事务开启时,都会被分配一个事务ID, 这个ID是递增的,所以最新的事务,ID值越大
在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID,这个ID是递增的,所以最新的事务,ID值越大)
开启事务的时候创建readview
Read View中有三个全局字段
trx_list:一个数值型数组,用来维护ReadView生成时刻正活跃的事务ID(即未提交的事务),该数组根据事务id从小到大排好序
up_limit_id:trx_list中的最小事务ID
low_limit_id:Read View生成时刻的系统尚未分配的下一个事务ID(不一定是数组中事务ID最大值 + 1,如下表)
事务1 | 事务2 | 事务3 | 事务4 |
---|---|---|---|
事务开始 | 事务开始 | 事务开始 | 事务开始 |
…… | …… | …… | 修改记录并提交 |
…… | 快照读 | …… | |
…… | …… | …… |
事务2进行快照读,生成Read View ,此时trx_list = [1,2,3] up_limit_id = 1 low_limit_id = 5
当事务2要进行快照读,如何判断该事务是否可以看到该记录的最新值(事务4修改之后的数据),比较规则如下:
if (DB_TRX_ID < up_limit_id) 代表这个事务在Read View生成之前已经commit,当前事务能看到DB_TRX_ID所在记录
else if (DB_TRX_ID >= up_limit_id) 代表DB_TRX_ID所在记录是在Read View生成之后才生成的,那么对当前事务肯定不可见
else if DB_TRX_ID在ReadView的数组里,代表当前事务还是活跃状态,还没commit,修改的数据当前事务看不到
else 则这个事务在Read View生成之前已经commit,那么修改结果对当前事务可见
答案:能看到,else的情况
访问数据的时候,获取事务id(获取事务id最大的记录),对比readview
如果在readview左面(比readview都小),可以访问(左边代表事务已经提交)
如果在readview(比readview都大),或者就在readview中获取roll_point,取上一个版本重新对比(在右边意味着,该事务在readview之后出现,在readview中意味着事务还未提交)
MVCC 在 MySQL InnoDB 中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读
RC:读已提交
RR:可重复读
RC和RR区别是生成ReadView策略不同,从而导致RC、RR级别下快照读的结果不同
RC:每次快照度都会生成并获取最新的一个独立的ReadView(提交的数据一定能看到)
RR:同一个事务在第一次快照读的时候生成一个ReadView,之后的快照读都复用之前的ReadView
读-读
:不存在任何问题,也不需要并发控制
读-写
:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读
写-写
:有线程安全问题,可能会存在更新丢失问题
当前读:像 select lock in share mode (共享锁), select for update; update; insert; delete (排他锁)这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁
快照读:像不加锁的 select 操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即 MVCC ,可以认为 MVCC 是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本
说白了 MVCC 就是为了实现读 - 写冲突不加锁,这个读指的就是快照读,,而非当前读,当前读实际是一种加锁的操作,是悲观锁的实现
多版本并发控制(MVCC)是一种用来解决读-写冲突
的无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照。 所以MVCC可以为数据库解决以下问题
在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能
同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题
MVCC就是因为大牛们,不满意只让数据库采用悲观锁这样性能不佳的形式去解决读-写冲突问题,而提出的解决方案