恼骚

最近在搞并发的问题,订单的异步通知和主动查询会存在并发的问题,用到了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)。

如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。 具体响应方式由开发者根据实际需要决定。

如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。

其间如果有其他对该记录做修改或加排他锁的操作,都会等待我们解锁或直接抛出异常。

以下这句话应用来自:http://www.cnblogs.com/bigfish--/archive/2012/02/18/2356886.html

在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)中才能生效。

注2: 要测试锁定的状况,可以利用MySQL的Command Mode ,开二个视窗来做测试。(点开链接,这里已经有人做个测试了)

先开始一把

使用悲观锁的原理就是,当我们在查询出 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;

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

主动查询 

1、加锁

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

执行结果,一直在阻塞中

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

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

2、不加锁

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

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

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

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

主动查询加锁则不受影响

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

下面研究以下行锁和表锁

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

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

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

  

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

例3:无主键,table lock

说明:

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

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

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

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

例4: 主键不明确,table lock

说明:

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

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

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

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

例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, 0 rows 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 set
Time: 0.001s
mysql@:Tinywan>

相同订单号第二次查询

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;

被阻塞掉了

不同订单号第一次查询

mysql@:Tinywan> START TRANSACTION;
Query OK, 0 rows 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 set
Time: 0.002s
mysql@:Tinywan>

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='支付订单';

相同订单号第一次查询

相同订单号第二次查询

不同订单号第一次查询

第一个提交事务

第二个提交事务

参考:

1、理解悲观锁与乐观锁

MySQL学习笔记(四)悲观锁与乐观锁的更多相关文章

  1. MySQL学习(四)深入理解乐观锁与悲观锁

    转载自:http://www.hollischuang.com/archives/934 在数据库的锁机制中介绍过,数据库管理系统(DBMS)中的并发控制的任务是确保在多个事务同时存取数据库中同一数据 ...

  2. MySql学习笔记四

    MySql学习笔记四 5.3.数据类型 数值型 整型 小数 定点数 浮点数 字符型 较短的文本:char, varchar 较长的文本:text, blob(较长的二进制数据) 日期型 原则:所选择类 ...

  3. MySQL学习笔记(四):存储引擎的选择

    一:几种常用存储引擎汇总表 二:如何选择 一句话:除非需要InnoDB 不具备的特性,并且没有其他办法替代,否则都应该优先考虑InnoDB:或者,不需要InnoDB的特性,并且其他的引擎更加合适当前情 ...

  4. MySQL学习笔记四:字符集

    1.字符集就是字符和其编码的集合,查看数据库支持的字符集 show character set 2.查看服务端启动时默认的字符集 mysql> show variables like 'char ...

  5. MySQL学习笔记(四)—存储过程

    一.概述      存储过程是数据库定义的一些SQL语句的集合,然后直接调用这些存储过程和函数来执行已经定义好的SQL语句.存储过程可以避免开发人员重复的编写相同的SQL语句,而且存储过程是在MySq ...

  6. mysql学习笔记四 —— AB复制

    要点:ab复制 mysql集群架构流程: ABB(主从复制)-->MHA(实现mysql高可用.读写分离.脚本控制vip飘逸)-->haproxy(对slave集群实现分发,负载均衡)-- ...

  7. MySql学习笔记(四) —— 数据的分组

    前面介绍的聚集函数只是用来计算行数,平均数,最大值,最小值而不用检索所有数据.通过count()函数,我们可以计算生产商1003提供的产品数目,但如果我要查询所有生产商提供的商品数,这就需要进行分组查 ...

  8. 谈谈MySQL支持的事务隔离级别,以及悲观锁和乐观锁的原理和应用场景?

    在日常开发中,尤其是业务开发,少不了利用 Java 对数据库进行基本的增删改查等数据操作,这也是 Java 工程师的必备技能之一.做好数据操作,不仅仅需要对 Java 语言相关框架的掌握,更需要对各种 ...

  9. 第36讲 谈谈MySQL支持的事务隔离级别,以及悲观锁和乐观锁的原理和应用场景

    在日常开发中,尤其是业务开发,少不了利用 Java 对数据库进行基本的增删改查等数据操作,这也是 Java 工程师的必备技能之一.做好数据操作,不仅仅需要对 Java 语言相关框架的掌握,更需要对各种 ...

随机推荐

  1. 进程命令(taskkill)

    taskkill 命令: // 描述: 结束一个或多个任务或流程. // 语法: taskkill [/s <computer> [/u [<Domain>\]<User ...

  2. SQLServer之触发器简介

    触发器定义 触发器是数据库服务器中发生事件时自动执行的一种特殊存储过程.SQLServer允许为任何特定语句创建多个触发器.它的执行不是由程序调用,也不是手工启动,而是由事件来触发,当对数据库进行操作 ...

  3. 通过ip查询自己电脑的共享文件夹

    查看电脑所有的共享文件或文件夹的三种方法如下: 方法一. 右键点击网上邻居,点击属性进入网上邻居属性页面. 选中本地连接,在窗口的左下方有详细信息,可以看到内网IP,记住IP地址. 直接在地址栏输入记 ...

  4. Django-CRM项目学习(六)-rbac模块(权限组件)

    1.rbac权限组件 1.1 模板分析(五表结构) 1.2 模板构建 人物和角色进行多对多绑定,角色与权限进行多对多绑定.其中人物和角色的多对多绑定的操作可以避免相同的人物多重权限的操作. 1.3 数 ...

  5. logrotate日志处理

    介绍 logrotate旨在简化生成大量日志文件的系统的管理.它允许日志文件的自动轮换.压缩.删除和邮件.每个日志文件可以每天.每周.每月处理,也可以在它变得太大时处理.通常,logrotate作为每 ...

  6. SkylineGlobe 7.0.1 & 7.0.2版本Web开发 如何正确使用三维地图控件和工程树控件

    Skyline TerraExplorer Pro目前正式发布的7.0.1&7.0.2版本,还只是64位的版本, 在Web开发的时候,如何在页面中正确嵌入三维地图控件,让一些小伙伴凌乱了. 下 ...

  7. WinForm程序完全退出总结

    this.Close();   只是关闭当前窗口,若不是主窗体的话,是无法退出程序的,另外若有托管线程(非主线程),也无法干净地退出: Application.Exit();  强制所有消息中止,退出 ...

  8. springBoot2.0+redis+fastJson+自定义注解实现方法上添加过期时间

    springBoot2.0集成redis实例 一.首先引入项目依赖的maven jar包,主要包括 spring-boot-starter-data-redis包,这个再springBoot2.0之前 ...

  9. 使用Nodejs在Windows上调用CMD命令

    要用nodejs执行cmd,需要引入一个包node-cmd npm install node-cmd var cmd=require('node-cmd'); cmd.get( 'notepad',/ ...

  10. Grovvy带参数的闭包

    定义带参数的闭包: