MysqlTutorial05
锁
实在写不动了,多拿了点别人的截图过来,自己写写额外的理解。锁还是为了解决并发场景下数据一致性的问题,我们将从锁的分类、加锁过程、死锁问题三个大方面去介绍何为锁。开始!
锁类型
自上而下分为:全局锁、表级锁、行级锁,其中重点是行级锁。
全局锁
顾名思义,锁的对象是整个数据库,一般用于数据库快照和备份存储(显然备份的时候是需要上锁的,毫无疑问)。
然而加上全局锁,意味着整个数据库都是只读状态。那么如果数据库里有很多数据,备份就会花费很多的时间,关键是备份期间,业务只能读数据,而不能更新数据,这样会造成业务停滞。
解决办法就是:如果数据库的引擎支持的事务支持可重复读的隔离级别,那么在备份数据库之前先开启事务,会先创建 Read View,然后整个事务执行期间都在用这个 Read View,而且由于 MVCC 的支持,备份期间业务依然可以对数据进行更新操作。
备份数据库的工具是 mysqldump,在使用 mysqldump 时加上 -single-transaction 参数的时候,就会在备份数据库之前先开启事务。这种方法只适用于支持「可重复读隔离级别的事务」的存储引擎。
InnoDB 存储引擎默认的事务隔离级别正是可重复读,因此可以采用这种方式来备份数据库。但是,对于 MyISAM 这种不支持事务的引擎,在备份数据库时就要使用全局锁的方法。
全局锁使用方法:
表级锁
表锁
如果我们想对学生表(t_student)加表锁,可以使用下面的命令:
需要明白的是,写锁和读锁之间满足的关系一般都是:写写互斥、读写互斥、读读共享。因此表锁也满足这些性质。值得注意的是,占有表锁的线程是不能去操作其他表的,即使占有的是读锁也不行。
要释放表锁,可以使用下面这条命令,会释放当前会话的所有表锁:
unlock tables
另外,当会话退出后,也会释放所有表锁。
在还没有出现更细粒度的锁的时候,表锁是最常用的处理并发的方式,不过尽量避免在使用 InnoDB 引擎的表使用表锁,因为表锁的颗粒度太大,会影响并发性能,InnoDB 牛逼的地方在于实现了颗粒度更细的行级锁。
元数据锁(MDL锁)
MDL的作用在于保护表结构,防止一个会话正在查询表的同时,另一个会话修改该表的结构,确保事务执行期间表结构不会发生改变。
我们不需要显示的使用 MDL,因为当我们对数据库表进行操作时,会自动给这个表加上 MDL:
- 对一张表进行 CRUD 操作时,加的是 MDL 读锁;
- 对一张表做结构变更操作的时候,加的是 MDL写锁
MDL是为了保证当用户对表执行 CRUD 操作时,防止其他线程对这个表结构做了变更。
当有线程在执行 select 语句(加 MDL读锁)的期间,如果有其他线程要更改该表的结构(申请 MDL写锁),那么将会被阻塞,直到执行完 select 语句(释放 MDL 读锁)。
反之,当有线程对表结构进行变更(加 MDL写锁)的期间,如果有其他线程执行了CRUD 操作(申请MDL读锁),那么就会被阻塞,直到表结构变更完成(释放 MDL写锁)。
前面说MDL不用显式地添加,而且它是在事务提交后才会释放,这意味着事务执行期间,MDL是一直持有的。然而这会产生什么问题呢?
如果数据库有一个长事务(所谓的长事务,就是开启了事务,但是一直还没提交),那在对表结构做变更操作的时候,可能会发生意想不到的事情,比如下面这个顺序的场景:
1.首先,线程 A 先启用了事务(但是一直不提交),然后执行一条 select 语句,此时就先对该表加上MDL 读锁;
2.然后,线程 B也执行了同样的 select 语句,此时并不会阻塞,因为「读读」并不冲突;
3.接着,线程C修改了表字段,此时由于线程A的事务并没有提交,也就是 MDL 读锁还在占用着,这时线程C 就无法申请到 MDL写锁,就会被阻塞;
那么在线程 C 阻塞后,后续有对该表的 select 语句,就都会被阻塞,如果此时有大量该表的 select 语句的请求到来,就会有大量的线程被阻塞住,这时数据库的线程很快就会爆满了。(写锁在等待中,后面的读锁会在写之后等待,写锁获取优先级高于读锁)。
所以为了能安全的对表结构进行变更,在对表结构变更前,先要看看数据库中的长事务,是否有事务已经对表加上了 MDL 读锁,如果可以考虑 kill 掉这个长事务,然后再做表结构的变更。
意向锁
意向锁表明事务“打算”在表中的某些行上设置特定类型的锁(共享或排他)。这样一来,当一个事务想要获取整个表的锁时,它不需要检查表中的每一行是否被锁定,只需检查表上是否存在相应的意向锁,就能快速判断是否可以获取表锁。
什么时候添加意向锁呢?
在使用 InnoDB 引擎的表里对某些记录加上「共享锁」之前,需要先在表级别加上一个「意向共享锁」;
在使用 InnoDB 引擎的表里对某些记录加上「独占锁」之前,需要先在表级别加上一个「意向独占锁」。
也就是,当执行插入、更新、删除操作,需要先对表加上「意向独占锁」,然后对该记录加独占锁;而普通的 select 是不会加行级锁的,普通的 select 语句是利用 MVCC 实现一致性读,是无锁的。
不过硬要加的话也是可以的:
意向共享锁和意向独占锁是表级锁,不会和行级的共享锁和独占锁发生冲突,而且意向锁之间也不会发生冲突,只会和共享表锁(lock tables .. read)和独占表锁(lock tables .. write)发生冲突。
如果没有「意向锁」,那么加「独占表锁」时,就需要遍历表里所有记录,查看是否有记录存在独占锁,这样效率会很慢。
那么有了「意向锁」,由于在对记录加独占锁前,先会加上表级别的意向独占锁,那么在加「独占表锁」时,直接查该表是否有意向独占锁,如果有就意味着表里已经有记录被加了独占锁,这样就不用去遍历表里的记录。
总而言之,意向锁的目的是为了快速判断表里是否有记录被加锁。记住这个就行。
AUTO-INC锁
这里讲述的是自增id的实现方式以及优化的方法。首先是AUTO-INC锁,但因为优先级太高会阻塞其他业务转而使用轻量级锁。接着为了解决并发时数据一致性的问题,使用 binlog_format = row代码解决。
原文链接:https://xiaolincoding.com/mysql/lock/mysql_lock.html#auto-inc-%E9%94%81
行级锁
前面也提到,普通的 select 语句是不会对记录加锁的,因为它属于快照读。如果要在查询时对记录加行锁,可以使用下面这两个方式,这种查询会加锁的语句称为锁定读。
共享锁(S锁)满足读读共享,读写互斥。独占锁(X锁)满足写写互斥、读写互斥。
行级锁的类型主要有三类:
- Record-Lock,记录锁,也就是仅仅把一条记录锁上;
- Gap-Lock,间隙锁,锁定一个范围,但是不包含记录本身;
- Next-Key Lock: Record-Lock+ Gap-Lock 的组合,锁定一个范围,并且锁定记录本身,
记录锁
锁定单条记录,使得其他事务无法对该记录进行修改。加的X锁、S锁的兼容性可以参照上方表格。
间隙锁
间隙锁只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。
间隙锁虽然存在X型间隙锁和S型间隙锁,但是并没有什么区别;
间隙锁之间是兼容的,即两个事务可以同时持有包含共同间隙范围的间隙锁,并不存在互斥关系,因为间隙锁的目的是防止插入幻影记录而提出的。
记录-间隙锁
Next-Key Lock 又称为临键锁,是 Record-Lock + Gap-Lock 的组合,锁定一个范围,并且锁定记录本身。
所以,next-key lock 即能保护该记录,又能阻止其他事务将新纪录插入到被保护记录前面的间隙中,是行级锁中级别最高的锁。
next-key lock 是包含间隙锁+记录锁的,如果一个事务获取了X型的 next-key lock,那么另外一个事务在获取相同范围的 X型的 next-key lock 时,是会被阻塞的。
比如,一个事务持有了范围为(1,10]的X型的 next-key lock,那么另外一个事务在获取相同范围的X型的 next-key lock 时,就会被阻塞。
虽然相同范围的间隙锁是多个事务相互兼容的,但对于记录锁,我们是要考虑X型与S型关系,X型的记录锁与 x型的记录锁是冲突的。
在判断语句的锁的种类时,一般先认为是临键锁,再根据不同类别判断其是否退化为记录锁或间隙锁。具体判断方法请见下方:加锁过程与锁退化。
插入意向锁
要理解插入意向锁,首先得理解一个关键点:在数据不存在时,无法直接对其加记录锁。记录锁只能加在已存在的记录上,而对于插入操作,记录在插入前并不存在,所以无法直接申请记录锁。但是系统仍然需要确保插入位置不会与其他事务的操作冲突。这就需要一种锁定”意向”而非锁定”记录”的机制。插入意向锁因此而生:最大化插入操作并发性能。
一般情况下(无间隙锁的非阻塞状态),插入意向锁的存在时间非常短暂,几乎会在短暂的时间内快速升级为记录锁(这里是原子性操作)。只有在被间隙锁阻塞时,界面上才会显示为插入意向锁,并陷入等待中(此时任务并未获取任何锁)。
举个例子吧。假设我们有一个包含ID字段的表,当前表中有ID值为5和10的两条记录:
1 | ID |
这里存在的间隙有:(-∞, 5)、(5, 10)和(10, +∞)。令事务A获取间隙锁,事务B获取插入意向锁。
具体时间线如下:
- 事务A开始,获取(5, 10)间隙锁
- 事务B尝试插入ID=7,被阻塞(处于等待状态)
- 事务A提交,释放间隙锁
- 事务B被唤醒,立即获取插入意向锁并执行插入
- 插入完成的同时,锁升级为ID=7上的记录锁(X锁)
- 事务B继续执行或提交
加锁过程与锁退化
行级锁加锁规则比较复杂,不同的场景,加锁的形式是不同的。
加锁的对象是索引(如果之前有说的不严谨的地方还请见谅),加锁的基本单位是 next-key lock,它是由记录锁和间隙锁组合而成的,next-key lock 是前开后闭区间,而间隙锁是前开后开区间。
说实话,加锁的对象是索引这句话还是挺难理解的。这里依旧举个例子:
![]()
执行
UPDATE users SET name='王小五' WHERE id=3;语句时,数据库首先通过主键索引(id=3)定位到记录,并在主键索引上加锁。若查询条件为where age = 28,那么除了二级索引加锁,还会对查询到的记录的主键索引项上加「记录锁」。物理行数据并不直接被锁定,而是通过索引记录的锁间接保护。
但是,next-key lock 在一些场景下会退化成记录锁或间隙锁。
那到底是什么场景呢?一句话,在能使用记录锁或者间隙锁就能避免幻读现象的场景下,next-key lock 就会退化成记录锁或间隙锁。其实看图是非常容易理解的,千万不要望而生畏。
接下来,我们把情况分为5类进行逐一讲解。
1.唯一索引等值查询
记录存在时,退化为记录锁;
记录不存在时,退化为间隙锁。
2.唯一索引范围查询
大于查询:
- 在 id =20 这条记录的主键索引上,加了范围为 (15,20]的 next-key 锁;
- 在特殊记录(supremum pseudo-record)的主键索引上,加了范围为(20,+∞]的 next-key 锁。
大于等于查询:
- 在 id = 15 这条记录的主键索引上,加了记录锁,范围是 id = 15 这一行记录。意味着其他事务无法更新或者删除 id = 15 的这一条记录;
- 在 id = 20 这条记录的主键索引上,加了 next-key 锁,范围是 (15,20]。意味着其他事务即无法更新或者删除 id = 20 的记录,同时无法插入 id 值为 16、17、18、19 的这一些新记录。
- 在特殊记录(supremum pseudo-record)的主键索引上,加了 next-key 锁,范围是 (20,+∞]。意味着其他事务无法插入 id 值大于 20 的这一些新记录。
小于查询:
- 在 id =1这条记录的主键索引上,加了范围为(-∞,1]的 next-key 锁,意味着其他事务即无法更新或者删除 id =1 的这一条记录,同时也无法插入 id 小于1的这一些新记录。
- 在 id =5 这条记录的主键索引上,加了范围为(1,5] 的 next-key 锁,意味着其他事务即无法更新或者删除 id =5 的这一条记录,同时也无法插入 id 值为 2、3、4 的这一些新记录。
- 在 id = 10 这条记录的主键索引上,加了范围为 (5,10) 的间隙锁。
小于等于查询:
- 在id =1这条记录的主键索引上,加了范围为(-∞,1 )的 next-key 锁。意味着其他事务即无法更新或者删除 id =1的这一条记录,同时也无法插入 id 小于 1的这一些新记录。
- 在 id =5 这条记录的主键索引上,加了范围为(1,5]的 next-key 锁。意味着其他事务即无法更新或者.删除 id =5 的这一条记录,同时也无法插入 id 值为 2、3、4 的这一些新记录。
3.非唯一索引等值查询
当我们用非唯一索引进行等值查询的时候,因为存在两个索引,一个是主键索引,-个是非唯一索引(二级索引),所以在加锁时,同时会对这两个索引都加锁,但是对主键索引加锁的时候,只有满足查询条件的记录才会对它们的主键索引加锁。
针对非唯一索引等值查询时,查询的记录存不存在,加锁的规则也会不同。
- 当查询的记录「存在」时,由于不是唯一索引,所以肯定存在索引值相同的记录,于是非唯一索引等值查询的过程是一个扫描的过程,直到扫描到第一个不符合条件的二级索引记录就停止扫描,然后在扫描的过程中,对扫描到的二级索引记录加的是 next-key 锁,而对于第一个不符合条件的二级索引记录该二级索引的 next-key 锁会退化成间隙锁。同时,在符合查询条件的记录的主键索引上加记录锁。
事务 A 加锁变化过程如下:
由于不是唯一索引,所以肯定存在值相同的记录,于是非唯一索引等值查询的过程是一个扫描的过程最开始要找的第一行是 age=22,于是对该二级索引记录加上范围为(21,22]的 next-key 锁。同时,因为 age = 22 符合査询条件,于是对 age= 22 的记录的主键索引加上记录锁,即对id = 10 这一行加记录锁。
接着继续扫描,扫描到的第二行是 age=39,该记录是第一个不符合条件的二级索引记录,所以该二级索引的 next-key 锁会退化成间隙锁,范围是(22,39)。停止查询。
- 当查询的记录「不存在」时,扫描到第一条不符合条件的二级索引记录,该二级索引的 next-key 锁会退化成间隙锁。因为不存在满足查询条件的记录,所以不会对主键索引加锁。
4.非唯一索引范围查询
非唯一索引和主键索引的范围査询的加锁也有所不同,不同之处在于非唯一索引范围查询,索引的 next-key lock 不会有退化为间隙锁和记录锁的情况,也就是非唯一索引进行范围查询时,对二级索引记录加锁都是加 next-key 锁。
比如执行:select * from user where age >= 22 for update;
事务 A 的加锁变化:
- 最开始要找的第一行是 age= 22,虽然范围查询语句包含等值查询,但是这里不是唯一索引范围查询,所以是不会发生退化锁的现象,因此对该二级索引记录加 next-key 锁,范围是(21,22]。
- 同时,对age = 22 这条记录的主键索引加记录锁,即对 id =10 这一行记录的主键索引加记录锁。
- 由于是范围查询,接着继续扫描已经存在的二级索引记录。扫面的第二行是 age=39 的二级索引记录,于是对该二级索引记录加 next-key 锁,范围是 (22,39];
- 同时,对 age = 39 这条记录的主键索引加记录锁,即对id =20 这一行记录的主键索引加记录锁。
- 虽然我们看见表中最后一条二级索引记录是 age= 39 的记录,但是实际在 Innodb 存储引擎中,会用一个特殊的记录来标识最后一条记录,该特殊的记录的名字叫 supremum pseudo-record ,所以扫描第二行的时候,也就扫描到了这个特殊记录的时候,会对该二级索引记录加的是范围为(39,+∞]的next-key 锁。
- 停止查询
问:在 age >= 22 的范围査询中,明明査询 age = 22 的记录存在并且属于等值査询,为什么不会像唯一索引那样,将 age =22 记录的二级索引上的 next-key 锁退化为记录锁?
答:因为 age 字段是非唯一索引,不具有唯一性,所以如果只加记录锁(记录锁无法防止插入,只能防止删除或者修改),就会导致其他事务插入一条 age=22 的记录,这样前后两次查询的结果集就不相同了,出现了幻读现象。
5.没有加索引查询
前面的案例,我们的查询语句都有使用索引查询,也就是查询记录的时候,是通过索引扫描的方式查询的,然后对扫描出来的记录进行加锁。
如果锁定读查询语句,没有使用索引列作为查询条件,或者查询语句没有走索引查询,导致扫描是全表扫描。那么,每一条记录的索引上都会加 next-key 锁,这样就相当于锁住的全表,这时如果其他事务对该表进行增、删、改操作的时候,都会被阻塞。
不只是锁定读査询语句不加索引才会导致这种情况,update 和 delete 语句如果查询条件不加索引,那么由于扫描的方式是全表扫描,于是就会对每一条记录的索引上都会加 next-key 锁,这样就相当于锁住的全表。
因此,在线上在执行 update、delete、select .. for update 等具有加锁性质的语句,一定要检查语句是否走了索引,如果是全表扫描的话,会对每一个索引加 next-key 锁,相当于把整个表锁住了,这是挺严重的问题。
死锁问题
什么是死锁
想象一下这样的场景:四个朋友围坐在一张圆桌前吃饭。餐厅的规矩很特别,每两个人之间只有一双筷子,而且每个人必须同时拿到左右两双筷子才能开始吃饭。
现在,如果所有人都先拿起了自己右手边的筷子,然后都在等待左手边的筷子,会发生什么呢?所有人都卡住了!每个人手里都有一双筷子,但又都在等另一双,而这双筷子正被别人握在手中。没有人能开始吃饭,也没有人愿意放下手中的筷子。这就是一个典型的死锁情况。
在计算机系统中,死锁是指两个或更多进程互相等待对方释放资源,导致所有相关进程都无法继续执行的状态。就像我们的就餐例子一样,系统中的进程相互等待,形成一个无法打破的等待循环,最终导致系统资源浪费和任务停滞。
死锁发生条件及举例
比如可重复读隔离级别下,这个场景会发生什么?
可以看到,事务A和 事务 B都在执行 insert 语句后,都陷入了等待状态(前提没有打开死锁检测),也就是发生了死锁,因为都在相互等待对方释放锁。
原因是,Time1建立X型间隙锁,范围为(20,30);Time2建立X型间隙锁,范围同样为为(20,30),二者不冲突。接下来,由于Time3时A中insert语句被B中间隙锁阻塞,Time4时B中insert语句被A中间隙锁阻塞,因此陷入死锁。
本次案例中,事务 A和事务 B在执行完后 update 语句后都持有范围为(28,30) 的间隙锁,而接下来的插入操作为了获取到插入意向锁,都在等待对方事务的间隙锁释放,于是就造成了循环等待,满足了死锁的四个条件:互斥、占有且等待、不可强占用、循环等待,因此发生了死锁。
Insert冲突解决
1.记录之间加有间隙锁
每插入一条新记录,都需要看一下待插入记录的下一条记录上是否已经被加了间隙锁,如果已加间隙锁,此时会生成一个插入意向锁,然后锁的状态设置为等待状态(PS: MySQL 加锁时,是先生成锁结构,然后设置锁的状态,如果锁状态是等待状态,并不是意味着事务成功获取到了锁,只有当锁状态为正常状态时,才代表事务成功获取到了锁),现象就是 Insert 语句会被阻塞。
2.遇到唯一键冲突
如果在插入新记录时,插入了一个与「已有的记录的主键或者唯一二级索引列值相同」的记录(不过可以有多条记录的唯一二级索引列的值同时为NULL,这里不考虑这种情况),此时插入就会失败,然后对于这条记录加上了S型的锁。
- 如果主键索引重复,插入新记录的事务会给已存在的主键值重复的聚簇索引记录添加S型记录锁;
- 如果唯一二级索引重复,插入新记录的事务都会给已存在的二级索引列值重复的二级索引记录添加S.型 next-key 锁。
如何避免死锁
由宏观至微观考虑:(数据库级)mysql参数—>(事务级)大事务拆分—>(行范围级)减间隙锁改RC/减范围加索引—>(行级)最可能造成锁冲突的锁往后放
实际上死锁是不能完全避免的,只要会加锁,在并发的场景就会发生死锁,但是我们可以通过一些手段,降低发生死锁的概率。
MySQL 的锁是在事务提交的时候才会释放的,所以可以通过缩短锁持久的时间,来降低死锁的概率,比如:
- 如果事务中需要锁多个行,要把最可能造成锁冲突的锁的申请时机尽量往后放,这样事务的持久锁的时间就会比较短。
- 避免大事务,尽量将大事务拆成多个小事务来处理,因为大事务占用耗时长,意味着占用锁占用时间长,与其他事务冲突的概率也会变高;
可以通过减少间隙锁,来降低死锁的概率:
- 如果能确定幻读和不可重复读对应用的影响不大,可以考虑将隔离级别改成 RC,因为 RC 隔离级别没有间隙锁,可以避免间隙锁导致的死锁;
可以通过减少加锁范围,来降低死锁的概率:
- 给表添加合理的索引,如果不走索引将会为表的每一行记录加行级锁,死锁的概率就会大大增大;
可以通过MySQL参数设置,来降低死锁的概率:
- 设置合适的锁等待超时阈值,当一个事务的等待时间超过该值后,将回滚当前语句(而不是整个事务),如果要回滚整个事务,请使用“innodb rollback on timeout”开启值为:ON,开启这个参数之后,锁超时就会对这个事务进行回滚,于是锁就释放了。
- 开启主动死锁检测,主动死锁检测在发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。
如何排查死锁
在遇到线上死锁问题时,我们应该第一时间获取相关的死锁日志。我们可以通过show engine innodb status命令来获取死锁信息。
然后就分析死锁日志。死锁日志通常分为两部分,上半部分说明了事务1在等待什么锁,下半部分说明了事务2当前持有的锁和等待的锁。
举例:对两个事务进行以下操作:
| 步骤 | 事务1 | 事务2 |
|---|---|---|
| 1 | begin | |
| 2 | delete from test where a = 2; | |
| 3 | begin | |
| 4 | delete from test where a = 2; (事务1卡住) | |
| 5 | insert into test (id, a) values (10, 2); | |
| 6 | 提示出现死锁:ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction. |
输入上方神秘代码之后查看日志:
从日志里我们可以看到事务1当前正在执行delete from test where a = 2,该条语句正在申请索引a的X锁,所以提示lock_mode X waiting。
从日志的HOLDS THE LOCKS(S)块中我们可以看到事务2持有索引a的X锁,并且是记录锁(Record Lock)。该锁是通过事务2在步骤2执行的delete语句申请的。
可以看到,事务2正在申请S锁,也就是共享锁。该锁是insert into test (id,a) values (10,2)语句申请的。insert语句在普通情况下是会申请排他锁,也就是X锁,但是这里出现了S锁。这是因为a字段是一个唯一索引,所以insert语句会在插入前进行一次duplicate key的检查,为了使这次检查成功,需要申请S锁防止其他事务对a字段进行修改。
那么为什么该S锁会失败呢?这是对同一个字段的锁的申请是需要排队的。S锁前面还有一个未申请成功的X锁,所以S锁必须等待,所以形成了循环等待,死锁出现了。
因此,通过阅读死锁日志,我们可以清楚地知道两个事务形成了怎样的循环等待,然后根据当前各个事务执行的SQL分析出加锁类型以及顺序,逆向推断出如何形成循环等待,这样就能找到死锁产生的原因了。
悲观锁与乐观锁
概念相关
首先我们需要清楚的一点就是无论是悲观锁还是乐观锁,都是人们定义出来的概念,可以认为是一种思想。
悲观锁(Pessimistic Lock): 就是很悲观,每次去拿数据的时候都认为别人会修改。所以每次在拿数据的时候都会上锁。这样别人想拿数据就被挡住,直到悲观锁被释放,悲观锁中的共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。这样的好处是增加数据的并发安全性。
但是在效率方面,处理加锁的机制会产生额外的开销,还有增加产生死锁的机会。另外还会降低并行性,如果已经锁定了一个线程 A,其他线程就必须等待该线程 A 处理完才可以处理。
数据库中的行锁,表锁,读锁(共享锁),写锁(排他锁),以及 syncronized 实现的锁均为悲观锁。
乐观锁(Optimistic Lock): 就是很乐观,每次去拿数据的时候都认为别人不会修改。所以不会上锁,但是如果想要更新数据,则会在更新前检查在读取至更新这段时间别人有没有修改过这个数据。如果修改过,则重新读取,再次尝试更新,循环上述步骤直到更新成功(当然也允许更新失败的线程放弃操作),乐观锁适用于多读的应用类型,这样可以提高吞吐量。
相对于悲观锁,在对数据库进行处理的时候,乐观锁并不会使用数据库提供的锁机制。一般的实现乐观锁的方式就是记录数据版本(version)或者是时间戳来实现,不过使用版本记录是最常用的。
实现方式
mysql默认的实现方式就是悲观锁。那么如何实现乐观锁呢?
可以基于版本号来实现乐观锁,修改数据的时候带上版本号(或者时间戳):
可以在数据库表增加一个版本号字段,利用这个版本号字段在数据库中实现乐观锁。
具体的实现:
每次更新数据的时候,都要带上版本号,同时将版本+1,比如现在要更新id=1,版本号为2的记录。这时候先要获取id=1的版本号,然后更新语句写成 update table set name=“小明”,version = version+1 where id = 1 and version =2。
比如:
1 | UPDATE student SET name ="小李",version= 2 WHERE id= 100 AND version= 1 |
如果这个版本号与表记录中的版本号一致的话,就能更新成功,如果不相等则不进行更新,然后需要重新获取该记录的最新版本号,然后再尝试更新数据。
最后总结,其实乐观锁和悲观锁并无高下之分。像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行重试,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。
附:如何在线添加索引?
这里考察的是元数据锁的知识。
线上环境可能存在很多事务都在读写这张表,如果对这张表进行了表结构修改,就会发生阻塞,原因是有事务对这张表进行读写操作的时候,会生成元数据读锁,而修改表结构的时候,会生成元数据写锁,这时候就产生了读写冲突,所以修改表结构的操作就会阻塞,并且后续事务的增查改操作都会阻塞。
所以最好的办法是: 停机修改!比如停机 30 分钟,或者找一个夜间访问量比较少的时间去改。😑
锁篇!就此完结!!!





