MySQL 的悲观锁和乐观锁如何使用?

当今是分布式架构的天下,在这种架构当中存在着各式各样的锁:大到分布式锁,小到代码的锁,还有数据库的锁。尽管这些各不相同的锁令人头疼,我们对锁的语义却都是相同的:

同一时刻只有获取锁的线程可以运行,其他线程必须等到锁释放才有可能执行

从上面的定义我们可以看出锁是作用于线程的,并且是排他的。此外要注意以上定义是针对写入操作的,对于读取操作并不适合,因为读取的情况往往不需要锁。

今天我们的主题是 MySQL 的锁,首先我们需要知道 MySQL 的锁在这么多“锁”之中处于什么样的地位。只有认清楚 MySQL 锁的位置,我们才能更好地使用它。我们从大家相对熟悉的应用的锁说起。

应用的锁和数据库的锁

无论你使用什么编程语言,这门语言都会有锁的语法,比如 Java 中的 Synchronized、Go 中的 Mutex 等等。确实,对业务场景来说,即便数据库不支持锁机制,也可以通过代码来实现“同一时刻只有一个线程可以访问”的效果。更多情况下我们会选择在应用中使用锁,因为开发者更熟悉自己所掌握的代码,数据库对于他们来说只是外部存储介质,能实现增删改查就行了。

举个实际的例子,我们经常要做的唯一性校验,它既可以放在应用中实现,也可以利用数据库的唯一性约束,二者都可以很好地实现。

而在更复杂的场景中,比如分布式环境,通常就要引入分布式锁了,因为我们要找到一个可以集中管控“锁”的中枢,这个中枢通常是 Redis。你还能找到别的中枢吗?对,另一个中枢就是 MySQL 本身。是不是有种骑马找马的感觉?如果 MySQL 本身就可以充当锁,我们又何必苦苦寻找另一个中枢呢?显然,利用 MySQL 的锁大大降低了系统复杂度,它把一个复杂分布式架构简化成为“传统的应用 + 数据库”模型

悲观锁

下面我们先来聊聊 MySQL 的悲观锁,悲观锁是显性的锁,你可以清楚地看到锁相关的语法。并且锁需要 MySQL 的 InnoDB 引擎和事务才可以生效,这是前提

悲观锁的语法是什么呢?很多同学会想到 select for update 语句,但这么想并不准确。实际上 MySQL 悲观锁的语法关键字就是 update在同一时间执行同一行操作的两条 update 语句之间只有一个会执行,另一个只能等待

后来人们发现除了 update 需要锁,很多情况下 select 也需要锁,从 select 获取锁开始到事务提交或者回滚,整个过程都具有排他性因此人们需要为 select 提供类似 update 悲观锁的能力,于是就有了 select for update 语句,它代表让 select 语句像 update 一样可以锁定行,直到事务执行完才能释放,我们来看下具体例子:

begin;
--A 操作
select name from user where id=1 for update;
--B 操作
commit;

这段事务中 select for update 夹在 A 操作和 B 操作之间,当执行到 select for update 时会对 ID 为 1 的行加锁,B 操作执行时其他线程不能操作当前的行直到 commit 执行才能释放锁。我们再来看一段代码:

begin;
--A 操作
update user set name='lg' where id=1;
--B 操作
commit;

这次我们把 select for update 语句换成 update 语句,当执行到 update 时会对 ID 为 1 的行加锁,B 操作执行时其他线程不能操作当前的行,直到 commit 执行才能释放锁。

可见,上面两段代码在锁的效力上完全一样。

那么如果想观察“锁”的过程该怎么办呢?其实只要让 B 操作执行的久一些就可以了,这时可以使用 MySQL 的 sleep 函数。

begin;
--A 操作
update user set name='lg' where id=1;
select sleep(20);
commit;

上面执行完 update 语句以后将 sleep 20 秒再 commit,这时如果你打开另一个窗口执行其他 update 操作,那么你的行为将被阻塞。

默认情况下,MySQL 只会锁定唯一索引行,如果没有唯一索引,将会锁表。

SELECT * FROM user WHERE id=1 FOR UPDATE;

比如上面,ID 为 1 的行确实存在,并且 ID 是唯一索引,因此会锁定这一行,这就是我们常说的行级锁。如果 ID 对应的行不存在,则不会产生任何锁。

SELECT * FROM user WHERE id>1 FOR UPDATE;

我们稍微改一下这个 SQL 条件,此时 ID 指向一个不明确的,或者是无限的范围,MySQL 找不到具体的行,就会进行表锁

SELECT * FROM user WHERE name='lg' FOR UPDATE;

 

 

之后id为2的name变成1234567了。

 

我们再改一下这个 SQL,name 列没有唯一索引,MySQL 依然会进行表锁

总而言之,MySQL 行级锁锁的是有限的唯一索引,找不到有限的唯一索引,就会锁表。

乐观锁

MySQL 乐观锁是基于悲观锁实现的,从这个角度来看也可以认为 MySQL 本身并没有乐观锁,但是可以通过巧妙的方法来实现。

乐观锁是悲观锁相对的存在,它假设数据不会发生冲突,因此在数据进行 commit 的时候,才会正式对数据的冲突情况进行检测,如果发现冲突了,则直接返回用户错误信息,让用户决定下一步如何做。乐观锁实现一般都是增加一列版本号 version,当更新数据时对版本号进行更新。

使用版本号时,可以在数据初始化时指定一个版本号,在每次数据更新时都对版本号进行 +1 操作。

select * from user where id=1
update user set name=#{name},version=version+1 where id=1 and version=0;

假设 2 个线程分别执行上面的语句,线程 A 和线程 B 为 name 传入不同的值,其中 select 语句是可以并行执行的,假设 select 语句执行以后返回的 version 字段值为 0,那么下面我们就该执行 update 语句。而因为 MySQL 悲观锁的特性,两个线程不可能同时 update 一条数据,所以在 update 同一条数据的时候,是有先后顺序的,只有在第一个线程执行完 update,才能释放行锁,让第二个线程继续进行 update。

第一个线程执行完成后,version 字段值将变成 1,所以第二个线程修改失败,实现了乐观锁控制。

今天我们聊了 MySQL 的悲观锁和乐观锁,其实 MySQL 只有悲观锁,乐观锁是悲观锁的一种巧妙用法。使用 MySQL 的锁可以简化我们的架构,尤其是分布式环境下,不需要再引入其他的锁。

当然并不是所有场景都适合使用 MySQL 的锁。虽然现在 MySQL 分表的场景也很多,但是遇到对性能特别敏感的场景比如秒杀,多数情况还是使用 Redis 之类的 NoSQL 来实现,这种情况下分布式锁就有用武之地了。在流量不是特别极端的情况下,MySQL 可以很好地支持业务,使用 MySQL 自带的锁也是完全足够的。

补充一句,一旦你决定了要用 MySQL 的锁,一定要评估好流量,做好压测工作,整体来说 MySQL 的性能还是很强劲的。

最后希望我的分享可以帮到你,欢迎你在评论区给我留言,也欢迎把这篇文章分享给你的朋友!

  • 5
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

智达教育‍

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值