决战圣地玛丽乔亚Day17 ----意向锁/MDL锁

算法:

给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。

示例:
输入: n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]

回溯算法的基本写法:

组合是有序的,1,2 和2,1算作一个结果。

所以如果判断n个数取k个的所有结果集。并返回结果集

首先对于取的顺序方式可以参考

从1开始取,每次在上一次的index基础上往下去取。

如果取够k个进行返回。

还要注意的一点是,取完最终结果需要回溯!这是精髓所在。

1,2取完, 2回溯。

1,3取完,3回溯。

1,  4  

然后发现这是的第二层的startindex到头,result集合被赋值完毕, 【1,2】【1,3】【1,.4】

 

 

 

 

1.递归参数和返回值

这一步更适合整体逻辑理顺后进行编写,如果想清楚参数和返回值,就要想先想清楚大体的逻辑才行。

vector<vector<int>> result; // 存放符合条件结果的集合
vector<int> path; // 用来存放符合条件单一结果
void backtracking(int n, int k, int startIndex) 

2.回溯函数的终止条件

if (path.size() == k) {
    result.push_back(path);
    return;
}
终止条件是取够数量

3.单层的搜索逻辑

for (int i = startIndex; i <= n; i++) { // 控制树的横向遍历
    path.push_back(i); // 处理节点 
    backtracking(n, k, i + 1); // 递归:控制树的纵向遍历,注意下一层搜索要从i+1开始
    path.pop_back(); // 回溯,撤销处理的节点
}

实际上,这个for循环,里面的backtracking是每一层,直到达到终止条件才是进入到了最深层。

path.pop_back实际上是从最深的一层往外弹出,寻找更多地可能性。

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

MYSQL的行锁: 独占锁、共享锁

表锁:除了独占锁共享锁之外,还有意向锁和MDL锁。

意向锁:
意向锁是一种不与行级锁冲突表级锁

意向锁数据引擎自己维护,自己无法去操作。

在为数据行加共享 / 排他锁之前,InooDB 会先获取该数据行所在在数据表的对应意向锁。

 

需要解决的问题:

如果事务A先给表加了排他锁,没有提交的情况下,事务B再去加共享锁就需要检查:表是否存在排它锁,表每一行是否存在排它锁。如果真的去检测每一行,效率很差,这时候就可以用意向锁。

 意向锁和意向锁之间不冲突。

共享锁和意向共享锁兼容。

其他的意向锁和  共享/排他锁冲突。

意向锁是表级锁,不与行级锁冲突。这里举例的S、X都是表级锁。

 

 

 意向锁和表级的排他/共享锁的互斥关系如图,意向共享锁和共享锁之间不会互斥

这一段写的很好:

摘自 https://juejin.cn/post/6844903666332368909

 

 

总结

  1. InnoDB 支持多粒度锁,特定场景下,行级锁可以与表级锁共存。
  2. 意向锁之间互不排斥,但除了 IS 与 S 兼容外,意向锁会与 共享锁 / 排他锁 互斥
  3. IX,IS是表级锁,不会和行级的X,S锁发生冲突。只会和表级的X,S发生冲突。
  4. 意向锁在保证并发性的前提下,实现了行锁和表锁共存满足事务隔离性的要求。

  刚开始意向锁的出现是为了解决:我一个锁去查一个表是否加了行的独占锁,如果每行去查效率低下的问题。所以在加独占锁的时候给整个表加一个意向独占锁,后面的事务看到就知道已经有独占锁了。

  如果有共享锁过来想拿表的共享锁,那么直接看到意向排他锁,被阻塞。如果有事务想拿某一行的排他锁,那么先拿到意向独占锁,然后看想加锁的那一行是否加了独占锁,如果没加就可以获取到那一行的独占锁。

 

 

MDL锁:

不需要显示的调用,在我们对表数据进行CRUD时,加MDL读锁;当我们对表进行结构修改的时候,加MDL写锁。为的是保证数据一致性,进行阻塞。但是如果是热点表会大量超时问题。

读读共享,读写互斥,写写互斥。

所以我们再修改表结构的时候,需要等所有的MDL锁释放才能拿到MDL写锁,拿到写锁后,其他线程的CRUD操作被阻塞,等到修改表结构结束才可以释放。

同时普及一下常见的名词:DDL(数据定义语言)CREATE TABLE/VIEW/INDEX/SYN/CLUSTER   

            DML(数据操纵语言)Insert/update/delete

 

 

区间锁:

1.间隙锁:

显示使用间隙锁:

SELECT * FROM table WHERE id BETWEN 1 AND 10 FOR UPDATE;

同样是是select for update语法,但是需要一个区间范围。

首先要了解间隙锁解决的问题是什么:

这是在可重复读RR隔离级别下会出现的幻读现象。

幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。

两次查询之前,被另一事务修改数据,导致读取不一致。

幻读的第二种可能(mvcc修改幻读):

事务A查结果集,set1

事务B修改结果集数据,结果集变为set2

事务A由于MVCC读到的还是 set1

事务A执行update操作后

事务A读到的结果集为set1

所以能看出来,对于读数据mvcc可以解决幻读情况,对于修改数据,mvcc还是会出现幻读。但是其实这种不算幻读

 

 

首先要什么是当前读,什么是快照读。

快照读是用mvcc多版本并发控制,解决了幻读现象,读的是之前的历史版本,不加锁。(数据行的版本号要小于或等于当前是事务的系统版本号,这样也就确保了读取到的数据是当前事务开始前已经存在的数据,或者是自身事务改变过的数据)

要注意快照产生在第一次select。

当前读是 

update/insert/delete等对数据库操作的sql,更新版本号。 读当前数据,加锁。是会产生幻读的现象,所以在当前读的情况下,如何解决幻读????

 

所以解决幻读:

1.快照读:mvcc来解决。

2.当前读:

  如果范围是主键:对主键索引加recordLock

  如果范围不是主键:通过next-key(record-lock+gap lock)处理

 

1,Record Lock:单个行记录上的锁。

2,Gap Lock:

当我们用范围条件而不是相等条件索引数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项枷锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”。

InnoDB也会对这个“间隙”枷锁,这种锁机制就是所谓的间隙锁(Next-Key锁)。为了防止其他事务在间隙内操作造成幻读加的锁。这样做会阻塞数据插入到间隙中

3,Next-Key Lock:(行锁+间隙锁),锁定一个范围,并且锁定记录本身。对于行的查询,都是采用该方法,主要目的是解决幻读的问题。

 

 

总结:

记录锁、间隙锁、临键锁,都属于排它锁

隔离级别RR

唯一索引只有锁住多条记录或者一条不存在的记录的时候,才会产生间隙锁,指定给某条存在的记录加锁的时候,只会加记录锁,不会产生间隙锁

普通索引不管是锁住单条,还是多条记录,都会产生间隙锁

间隙锁会封锁该条记录相邻两个键之间的空白区域,防止其它事务在这个区域内插入、修改、删除数据,这是为了防止出现 幻读 现象;

next-lock加锁,范围内阻塞:

  1. select * from table where id<6 lock in share mode;--共享锁
  2.  select * from table where id<for update;--排他锁

需要注意的一点,如果是普通的查询是走快照读,只有for update或者 lock in share mode这种才会当前读。

加锁原则:

  • 1.加锁的基本单位是(next-key lock),他是前开后闭原则
  • 2.插叙过程中访问的对象会增加锁
  • 3.索引上的等值查询--给唯一索引加锁的时候,临键锁升级为行锁
  • 4.索引上的等值查询--向右遍历时最后一个值不满足查询需求时,next-key lock 退化为间隙锁
  • 5.唯一索引上的范围查询会访问到不满足条件的第一个值为止

 

需要注意的是,当id列上没有索引时,SQL会走聚簇索引的全表扫描进行过滤,由于过滤是在MySQL Server层面进行的。因此每条记录(无论是否满足条件)都会被加上X锁。但是,为了效率考量,MySQL做了优化,对于不满足条件的记录,会在判断后放锁,最终持有的,是满足条件的记录上的锁。但是不满足条件的记录上的加锁/放锁动作是不会省略的。所以在没有索引时,不满足条件的数据行会有加锁又放锁的耗时过程。

根据检索条件向下寻找最靠近检索条件的记录值A作为左区间,向上寻找最靠近检索条件的记录值B作为右区间,即锁定的间隙为(A,B)。

 更需要你注意的是,当你再执行update t set number = 6 where id = 1也会被阻塞。这是为什么?你想想看,要保证每次查询number=6的数据行数不变,如果你将另外一条数据修改成了6,岂不会多了一条?所以此时不会允许任何一条数据被修改成6。

 

 

各种加锁举例:

例如给2~25加next-lock:

 

 (-∞,5](5,10](10,15](15,20](20,25](25,+supernum]  加锁范围是这个

数据和间隙都会加锁

在此前提下:

 

 当有如下事务A和事务B时,事务A会对数据库表增加(10,15]这个区间锁,这时insert id = 12 的数据的时候就会因为区间锁(10,15]而被锁住无法执行。

由于间隙锁之间不互斥,所以可能会发生死锁

 

两个人都拿到了间隙锁,往间隙插入数据都被阻塞

事务A获取到(5,10]之间的间隙锁不允许其他的DDL操作,在事务提交,间隙锁释放之前,事务B也获取到了间隙锁(5,10],这时两个事务就处于死锁状态

 

 

 

1.加锁的范围是(5,10]的范围锁
2.由于数据是等值查询,并且表中最后数据id = 10 不满足id= 7的查询要求,故id=10 的行级锁退化为间隙锁,(5,10)
3.所以事务B中id=8会被锁住,而id=10的时候不会被锁住

 

 

1.加锁的范围是(0,5],(5,10]的范围锁
2.由于c是普通索引,根据原则4,搜索到5后继续向后遍历直到搜索到10才放弃,故加锁范围为(5,10]
3.由于查询是等值查询,并且最后一个值不满足查询要求,故间隙锁退化为(5,10)
4.因为加锁是对普通索引c加锁,而且因为索引覆盖,没有对主键进行加锁,所以事务B执行正常
5.因为加锁范围(5,10)故事务C执行阻塞
6.需要注意的是,lock in share mode 因为覆盖索引故没有锁主键索引,如果使用for update 程序会觉得之后会执行更新操作故会将主键索引一同锁住

 

 

  • next-key lock 增加范围锁(5,10]
  • 根据原则5,唯一索引的范围查询会到第一个不符合的值位置,故增加(10,15]
    3.因为等值查询有id =10 根据原则3间隙锁升级为行锁,故剩余锁[10,15]
    4.因为查询并不是等值查询,故[10,15]不会退化成[10,15)
    5.故事务B(13,13,13)阻塞,事务C阻塞

 

 

next-key lock 增加范围锁(5,10],(10,15]
2.因为c是非唯一索引,故(5,10]不会退化为10
3.因为查询并不是等值查询,故[10,15]不会退化成[10,15)
4.所以事务B和事务C全部堵塞

 

posted @   NobodyHero  阅读(54)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
· 零经验选手,Compose 一天开发一款小游戏!
点击右上角即可分享
微信分享提示