mysql innodb读写锁_浅谈MySQL InnoDB锁

基于MySQL 5.6.16

SQL92标准-事务级别:

序列化:排它锁

92b6329c1100d4884551eb6e4eb15be7.png

可重复读:读写锁,读读并行,写排他;由于读锁和写锁都是记录数,无法锁定不存在的记录,所以无法阻止插入,会出现幻读。

787d85accbae1ed06ef873c2d4e67ecd.png

读已提交:读写锁,读读并行,读写并行(写读不能并行);事务1读的时候,事务2可以写,事务2提交事务释放锁之后,事务1再读,就会出现不可重复。

8e15712a9e570444e7732c5caae2f9da.png

session1

session2

begin tx

select name from user where id = 7

-

-

begin tx

-

update user set name = 'chen' where id = 7

-

commit tx

select name from user where id = 7 // 数据不一致

-

commit tx

-

读未提交:写锁,读读并行,读写并行,写读并行;事务1读的时候,事务2可以写,事务2还未提交事务,事务1还可以再读,就会出现脏读。

830e65b4829989165db5ee1277bd1376.png

session1

session2

begin tx

-

select name from user where id = 7

-

-

begin tx

-

update user set name = 'chen' where id = 7

select name from user where id = 7 // 脏数据

-

commit tx

-

-

commit tx

以上为已过时的处理事务的方式(92年被批准的标准),列出来是为了引出共享锁和排它锁的概念!

锁:共享锁、排它锁、意向共享锁、意向排它锁

select * from user where name = 'lin' for update

以上SQL是否有加锁,对那些记录加锁?

当name为主键时,锁的是聚簇索引对应的记录。

当name为唯一索引时,锁的是唯一索引对应的记录、聚簇索引对应的记录。

当name为普通索引时,锁的是普通索引对应的记录和间隙,聚簇索引对应的记录。

当name不是索引时,锁的是整个表(每一条记录上加记录锁和间隙锁,实际上MySQL有做了优化,再加完锁只有会排查并释放掉不符合条件的记录上的锁,后面会讲到)。

意向锁是什么?

当name不是索引时,需要锁住所有的记录,那是不是要一条条记录检查是否有加锁?这样判断的效率非常低。

意向锁是表级别的,当对记录加锁时,同时会在表上加上对应的意向锁(共享锁 -> 意向共享锁,排它锁 -> 意向排它锁)。

以上SQL,在加排他锁时,发现表上如果已经有了意向锁,就会被阻塞。

-

共享锁(S)

排它锁(X)

意向共享锁(IS)

意向排它锁(IX)

共享锁(S)

兼容

不兼容

兼容

不兼容

排它锁(X)

不兼容

不兼容

不兼容

不兼容

意向共享锁(IS)

兼容

不兼容

兼容

兼容

意向排它锁(IX)

不兼容

不兼容

兼容

兼容

还有其他的“自增锁”、“insert时候的隐式锁”,本文不会说明,详细可参考:解决死锁之路 - 常见 SQL 语句的加锁分析

MVCC(Multi-Version Concurrent Control)

select * from user where name = 'lin'

以上是否有加锁?

在读已提交和可重复读级别下,查询使用了MVCC方式,是不加锁的。

InnoDB 每个聚集索引都有 4 个隐藏字段,分别是行ID(DB_ROW_ID,隐含的自增ID,如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引),最近更改的事务ID(DB_TRX_ID,每条记录有独立的事务ID,数据修改并提交成功的同时,会将事务ID修改成当前事务ID,新ID肯定会大于旧ID),undo Log 的指针(回滚指针DB_ROLL_PTR,记录删除的时候全局事务ID号,指向这条记录在undo log上的回滚数据),删除标记(记录头信息有专门的bit标志,用来表示当前记录是否已经被删除,当删除时,不会立即删除,而是打标记,然后异步删除)。

数据库每次对数据进行更新操作时,会将修改前的数据保存到undo log中,通过undo log可以实现事务回滚,并且可以根据undo log回溯到某个特定的版本的数据,实现MVCC。undo log分为insert和update,delete与update操作被归成一类。

dbf1541f224928e0c6390e141bf06e90.png

其中DB_ROLL_PTR长度为7个字节(56个字节),数据结构如下:

UNIV_INLINE

void

trx_undo_decode_roll_ptr(

/*=====================*/

roll_ptr_t roll_ptr, /*!< in: roll pointer */

ibool* is_insert, /*!< out: TRUE if insert undo log */

ulint* rseg_id, /*!< out: rollback segment id */

ulint* page_no, /*!< out: page number */

ulint* offset) /*!< out: offset of the undo

entry within page */

{

#if DATA_ROLL_PTR_LEN != 7

# error "DATA_ROLL_PTR_LEN != 7"

#endif

#if TRUE != 1

# error "TRUE != 1"

#endif

ut_ad(roll_ptr < (1ULL << 56));

*offset = (ulint) roll_ptr & 0xFFFF; //获取低16位 为OFFSET

roll_ptr >>= 16; //右移16位

*page_no = (ulint) roll_ptr & 0xFFFFFFFF;//获取32位为 page no

roll_ptr >>= 32;//右移32位

*rseg_id = (ulint) roll_ptr & 0x7F;//获取7位为segment id

roll_ptr >>= 7;//右移7位

*is_insert = (ibool) roll_ptr; // TRUE==1 ,最高位,标识修改或插入

}

字节位置

字节长度

作用

55

1

操作类型:1=INSERT,0=UPDATE

48-54

7

undolog segment id

16-47

32

undolog 页编号

0-15

16

undolog 页上的偏移量

数据操作

查询:返回的记录需要满足两个条件。

当前数据未被删除,或者删除事务ID大于当前事务ID。

事务ID小于当前事务ID(下面会讲到可见性)。

插入:该操作生产的undo log仅仅用于事务回滚,一旦事务提交,就会被删除。

删除:将旧版本信息记录在undo log中,将数据标志为删除(Deleted bit设置为1,而不是直接删除记录),设置事务ID为当前事务ID。

修改:分为两种情况,update的列是否是主键列。

如果不是主键列,将旧版本信息记录在undo log中,设置当前记录的事务ID为当前事务ID。

如果是主键列,update分两部执行:

将数据标志为删除(Deleted bit设置为1),设置事务ID为当前事务ID。

插入新的记录,新记录的事务ID为当前事务ID。

如果有二级索引,二级索引也需要做相应的更新(二级索引中包含主键项)。

undo log清理

mysql有后台purge进程来删除无用的undo log,按顺序从老到新定时扫描undo log,直到完全清除或者遇到一个不能清除的undo log。purge进程有自己的read view(等同于进程开始时最老的活动事务之前的view,trx_sys->mvcc->clone_oldest_view),保证清除的数据对任何事务来说都是不可见的。

数据可见性

MySQL在RC级别下通过MVCC解决了脏读,在RR级别下通过MVCC方案解决了脏读、不可重复读。

RR是事务级快照、RC是语句级快照。

RR:在一个事务内同一快照读执行任意次数,得到的数据一致;且只能读到第一次执行前已经提交的数据或本事务内更改的数据。

在InnoDB中,创建一个新事务的时候,InnoDB会将当前系统中的活跃事务列表(trx_sys->trx_list)创建一个副本(read view),副本中保存的是系统当前不应该被本事务看到的其他事务id列表。当用户在这个事务中要读取该行记录的时候,InnoDB会将该行当前的版本号与该read view进行比较。

设该行的当前事务id为trx_id,read view中最早的事务id为trx_id_min, 最迟的事务id为trx_id_max(trx_id_max是当前所有已提交的事务中最大XID+1)。

如果trx_id < trx_id_min的话,那么表明该行记录所在的事务已经在本次新事务创建之前就提交了,所以该行记录的当前值是可见的。

如果trx_id > trx_id_max的话,那么表明该行记录所在的事务在本次新事务创建之后才开启,所以该行记录的当前值不可见。

如果trx_id_min <= trx_id <= trx_id_max,遍历read view,查找trx_id是否在read view列表中:

如果trx_id在read view列表中,此记录的最后一次修改在read_view创建时尚未commit,不可见。

如果trx_id不在read view列表中,此记录在read_view创建之前已经commit,可见。

如果该行数据不可见,则从该行记录的回滚指针DB_ROLL_PTR指向的Undo Log中取出对应的数据,然后重新从第一步开始判断。

需要注意的是,新建事务(当前事务)与正在内存中commit 的事务不在活跃事务链表中。

RC:每次快照读均会创建新的read view。

读数据时,要不要开启“读事务”

如果你一次执行单条查询语句,则没有必要启用事务支持,数据库默认支持SQL执行期间的读一致性;

如果你一次执行多条查询语句,例如统计查询,报表查询,在这种场景下,多条查询SQL必须保证整体的读一致性,否则,在前条SQL查询之后,后条SQL查询之前,数据被其他用户改变,则该次整体的统计查询将会出现读数据不一致的状态,此时,应该启用事务支持。

间隙锁

RR隔离级别下,MySQL通过MVCC + next-key(记录锁 + 间隙锁(gap锁))解决了幻读,gap只跟insert冲突,gap之间不冲突。

快照读:简单的select操作(select * from user where name = 'lin'),属于快照读,不加锁。

当前读:所有的锁定读都是当前读,也就是读取当前记录的最新版本,不会利用 undo log 读取镜像。

select * from table where ? lock in share mode;

select * from table where ? for update;

insert into table values (…);

update table set ? where ?;

delete from table where ?;

在Serializable级别下,所有的读都是当前读。

select * from user where age = 10 for update;

当age为普通索引时,age索引加锁如下:

1

5

10

12

15

“10”上会加上排它锁,(5, 10) 和 (10, 12)间会加上间隙锁。

RR模式是否解决了幻读?这一点还存在争议,比如github上的这一个争议:https://github.com/Yhzhtk/not...。

session1

session2

begin tx

select * from user where id = 7

-

-

begin tx

-

inset into user(id) values(7)

-

commit tx

select * from user where id = 7 for update

-

commit tx

-

session1的两次select查询结果不一致。

对于这个争议,要看对幻读的定义,“快照读和当前读的结果不一致”属不属于幻读的范围。官网定义的“The so-called phantom problem occurs within a transaction when the same query produces different sets of rows at different times.”,在我看来“same query”应该是表示完全一样的sql,所以要么都是当前读,要么都是快照读,如果按这个理解来看,RR级别下就解决了幻读。

简易例子

delete from t1 where id = 10;

这个SQL会加什么锁?

回答这个问题,我们需要知道以下前提:

当前事务隔离级别是什么?

id是主键?唯一索引?普通索引?非索引?

是否存在id值为10的数据?

1. id是主键 + RC:

bcd81ae85fba5d8b720000d4df09e222.png

结论:id是主键时,此SQL只需要在id=10这条记录上加X锁即可。

2. id是唯一索引 + RC:

e8692eb8dc96086c65967975e2109c36.png

结论:若id列是unique列,其上有unique索引。那么SQL需要加两个X锁,一个对应于id unique索引上的id = 10的记录,另一把锁对应于聚簇索引上的[name=’d’,id=10]的记录。

3. id是普通索引 + RC:

1a60a30d8c7985b747e3472e85c659c9.png

结论:若id列上有非唯一索引,那么对应的所有满足SQL查询条件的记录,都会被加锁。同时,这些记录在主键索引上的记录,也会被加锁。

4. id是非索引字段 + RC:

b8240223414f826663577e7ffb45a6d7.png

由于id列上没有索引,因此只能走聚簇索引,进行全部扫描。从图中可以看到,满足删除条件的记录有两条,但是,聚簇索引上所有的记录,都被加上了X锁。无论记录是否满足条件,全部被加上X锁。既不是加表锁,也不是在满足条件的记录上加记录锁。

有人可能会问?为什么不是只在满足条件的记录上加锁呢?这是由于MySQL的实现决定的。如果一个条件无法通过索引快速过滤,那么存储引擎层面就会将所有记录加锁后返回,然后由MySQL Server层进行过滤。因此也就把所有的记录,都锁上了。

注:在实际的实现中,MySQL有一些改进(semi-consistent read),在MySQL Server过滤条件,发现不满足后,会调用unlock_row方法,把不满足条件的记录放锁 (违背了2PL的约束)。这样做,保证了最后只会持有满足条件记录上的锁,但是每条记录的加锁操作还是不能省略的。

结论:若id列上没有索引,SQL会走聚簇索引的全扫描进行过滤,由于过滤是由MySQL Server层面进行的。因此每条记录,无论是否满足条件,都会被加上X锁。但是,为了效率考量,MySQL做了优化,对于不满足条件的记录,会在判断后放锁,最终持有的,是满足条件的记录上的锁,但是不满足条件的记录上的加锁/放锁动作不会省略。同时,优化也违背了2PL的约束。

5. id是主键 + RR:与第1个一样。

6. id是唯一索引 + RR:与第2个一样。

7. id是普通索引 + RR:

6cea63f4d63c7e89018a445758690986.png

与第3个区别在于,这多了一个间隙锁(GAP锁),而且GAP锁不是加在记录上的,而是加载两条记录之间的位置。

Insert操作,如insert [10,aa],首先会定位到[6,c]与[10,b]间,然后在插入前,会检查这个GAP是否已经被锁上,如果被锁上,则Insert不能插入记录。

结论:Repeatable Read隔离级别下,id列上有一个非唯一索引,对应SQL:delete from t1 where id = 10; 首先,通过id索引定位到第一条满足查询条件的记录,加记录上的X锁,加GAP上的GAP锁,然后加主键聚簇索引上的记录X锁,然后返回;然后读取下一条,重复进行。直至进行到第一条不满足条件的记录[11,f],此时,不需要加记录X锁,但是仍旧需要加GAP锁,最后返回结束。

8. id是非索引字段 + RR:

8fe649d0080d2983cbc4dfa390dc8dfd.png

如图,这是一个很恐怖的现象。首先,聚簇索引上的所有记录,都被加上了X锁。其次,聚簇索引每条记录间的间隙(GAP),也同时被加上了GAP锁。这个示例表,只有6条记录,一共需要6个记录锁,7个GAP锁。试想,如果表上有1000万条记录呢?

在这种情况下,这个表上,除了不加锁的快照度,其他任何加锁的并发SQL,均不能执行,不能更新,不能删除,不能插入,全表被锁死。

结论:在Repeatable Read隔离级别下,如果进行全表扫描的当前读,那么会锁上表中的所有记录,同时会锁上聚簇索引内的所有GAP,杜绝所有的并发 更新/删除/插入 操作。当然,跟组合四:[id无索引, Read Committed]类似,这个情况下,也可以开启semi-consistent read,来缓解加锁开销与并发影响,但是semi-consistent read本身也会带来其他问题,不建议使用。

注:(我在:MySQL版本号5.6.16,RR级别,表数据量为223条记录的情况下做测试“update t set a = 'b'”,其中a是非索引字段,结果为:在事务结束前,会锁住所有的记录,可以通过“select * from information_schema.innodb_locks”看到,lock_model为“X”,lock_type为“RECORD”)。

注:就是所谓的semi-consistent read。semi-consistent read开启的情况下,对于不满足查询条件的记录,MySQL会提前放锁。针对上面的这个用例,就是除了记录[d,10],[g,10]之外,所有的记录锁都会被释放,同时不加GAP锁。semi-consistent read如何触发:要么是read committed隔离级别;要么是Repeatable Read隔离级别,同时设置了 innodb_locks_unsafe_for_binlog 参数。详细见:MySQL+InnoDB semi-consitent read原理及实现分析

9. id是普通索引 + RR + 没有id=5这条记录:

结果:会锁住小于5到大于5这段的间隙,例如:{1、3、7、9、10}数据中会锁住(3, 7)这段间隙。

一条相对复杂的SQL:

c7620ffcb4f88c4a48d9fbb264d4fe73.png

结论:在Repeatable Read隔离级别下,针对一个复杂的SQL,首先需要提取其where条件。Index Key确定的范围,需要加上GAP锁;Index Filter过滤条件,视MySQL版本是否支持ICP,若支持ICP,则不满足Index Filter的记录,不加X锁,否则需要X锁;Table Filter过滤条件,无论是否满足,都需要加X锁。

注:一个SQL中的where条件如何拆分?具体的介绍,建议阅读SQL中的where条件,在数据库中提取与应用浅析。

Index key:pubtime > 1 and puptime < 20。此条件,用于确定SQL在idx_t1_pu索引上的查询范围。Index key:pubtime > 1 and puptime < 20。此条件,用于确定SQL在idx_t1_pu索引上的查询范围。

Index Filter:userid = ‘hdc’ 。此条件,可以在idx_t1_pu索引上进行过滤,但不属于Index Key。Index Filter:userid = ‘hdc’ 。此条件,可以在idx_t1_pu索引上进行过滤,但不属于Index Key。

Table Filter:comment is not NULL。此条件,在idx_t1_pu索引上无法过滤,只能在聚簇索引上过滤。

从图中可以看出,在Repeatable Read隔离级别下,由Index Key所确定的范围,被加上了GAP锁;Index Filter锁给定的条件 (userid = ‘hdc’)何时过滤,视MySQL的版本而定:

在MySQL 5.6版本之前,不支持Index Condition Pushdown(ICP),因此Index Filter在MySQL Server层过滤,即所有符合pubtime > 1 and pubtime < 20的数据都通过回表查出来之后,才做userid = ’hdc‘过滤。

在5.6后支持了Index Condition Pushdown,则在index上过滤。

若不支持ICP,不满足Index Filter的记录,也需要加上记录X锁,若支持ICP,则不满足Index Filter的记录,无需加记录X锁 (图中,用红色箭头标出的X锁,是否要加,视是否支持ICP而定);而Table Filter对应的过滤条件,则在聚簇索引中读取后,在MySQL Server层面过滤,因此聚簇索引上也需要X锁。最后,选取出了一条满足条件的记录[8,hdc,d,5,good],但是加锁的数量,要远远大于满足条件的记录数量。

进阶

MySQL加锁规则里面,包含了五个原则。

加锁的基本单位是next-key lock,next-key lock 是前开后闭区间。

select * from t where c > 12 and c < 16 for update;

当c为索引时,假设c的值有“1,3,11,15,17,20”,则加的锁为:(11, 15],(15, 17],即在(11,15)和(15, 17)上加间隙锁,15和17上加记录锁。

访问到的对象才会加锁。

select id from t where c = 15 lock in share mode;

c为索引,id为主键,加读锁时, 覆盖索引优化情况下, 不会访问主键索引, 因此如果要通过 lock in share mode 给行加锁避免数据被修改, 那就需要绕过索引优化, 如 select 一个不在索引中的值。

但如果改成 for update , 则 mysql 认为接下来会更新数据, 因此会将对应主键索引也一起锁了。

索引上的等值查询,给唯一索引加锁的时候,next-key lock 退化为记录锁。

当c是唯一索引或主键,假设c的值有“1,3,11,15,17,20”。

select * from t where c = 15 for update;

只会在15上加记录锁。

select * from t where c >= 15 and c < 17 for update;

虽然查出来的结果跟=15是一样的,但是加的锁却不一样。Mysql定位到第一个符合条件的数据15(查询第一个符合条件的数据是,通过树搜索的方式定位记录,用的是“等值查询”的方法),由于在(11, 15]上是等值查询,所以退化成记录锁,整体加锁是:记录锁15和next-key (15, 17]

索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock 退化为间隙锁。

当c是普通索引,假设c的值有“1,3,11,15,17,20”。

select * from t where c = 11 for update;

引擎在找到c=11这一条索引项后继续向右遍历到c=15这一条, 此时锁范围是 (3, 11], (11, 15)

范围查询会访问到不满足条件的第一个值为止。

select * from t where c >= 11 and c <= 15;

当c是唯一索引,假设c的值有“1,3,11,15,17,20”。从常理上看,c是唯一索引,不会出现重复的数据,所以加的锁应该为为“11,(11, 15]”。

但是mysql在这种场景上,处理方式跟普通索引一样,会继续向后找第一个不满足条件的记录,最终加的锁是“11,(11, 15],(15, 17]”

示例

RR级别下,列id有以下几条数据

id

5

10

15

20

25

30

id是主键

条件

说明

id = 1

由于不存在1的值,会在第一个大于1的值上加next-key,根据规则4,等值查询会退化成间隙锁

(-∞, 5)

id < 5

根据规则5,向后查询第一个不符合条件的值,在找到的值上加next-key

(-∞, 5]

id = 5

根据规则3,唯一索引/主键上等值查询,退化成记录锁

5

id <= 5

根据规则五,即使id是主键,也会继续向后查找

(-∞, 5](5, 10]

id > 5 and id < 10

(5, 10]

id >= 5 and id < 10

规则三,唯一索引上等值查找,退化成记录锁

5, (5, 10]

id >= 5 and id <= 10

5, (5, 10], (10, 15]

id = 8

8记录不存在,同第一个条件

(5, 10)

id = 10

10

id > 25 and id < 30

(25, 30]

id > 25 and id <= 30

数据末尾,会对正无穷大加锁

(25, 30], (30, +∞]

id >= 30

数据末尾,会对正无穷大加锁

30, (30, +∞]

id是普通索引

条件

说明

id = 10

普通索引跟唯一索引不一样的点在于,普通索引是存在重复的可能性,
所以即使等值查询,也是按next-key加锁,
根据规则4,继续向后查询时,退化成间隙锁

(5, 10], (10, 15)

id = 12

12记录不存在

(10, 15)

id > 10 and id <= 15

(10, 15], (15, 20]

id >= 10 and id <= 15

(5, 10], (10, 15], (15, 20]

id不是索引

条件

说明

id =15

由于id不是索引,在没有开启semi-consistent read情况下会锁住全部数据(RR级别默认不开启)

(-∞, 5], (5, 10], (10, 15], (15, 20], (20, 25], (25, 30], (30, +∞]

limit

RR级别下,普通索引id有以下几条数据

id

5

10

10

10

15

30

delete from t where id = 10

锁的是(5, 10], (10, 10], (10, 10], (10, 15)

delete from t where id = 10 limit 2

锁的是(5, 10], (10, 10]。

在删除数据的时候尽量加 limit。这样不仅可以控制删除数据的条数,让操作更安全,还可以减小加锁的范围。

order by

索引搜索就是:找到第一个值,然后向左或向右遍历。

order by 是用最小的值来找第一个。

order by desc 是用最大的值来找第一个。

RR级别下

id

5

10

15

20

25

30

select * from t where id >= 15 and id <= 20 order by id desc for update

如果id是主键索引,由于order by id desc,所以从id <= 20开始等值查询第一个符合id=20条件的数据,查找到20之后,按道理根据规则3,会退化成记录锁,但测试发现,还多锁了个间隙锁,这一点目前还没在哪个资料上找到对这个的说明,继续向左查询,找到最后符合id >= 15的数据15,根据规则5还会继续查找到第一个不符合条件的数据10,所以最终的加锁是(5, 10], (10, 15], (15, 20], (20, 25)。

如果id是普通索引,以上SQL查找到20之后,根据规则4,会退化成间隙锁,继续向左查询,找到最后符合id >= 15的数据15,根据规则5还会继续查找到第一个不符合条件的数据10,所以最终的加锁是(5, 10], (10, 15], (15, 20], (20,25)

select * from t where id >= 15 and id < 22 order by id desc for update

如果id是主键索引,由于id=22不存在,找到(20, 25)这个间隙,由于MySQL定位第一个值用的是等值查找,根据规则4,会遍历到25且退化成间隙锁,所以最终的加锁是(5, 10], (10, 15], (15, 20], (20,25)

in

RR级别下,普通索引id有以下几条数据

id

5

10

10

10

15

30

select * from t where id in (5, 10, 15) for update

MySQL是先加锁id=5,在继续id=10,id=15,一个个加锁上去的。

session1

session2

begin tx

begin tx

select * from t where id in (5, 10, 15) for update

select * from t where id in (5, 10, 15) order by id desc for update

commit tx

commit tx

order by id desc导致session2是按顺序从15, 10, 5加锁,session1和session2加锁顺序相反,导致这两条SQL可能会发生死锁。

动态间隙锁

RR级别下,主键索引id有以下几条数据

id

5

10

15

20

25

30

select * from t where id > 15 and id <= 20 for update

以上SQL的锁是(15, 20], (20, 25],如果这时候记录15被删除(delete from t where id = 15),以上SQL的锁会动态变成(10, 20], (20, 25],

其他例子

RR级别下:

数据:

idx

2

5

9

6

14

15

事务:

session1

session2

begin tx

begin tx

select * from user where idx = 3 for update

select * from user where idx < 3 for update

commit tx

commit tx

以上两个事物互相不干扰,session1的锁范围是(2, 5),session2的锁范围是(2, 5],间隙锁只会阻塞插入操作。

session1

session2

begin tx

begin tx

delete from user where idx = 7

delete from user where idx = 8

insert into user(idx) values (7)

insert into user(idx) values (8)

commit tx

commit tx

由于idx不存在值为7和8的记录,session1和session2都持有(5, 9)间隙锁,锁只有在事务提之后的时候才会释放,此时出现两个事务互相等待对方持有的间隙锁而无法插入,出现死锁。

避免更新或者删除不存在的记录,容易导致死锁问题。

Serializable级别作用

既然RR级别下已经不会出现幻读,那为什么还需要Serializable:

防止数据丢失(被覆盖):

session1

session2

begin tx

select name from user where id = 7

-

-

begin tx

-

update user set name = 'chen' where id = 7

-

commit tx

update user set name = 'lin' where id = 7

-

commit tx

-

防止出现幻觉(RR级别,id为主键):

session1

session2

begin tx

select * from user where id = 7 // 发现数据不存在

-

-

begin tx

-

inset into user(id) values(7)

-

commit tx

inset into user(id) values(7) // 报错,主键冲突

-

select * from user where id = 7 // 仍然发现数据不存在

-

commit tx

-

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值