在RC和RR下innodb的锁

事务:

  事务是数据库区别与文件系统的重要特性之一,锁为事务服务,实现事务的特性,先来简单回归一下事务的各种特性。



事务的ACID特性:

事务有四种特性来保证事务能够很好地为我们服务。

原子性(atomicity)一个事务要么全部执行,要么全部失败
一致性(consistency)事务将数据库从一种一致的状态转变为下一种一致的状态,数据库的完整性约束没有被破坏
隔离性(isolation)不同的事务之间不应该相互干扰
持久性(durability)事务一旦提交就应该是永久改变的

事务三种常见问题:


  事务在并发场景下会产生三种常见问题。

脏读一个事务修改了数据,还没有提交就被另一个事务访问到
不可重复读一个事务中,第一次读到的数据值和第二次读到的数据值不一致。(读到了update未提交的内容)
幻读一个事务中,第一次读到的数据量和第二次读到的数据量不一致。(读到了insert未提交的内容)


事务的四种隔离级别:

  为了解决这三种问题,提出了四种经典的事务隔离级别,其中RC和RR使用最为广泛。



Read uncommitted读未提交毫无隔离性可言
Read committed读已提交解决脏读
Repeatable read重复读解决不可重复读
Serializable串形化解决幻读

Tips:
- mysql的默认隔离级别为RR(repeatable read)

锁:

  锁为事务服务,是实现事务的具体方式。与很多数据库引擎相比,innodb引擎不存在锁升级的问题,其根据事务访问的每个页来对行锁进行管理,采用位图的方式。因此无论一个事务锁住页中的一条记录还是多条记录,其开销通常是一致的。

按兼容性划分:

S锁共享锁(读锁)并发读之间不会相互阻碍
X锁排他锁(写锁)同一时间只能有一个X锁
IS锁意向共享锁表示当前表有活跃事务在给某些行上S锁
IX锁意向排他锁表示当前表有活跃事务在给某些行上X锁

Tips:
- 意向锁的作用是可以快速判断当前表是否有活跃的事务在给其中某些行上锁,是提高并发的一种手段。

按模式划分:

Record 记录锁行锁都是加在索引上的,表示对该行数据的读锁或者写锁
GAP 间隙锁给两个相邻索引叶子结点之间上锁,避免其他事务在此区间内插入数据
Next-key 下一键锁下一键锁 = 两个相邻索引叶子结点之间的间隙锁 + 右边界叶子结点的记录锁
II-GAP 插入意向锁在insert操作将要执行时首先获取该区间的插入意向锁,插入意向锁之间并不会冲突,但和间隙锁、下一键锁冲突。设计可以满足多个insert并发,但和区域select互斥
Auto-Inc 自增锁锁定自增资源本身,在获取自增id后,立即释放

锁兼容矩阵:

表锁开销小、加锁快;但粒度大,并发度低。
行锁开销大、加锁慢;但粒度小,并发度高。

表兼容矩阵:

  上边一行是已有的锁,左侧一列是要加的锁

S锁X锁IS锁IX锁
S锁YY
X锁
IS锁YYY
IX锁YY

行兼容矩阵:

  同样,上边一行是已有的锁,左侧一列是要加的锁

记录锁间隙锁下一键锁插入意向锁
记录锁YY
间隙锁YYYY
下一键锁YY
插入意向锁YY

死锁的条件:

锁和资源的调度有时会产生一种致命的错误,死锁。死锁的触发条件如下,缺一不可。

资源互斥资源一旦分配,在没有释放之前,都不能分配给其他事务
占有且等待一个事务占有数个资源之后,仍然在申请其他资源
不可抢占资源分配之后,没有释放之前,不能被其他事务抢占
循环等待事务相互占有对方申请的资源,依赖链构成一个环

innodb解决死锁的办法:

  死锁检测的原理是构建一个以事务为顶点、锁为边的有向图,判断有向图是否存在环,存在即有死锁。

  innodb在sql执行前会进行死锁的检查,如果发现某个新事务中的sql会导致死锁,会选择更新行数最少的事务进行回滚(基于INFORMATION_SCHEMA.INNODB_TRX表)中的 trx_weight字段来判断。

索引:

  innodb的锁都是加在索引上的,B+树索引在数据库中有一个特点是高扇出性。
因此在数据库中,B+树的高度一般都在2~4层,这也就是说查找某一键值的行记录时最多只需要2到4次IO。

快照读:

  快照读也称非锁定读取,RR下称为一致性非锁定读。快照读并不会加锁,而是通过MVCC完成事务隔离。MVCC(多版本并发控制)在RC和RR两个隔离级别下的表现会不同,通过undo log、隐藏列、读视图三个部分实现。

undo log:

  Undo log 存放老版本数据,当一个事务需要读取记录行时,如果当前记录行不可见,则顺着undolog找到满足可见性的记录行版本。

  • Insert undo log: 插入操作时产生的undo log,事务回滚时使用,并在事务提交后可以立即删除。
  • Update undo log:更新和删除操作时产生的undo log,事务回滚和快照读使用,删除本质上也是更新标标识位,然后经过一个离线purge线程将记录删除。

undo_log

隐藏列:

innodb引擎在每行数据后面添加了三个隐藏列,例如下表中后三个列。

idnoDB_ROW_IDDB_TRX_IDDB_ROLL_PTR
主键编号6字节的行ID,当表没有主键时聚簇索引使用这个ID6字节,最近一次写操作的事务ID(和是否提交无关)7字节,指向undo log的下一跳

读视图:

读视图主要用来做可见性判断。

struct read_view_t {
    trx_id_t  up_limit_id; // 当前活跃事务中最小id
    trx_id_t  low_limit_id; // 已分配的最大事务id+1
    trx_id_t* trx_ids; // 当前活跃的事务id列表 
    // 可以通过 select * from information_schema.innodb_trx查看
}
- RC下:执行每条select快照读,都会更新读视图,判断当前数据行是否可见。
- RR下:执行第一条select快照读,创建读视图,并且之后读视图不会被刷新。

可见性判断算法:

  • db_trx_id < up_limit_id,表示当前版本在当前事务创建前就提交了,所以对当前事务可见。

view_1

  • db_trx_id >= low_limit_id,表示当前版本在当前事务创建后才修改,所以不可见,undo log找下一版本判断。

view_2

  • up_limit_id <= db_trx_id < low_limit_id,表示当前版本在当前事务创建时还处于活跃状态,则需要在trx_ids数组里二分查找值为db_trx_id的事务,能找到则当前版本不可见,需要从undo log中找下一个版本去判断,找不到则直接可见。

view_3

例子:


数据事务A事务B事务C事务D
data=吃饭
DB_TRX_ID=1000
trx_id=1001trx_id=1002trx_id = 1003trx_id = 1004
begin
begin
data=吃饭
DB_TRX_ID=1000
select data=?
trx_ids:[1002]
up_limit_id:1002
low_limit_id=1003
DB_TRX_ID=1000< up_limit_id
可见
data=吃饭
begin
data=吃饭
DB_TRX_ID=1000
select data=?
trx_ids:[1001,1003]
up_limit_id:1001
low_limit_id=1003
DB_TRX_ID=1000< up_limit_id
可见
data=吃饭
begin
data=睡觉
DB_TRX_ID=1003
->
data=吃饭
DB_TRX_ID=1000
update set
data=睡觉
commit
commit
data=睡觉
DB_TRX_ID=1003
->
data=吃饭
DB_TRX_ID=1000
RC select data=?
RR select data=?

(RC隔离级别,视图更新)
trx_ids:[1004]
up_limit_id:1004
low_limit_id=1005
DB_TRX_ID=1003< up_limit_id
可见
找到data=睡觉

(RR隔离级别,视图更新)
trx_ids:[1001,1003]
up_limit_id:1001
low_limit_id=1004
DB_TRX_ID=1003> up_limit_id
不可见
遍历undo log
找到data=吃饭

当前读:

当前读的操作包括select for update, update, delete,并且innodb在RR下,会使用next-key机制来避免幻读,常见的当前读加锁情况如下。

无索引:

select * from test where id = 5 for update 

no

Tips:
- 更新操作的where条件最好走索引,不然锁的开销很大。

聚簇索引-值查询命中:

select * from test where id = 5 for update 

primary_yes

Tips:
- RC和RR下,命中都会对数据行加X锁。

聚簇索引-值查询未命中:

select * from test where id = 4 for update 

primary_no

Tips:
- RR下未命中,则会对最后的查询区间加Gap锁。

聚簇索引-范围查询命中:

select * from test where id > 3 and id < 6 for update 

primary_range_yes

Tips:
- RR下范围命中,则会对范围内的记录区间加Gap锁。
- 由于next-key锁机制会对范围最后的一个record(可能不在范围内)加X锁

二级唯一索引-值查询命中:

select * from test where no = '1005' for update 

unique_yes

Tips:
- innodb 中唯一索引的null可以是多个行,null也可以走索引
- 当索引有唯一属性时,next-key锁会降级为record锁,所以这里即使在RR下,二级索引也不会有gap锁。

二级唯一索引-值查询未命中:

select * from test where no = '1004' for update 

在这里插入图片描述

Tips:
- RR下二级索引未命中只会对二级索引加Gap锁。

二级非唯一索引-值查询命中:

select * from test where name = 'Alice' for update 

normal_yes

Tips:
- RR下普通二级索引命中可能会命中多个值。由于next-key锁的机制,每一个命中的二级索引前面的gap也会被锁定。

二级非唯一索引-值查询未命中:

select * from test where name = 'Clare' for update 

normal_no

Tips:
- RR下普通二级索引未命中只会对最后的扫描区域加gap。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值