事务机制的分析
作者:互联网
工作的时候,发现有个接口超时了:
可以看出报错信息是锁超时的了。
sql具体信息如下:
此sql经由sharding jdbc改写,所以首先怀疑到此插件上。 因使用版本不能看到相应改写代码,因此只能查看数据库的信息
show processlist;
select * from information_schema.PROCESSLIST where info is not null;
可以看到当运行的时候会有个sql卡住。
更新的时候卡住,并且有锁,说明其他事务上了锁,和当前的sql有冲突。 需要找证据,顺着代码向前追溯。
这行代码是对当前集合id的更新。
更新就上锁了嘛? 此时需要复习一下mysql的锁机制。
mysql 分为当前读和快照读。
当前读: 像select for update,update,insert,delete这些操作都是当前读,就是读取当前最新版本,读取时还要保证其他并发事务不能修改当前记录,对读取的记录进行加锁。
快照读:不加锁的select操作就是快照读,不加锁的非阻塞读。快照读的实现基于MVCC,在很多情况下,避免了加锁操作,降低了开销;
快照读是通过MVCC来是实现的,简单介绍一下MVCC解决什么问题、怎么解决问题以及当前存在的问题。
MVCC是一种用来解决读-写冲突的无锁并发控制,也就是为事务分配单向递增的事务ID,为每个修改保存一个版本。读操作只读该事务开始前的数据库的快照。 MVCC为数据库解决了以下问题:
在并发读写数据库时,可以做到在读操作时不用阻塞写操作, 写操作也不用阻塞读操作,但是写写操作需要互斥,提高了数据库并发读写的性能。
同时还可以解决脏读、幻读(并不能解决),不可重复读(这个要看隔离事务级别,RC的依然不能解决,因为每次当前读都需要更新视图)。
MVCC 主要依赖记录中的3个隐藏字段、undo日志(版本链),ReadView来实现。
首先是隐藏字段:
1. DB_TRX_ID:6byte,最近修改事务ID:记录创建这条记录/最后一次修改该记录的事务ID。
2. DB_ROLL_PTR: 7Byte回滚指针,指向这条记录的上一个版本(存储于rollback segment,版本链)
3. DB_ROW_ID: 如果没有主键或者唯一索引,会自动以DB_ROW_ID产生一个聚簇索引。
首先通过版本链,不同事务或者相同事务对同一记录的修改,就会导致该记录的undo log成为一条记录版本线性表。
其次通过ReadView(读视图) 在该事务执行快照读的那一刻,会生成数据库系统当前的一个快照读,记录并维护系统当前活跃事务的ID。
ReadView 遵循一个可见性算法,主要是将要被修改最新记录的DB_TRX_ID取出来,与系统当前其他活跃事务的ID去对比ReadView,通过比较看是否符合可见性。
ReadView 其实就是个对象,其中包含四个属性:
m_ids: 在生成ReadView 时当前系统中活跃的去写事务的事务id的列表
min_trx_id 表示 m_ids中最小值
max_trx_id 表示生成ReadView时,系统中应该分配给下一个事务的id值
creator_trx_id 表示生成该ReadView的事务id。
如何判断版本链中哪个版本可见
小于最小可见、大于最大不可见,中间的,在里面的都不可见,里面以外的,已提交的就可见了。
其实光介绍MVCC是没有意义的,还需要根据不同的隔离级别进行判断:
1. RC情况下:
RC是可以使用MVCC的,在一个事务里面每次进行一次快照读就会更新一次 ReadView,因此每次都更新,导致之前已提交的事务的内容的变得可见,因此活跃事务中没它了,因此对同一个数据进行读取的时候,变得不可重复读。 但是为啥还要加MVCC呢,如果不加的话,就会读写互斥,加了以后当前读不影响其他事务。 因此其只能做到读已提交,做不到可重复读,更不用说幻读。
2. RR情况下:
RR使用MVCC,事务在第一次快照读生成一次ReadView,以后都不更新,这就保证了别的事务提交的事务并不影响我读取数据,因此其能保证可重复读,但是其能保证幻读吗?
答案是不能哦,幻读一般分为两种方式: 一种是范围查询,一种是更新一个查询不到的数据,但是却更新成功,并且能读取了。
首先是解决范围查询的问题。
select * from A where a>100 语句1
update A set colum = "default"; 语句2
select * from A where a>100 语句3
当别的事务插入一个 a =101 并在语句2之前提交,此时语句3查询就会多出来一个。老子出现幻觉了?
那怎么解决这个问题呢?
需要在语句1加上for update 让mysql加间隙锁,使得相应的记录,此时就保证了别的事务插入的时候就会阻塞,但是保证了当前事务不会出现幻读情况。
第二种情况也是类似,新加入的记录被当前事务操作,变得可见,查询的时候出了幻觉,和之前查询的结果并不一样。
因此在RR情况下,幻读的解决是需要严谨的使用sql来解决。
刚才提到的间隙锁是个什么情况呢?
首先间隙锁在RC是不存在的:
当插入id =12的时候,是阻塞住的
当插入id =13的时候,没有阻塞,说明RC隔离级别下是没有间隙锁的,只有RR隔离级别下是有间隙锁。
接下来介绍一下记录锁、间隙锁、临键锁的区别
记录锁:record lock,即锁住一条记录
间隙锁:gap lock,即锁定一个区间,左开右开
临键锁:记录锁+间隙锁锁定的区间,左开右闭
例子如下:
A表:
id a b
0 0 0
8 8 8
16 16 16
id是主键索引,a是普通列 ,b是普通索引
唯一索引等值查询:
1. 当查询记录存在的,在用唯一索引进行等值查询时,next-key lock 会退化为记录锁 (主键索引也是唯一索引)
2. 当查询记录不存在时,在用唯一索引进行等值查询时,next-key lock 会退化为间隙锁
行数据存在加行锁,行数据不存在加间隙锁
索引等值查询,需要访问到第一个不满足条件的值,此时的next-key lock会退化为间隙锁
普通索引等值查询: 等值会退化。
1. 当查询的记录存在时,除了会加next-key lock外,还额外加间隙锁,相当于加两把锁 (] 和() 索引等值查询,需要访问到第一个不满足条件的值,此时的next-key lock会退化为间隙锁。
存在就加两个,有一个退化。
2. 当查询记录不存在时,只会加next-key lock,然后会退化为间隙锁,也就是说只会加一把锁()还是退化了的
不存在就加一个退化的锁
总结。等值会退化。
比较有争议的,版本不一样加的锁不太一样
唯一索引范围查询:
select * from t_test where id >=8 and id <9 for update
因为是大于等于,首先找到等于,其第一行 id =8 加next-key lock (0,8] 但因id是唯一索引,该记录是存在的,因此会退化为记录锁,因此只会对这id= 8这一行加锁。
但是因为是范围查询,索引就要找下一个区间,下一个区间是16, 所以需要加锁(8,16]。 (低版本范围查询不会退化 MySQL version 5.6.47)
也有版本将锁范围加成了 (8,16)。 (高版本的范围查询会退化,MySQL8.0.18版本)
因此所有的加锁范围变成了 id=8 和(8,16) 这个没有争议。 一个是行锁,一个间隙锁,两个都退化了
普通索引范围查找:
非唯一索引和主键索引的范围查询的加锁也有所不同,不同之处在于普通索引范围查询,next-key lock 不会退化为间隙锁和记录锁。
select * from t_test where b >=8 and b <9 for update
最开始要找第一行是b=8 因此next-key lock(0,8],但是由于b不是唯一索引,
所以加锁为(0,8],(8,16] (低版本范围查询不会退化 MySQL version 5.6.47)
有的说是有退化,导致加锁区间为 (0,8],(8,16) (高版本的范围查询会退化,MySQL8.0.18版本)
这个就有争议了,有的是后一个退化了,有的没有退化,可能是版本不一样导致的。
话峰一转,此时需要了解一下事务的传递机制,且听下次分析
标签:分析,事务,记录,查询,索引,退化,机制,id 来源: https://www.cnblogs.com/followers/p/16588083.html