mysql数据库乐观锁和悲观锁_MySQL学习笔记(四)悲观锁与乐观锁

恼骚

最近在搞并发的问题,订单的异步通知和主动查询会存在并发的问题,用到了Mysql数据库的 for update 锁

在TP5直接通过lock(true),用于数据库的锁机制

Db::name('pay_order')->where('order_no',‘S1807081342018949’)->lock(true)->find();

打印生成的SQL语句

SELECT * FROM `pay_order` WHERE `order_no` = 'S1807081342018949' LIMIT 1 FOR UPDATE

上面的查询语句中,我们使用了 select…for update 的方式,这样就通过开启排他锁的方式实现了悲观锁。此时在 pay_order 表中,order_no 为S1807081342018949 的那条数据就被我们锁定了,其它的事务必须等本次事务提交之后才能执行。这样我们可以保证当前的数据不会被其它事务修改。

上面我们提到,使用 select…for update 会把数据给锁住,不过我们需要注意一些锁的级别,MySQL InnoDB默认行级锁。行级锁都是基于索引的,如果一条SQL语句用不到索引是不会使用行级锁的,会使用表级锁把整张表锁住,这点需要注意。

理解悲观锁与乐观锁

在数据库的锁机制中介绍过,数据库管理系统(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: (明确指定主键,若查无此笔资料,无lock)

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;

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

先开始一把

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

第一次,开启事务,但是不提交事务

异步通知

-- 开启事务

START TRANSACTION;

-- 查询订单

SELECT id,order_no,`status` FROM `pay_order` WHERE `order_no` = 'S1807081342018949' LIMIT 1 FOR UPDATE;

-- 修改订单

UPDATE `pay_order` SET `status` = 11 WHERE id = 347;

COMMIT;

-- 查询数据是否修改成功

SELECT id,order_no,`status` FROM `pay_order` WHERE `order_no` = 'S1807081342018949' LIMIT 1 FOR UPDATE;

执行结果:很快就执行完毕了,但是数据并没有修改成功(注意:但是重复执行一次,则数据又修改成功了)

45dcc57a6e3a911cdf23ac53f2e08a15.png

主动查询

1、加锁

SELECT id,order_no,`status` FROM `pay_order` WHERE `order_no` = 'S1807081342018949' LIMIT 1 FOR UPDATE;

执行结果,一直在阻塞中

2aa665b468908873dcfdba7052e66878.png

过一会,会自动取消锁机制

[Err] 1205 - Lock wait timeout exceeded; try restarting transaction

2、不加锁

SELECT id,order_no,`status` FROM `pay_order` WHERE `order_no` = 'S1807081342018949';

执行结果,没有阻塞,则能正常查询出数据,不会受第一个事务的影响

925d6b220bacd975baa4600f4095dd6e.png

第二次,开启事务,提交事务

异步查询开启事务,提交事务

主动查询加锁则不受影响

总结:锁如果是回滚或者提交事务,会自动释放掉锁的。

下面研究以下行锁和表锁

例1: 明确指定主键,并且有此数据,row lock

19ca78a2bb48fd48212d3cbb01330ee2.png

说明:通过上面的演示,可以清楚的看到,锁的是同一个记录(id = 347),记录(id = 348)并没有受到上一条记录的影响。

例2: 明确指定主键,若查无此数据,无lock

57cebd9acdbd827a102db4a3b90d250f.png

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

例3:无主键,table lock

c766097cf40ba3d92dd93734787df996.png

说明:

窗口1 开启了事务,查询订单号 : order_no = "S1807081342018949",查询数据正常。

窗口2 也开启了事务,查询订单号 : order_no = "S1807081342018949",查询阻塞,说明 窗口1把该记录给锁住了(其实这里表已经被锁定, 而不是该记录了)。

窗口3开启了事务,查询订单号 : order_no = "S1807171712053133",查询阻塞,说明 窗口1把该表给锁住了,不是同一条记录都不给查啊,阻塞的不要不要的。

只有 窗口1 的记录回滚或者提交了,窗口2的查询阻塞立刻释放掉了,但是 窗口3 依然在阻塞中(由于 窗口2开启了事务导致的)。同理,回滚或者提交 窗口2 的事务后,窗口3 的记录也可以正常查询了。

例4: 主键不明确,table lock

26e7fa97c432ca0eefacbeae13212a14.png

说明:

窗口1 开启了事务,查询主键 : id > 375 的记录,查询数据正常(3条记录)。

窗口2 也开启了事务,查询订单号 :  id > 375 的记录,查询阻塞,说明 窗口1把该记录给锁住了(其实这里表已经被锁定, 而不是该记录了)。

窗口3开启了事务,查询订单号 : id > 376 的记录,查询阻塞,说明 窗口1把该表给锁住了,不是同一条记录都不给查啊,阻塞的不要不要的。

只有 窗口1 的记录回滚或者提交了,窗口2 的查询阻塞立刻释放掉了,但是 窗口3 依然在阻塞中(由于 窗口2 开启了事务导致的)。同理,回滚或者提交 窗口2 的事务后,窗口3 的记录也可以正常查询了。

70477dd55bef5f3f2c7d4c73a0c65b0d.png

例5: 主键不明确,table lock

select * from pay_order where id<>1 for update;

索引对数据库的锁定级别

例6: 明确指定索引,并且有此数据,row lock

mysql> select id,status,order_no from pay_order where status=1 for update;

+------+----------+-------------------+

| id | status | order_no |

|------+----------+-------------------|

| 348 | 1 | S1807081353042055 |

| 349 | 1 | S1807081356043257 |

+------+----------+-------------------+

13 rows in set

Time: 0.003s

注意:上面的字段 status 是建立过索引的

例7: 明确指定索引,若查无此数据,无lock

mysql> select id,status,order_no from pay_order where status=11 for update;

+------+----------+------------+

| id | status | order_no |

|------+----------+------------|

+------+----------+------------+

0 rows in set

Time: 0.001s

演示操作

1、定义索引字段开启事务加悲观锁

pay_order 表结构,order_no 定义索引。

CREATE TABLE`pay_order` (

`id`int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '#',

`order_no`varchar(255) NOT NULL COMMENT '订单号',PRIMARY KEY(`id`) USING BTREE,UNIQUE KEY`idx_order_no` (`order_no`) USING BTREE

) ENGINE=InnoDB AUTO_INCREMENT=514 DEFAULT CHARSET=utf8mb4 COMMENT='支付订单';

相同订单号第一次查询

mysql@:Tinywan> START TRANSACTION;

Query OK,0rows affected

Time:0.000s

mysql@:Tinywan> SELECT id,order_no,`status` FROM `pay_order` WHERE order_no = 'S64064191 -> 1161202555241' LIMIT 1 FOR UPDATE;+------+------------------------+----------+

| id | order_no | status |

|------+------------------------+----------|

| 11 | S640641911161202555241 | 1 |

+------+------------------------+----------+

1 row in setTime:0.001s

mysql@:Tinywan>

6c99fd7d3b06d81c8aa6bb0a737b4ac6.png

相同订单号第二次查询

mysql@:Tinywan> START TRANSACTION;

Query OK, 0 rows affected

Time: 0.001s

mysql@:Tinywan> SELECT id,order_no,`status` FROM `pay_order` WHERE order_no = 'S640641911161 -> 202555241' LIMIT 1 FOR UPDATE;

被阻塞掉了

11010ee113139af8ac1032bc432d098b.png

不同订单号第一次查询

mysql@:Tinywan> START TRANSACTION;

Query OK,0rows affected

Time:0.000s

mysql@:Tinywan> SELECT id,order_no,`status` FROM `pay_order` WHERE

-> order_no = 'T705961911161238428844' LIMIT 1 FOR UP ->DATE;+------+------------------------+----------+

| id | order_no | status |

|------+------------------------+----------|

| 25 | T705961911161238428844 | 0 |

+------+------------------------+----------+

1 row in setTime:0.002s

mysql@:Tinywan>

d4733a266d661d4ad5beeed09a576f29.png

2、未定义索引

表结构

CREATE TABLE`pay_order` (

`id`int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '#',

`order_no`varchar(255) NOT NULL COMMENT '订单号',PRIMARY KEY(`id`) USING BTREE

) ENGINE=InnoDB AUTO_INCREMENT=514 DEFAULT CHARSET=utf8mb4 COMMENT='支付订单';

相同订单号第一次查询

042790cd511096a4fceee17466ae3316.png

相同订单号第二次查询

64111dea655808d2b34a8984e9c7b3f5.png

不同订单号第一次查询

ae6b8271b0167db302b75cb0713c70fa.png

第一个提交事务

d6671fdb4a841251584da65207d530e2.png

第二个提交事务

072de220bec913384e0cfa081c6caf1f.png

参考:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
回答: 乐观锁悲观锁是实现并发控制的两种主要手段。乐观锁适用于读多写少的情况,它不依赖于数据库中的锁机制,而是通过在表中新增一个版本号来实现。乐观锁的特点是无论是否开启事务,都可以在逻辑上实现乐观锁乐观锁的开销比悲观锁小,但一旦发生锁冲突需要回滚,开销比较大。乐观锁适合用在取锁失败概率较小的场景,可以提升系统的并发性能。\[2\] 悲观锁适用于写多读少的情况,它需要依赖数据库的锁机制来实现。在事务中使用select ... for update操作来实现悲观锁,可以最大程度地保证数据操作的独占性。悲观锁的特点是需要开启事务,并且会对扫描过的行进行锁定,这可能导致性能问题。悲观锁主要用于数据争用激烈的环境,以及在锁保护数据的成本低于回滚事务的成本的情况下使用。\[3\] 需要注意的是,乐观锁悲观锁不仅仅存在于关系型数据库系统中,还存在于其他系统中,如hibernate、tair、memcache等。因此,不应该将乐观锁悲观锁与其他数据库锁进行对比。它们是一种思想,用于实现并发控制。\[1\] #### 引用[.reference_title] - *1* [MySql悲观锁(行锁)和乐观锁](https://blog.csdn.net/weixin_45433031/article/details/120838045)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [MySQL中的悲观锁乐观锁](https://blog.csdn.net/lamp_yang_3533/article/details/79180370)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值