MySQL系列 | 悲观锁与乐观锁最佳实践

在关系数据库管理系统里,悲观并发控制(又名“悲观锁”,Pessimistic Concurrency Control,缩写“PCC”)是一种并发控制的方法。它可以阻止一个事务以影响其他用户的方式来修改数据。如果一个事务执行的操作读某行数据应用了锁,那只有当这个事务把锁释放,其他事务才能够执行与该锁冲突的操作。悲观并发控制主要用于数据争用激烈的环境,以及发生并发冲突时使用锁保护数据的成本要低于回滚事务的成本的环境中。

一、理解悲观锁与乐观锁获取锁的前提:结果集中的数据没有使用排他锁或共享锁时,才能获取锁,否则将会阻塞。需要注意的是, FOR UPDATE 生效需要同时满足两个条件时才生效:

  • 数据库的引擎为 innoDB
  • 操作位于事务块中(BEGIN/COMMIT)

二、理解悲观锁与乐观锁

在数据库的锁机制中介绍过,数据库管理系统(DBMS)中的并发控制的任务是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和一致性以及数据库的一致性。

乐观并发控制(乐观锁)和悲观并发控制(悲观锁)是并发控制主要采用的技术手段。无论是悲观锁还是乐观锁,都是人们定义出来的概念,可以认为是一种思想。其实不仅仅是数据库系统中有乐观锁和悲观锁的概念,像memcache、hibernate、tair等都有类似的概念。

针对于不同的业务场景,应该选用不同的并发控制方式。所以,不要把乐观并发控制和悲观并发控制狭义的理解为DBMS中的概念,更不要把他们和数据中提供的锁机制(行锁、表锁、排他锁、共享锁)混为一谈。其实,在DBMS中,悲观锁正是利用数据库本身提供的锁机制来实现的。三、悲观锁的流程

在对任意记录进行修改前,先尝试为该记录加上排他锁(exclusive locking)。如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。具体响应方式由开发者根据实际需要决定。

如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。其间如果有其他对该记录做修改或加排他锁的操作,都会等待我们解锁或直接抛出异常。在oracle中,利用 select * for update 可以锁表。假设有个表单products ,里面有id跟name二个栏位,id是主键。案例说明
1 明确指定主键,并且查询的数据存在,行锁(row lock)

SELECT * FROM products WHERE id='3' FOR UPDATE;

2 明确指定主键,并且查询的数据 不存在,无锁

SELECT * FROM products WHERE id='-1' FOR UPDATE;

3 无主键,表锁(table lock)

SELECT * FROM products WHERE name='Mouse' FOR UPDATE;

4 主键不明确,表锁(table lock)

SELECT * FROM products WHERE id<>'3' FOR UPDATE;

5 主键不明确,表锁(table lock)

SELECT * FROM products WHERE id LIKE '3' FOR UPDATE;

注意:FOR UPDATE仅适用于InnoDB,且必须在交易区块(BEGIN/COMMIT)中才能生效。

测试说明

使用悲观锁的原理就是,在查询出 pay_order 信息后就把当前的数据锁定,直到我们修改完毕后再解锁。那么在这个过程中,因为 pay_order 被锁定了,就不会出现其他操作者来对其进行修改了。

1、不开启事务,明确指定主键,且该数据存在

图片

可以看出如果不在事务中,则查询加锁是无效的。

2、开启事务,明确指定主键,且该数据存在。提交和不提交事务

图片
  • 开启事务,不提交事务,按照主键查询该语句,则第二个查询相同主键的的语句会阻塞。
  • 开启事务,不提交事务,按照主键查询该语句,则第二个查询不相同主键的的语句不会阻塞。
  • 开启事务,锁如果是回滚或者提交事务,会自动释放掉锁的。按照主键查询该语句,则第二个查询相同主键的的语句会自动解除阻塞(锁已经被释放掉了),查询结果。
  • 说明:通过上面的演示,可以清楚的看到,锁的是同一个记录(id = 9),记录(id = 8)并没有受到上一条记录的影响。

3、开启事务,明确指定主键,但该数据不存在

图片

说明:窗口1 查询结果为空。窗口2 查询结果也为空,查询无阻塞,说明 窗口1 没有对数据执行锁定。

4、开启事务,不指定主键,且该数据存在

图片
  • 窗口1 开启了事务,查询订单号 :order_no = “S640641911161202555241″,查询数据正常。
  • 窗口2 也开启了事务,查询订单号 :order_no = “S640641911161202555241″,查询阻塞,说明 窗口1 把该记录给锁住了(其实这里表已经被锁定, 而不是该记录了)。
  • 窗口3 开启了事务,查询订单号 :order_no = “S1807171712053133″,查询阻塞,说明 窗口1 把该表给锁住了,不是同一条记录都不给查啊,阻塞的不要不要的。
  • 只有 窗口1 的记录回滚或者提交了,窗口2 的查询阻塞立刻释放掉了,但是 窗口3 依然在阻塞中(由于 窗口2 开启了事务导致的)。同理,回滚或者提交 窗口2 的事务后,窗口3 的记录也可以正常查询了。

5、开启事务,主键不明确,则会表锁(table rock)

图片
  • 窗口1 开启了事务,查询主键 :id > 511 的记录,查询数据正常(3条记录)。
  • 窗口2 也开启了事务,查询订单号 :id > 511 的记录,查询阻塞,说明 窗口1 把该记录给锁住了(其实这里表已经被锁定, 而不是该行被锁住了)。
  • 窗口3 开启了事务,查询订单号 :id > 512 的记录,查询阻塞,说明 窗口1 把该表给锁住了,不是同一条记录都不给查啊,阻塞的不要不要的。
  • 只有 窗口1 的记录提交事务了,窗口2 的查询阻塞还是没有立刻释放掉,窗口3 的记录记录则是自动释放掉了。只能说明窗口1提交事务只是释放了整个表的锁,而不是这个行的锁。
  • 只有 窗口3 的记录提交(回滚)事务了,窗口2 的查询阻塞则会释放掉。证明了主键不明确开启事务查询后果很严重。只要有一个不明确的事务查询存在,则这个表一直是被锁定的,太可怕了!!!

四、小结

当执行 select … for update时,将会把数据锁住,因此,我们需要注意一下锁的级别。MySQL InnoDB 默认为行级锁。当查询语句指定了主键时,MySQL会执行「行级锁」,否则MySQL会执行「表锁」。常见情况如下:

  • 若明确指明主键,且结果集有数据,行锁;
  • 若明确指明主键,结果集无数据,则无锁;
  • 若无主键,且非主键字段无索引,则表锁;
  • 若使用主键但主键不明确,则使用表锁;

innoDB的行锁是通过给索引上的索引项加锁实现的,因此,只有通过索引检索数据,才会采用行锁,否则使用的是表锁。

五、总结

悲观锁采用的是「先获取锁再访问」的策略,来保障数据的安全。但是加锁策略,依赖数据库实现,会增加数据库的负担,且会增加死锁的发生几率。

此外,对于不会发生变化的只读数据,加锁只会增加额外不必要的负担。在实际的实践中,对于并发很高的场景并不会使用悲观锁,因为当一个事务锁住了数据,那么其他事务都会发生阻塞,会导致大量的事务发生积压拖垮整个系统。

阅读原文

简介:专注互联网技术干货分享。包括Redis、PHP、MySQL、Nginx/Openresty、网关、消息队列、Docker、微服务、代码案例等。欢迎关注微信公众号:开源技术小栈
(0)
打赏 喜欢就点个赞支持下吧 喜欢就点个赞支持下吧

声明:本文来自“开源技术小栈”,分享链接:https://www.zyxiao.com/p/299599    侵权投诉

网站客服
网站客服
内容投稿 侵权处理
分享本页
返回顶部