[用事实说明两个凡是]一个由mysql事务隔离级别造成的问题分析

背景

最近要做一个批跑服务, 基本逻辑就是定时扫描数据库的记录, 有满足条件的就进行处理(一条记录代表一个任务,以下任务与记录含义相同). 要求支持多机部署批跑服务.

批跑支持多机部署实现方案

要实现多机部署, 只要保证每个批跑服务实例每次只获取一条记录, 处理完再获取下一条即可. 其中最种要的是避免不同的实例获取到同一条记录,即所谓抢任务.

先看表结构设计:

create database if not exists ae;
create table ae.task (
id int primary key,
status int);
-- status为0说明任务可处理,其它不可处理

以上是简化的表结构,但足以说明本文试图说明的问题.

要避免抢任务, oracle的做法, 直接

update ae.task set status=1 where status=0 and rownum = 1 returning id

即可.

mysql的要啰嗦一点:

select id from ae.task where status=0; -- 得到ID
update ae.task where id = ${id} and status=0;

这两个sql,第一个sql用于获取符合条件的任务, 第二个sql用户将任务锁定. 在并发的场景下, 有可能不同的批跑实例的第一个SQL会返回相同的记录, 但第二个sql只有一个会更新成功, 通过判断affected rows即可知道哪个锁定成功. 锁定成功的继续处理本任务, 锁定失败的继续处理其它任务.

问题现象

管理后台提交了一个任务后, 两个批跑实例恰好同时启动, 进入抢任务环节. 结果发现异常, 其中一个实例成功抢到任务, 但另一个实例则挂死了:

抢到任务的实例:

2015-11-23 19:42:01|INFO|exec_task.php|40||get one task: 11
...
2015-11-23 19:42:01|INFO|exec_task.php|107||line_count: 9
2015-11-23 19:42:01|INFO|exec_task.php|147||fork child success: 8346
2015-11-23 19:42:01|INFO|exec_task.php|264||[0] pid: 8346, start: 0, stop: 0

2015-11-23 19:42:01|INFO|exec_task.php|147||fork child success: 8347
2015-11-23 19:42:01|INFO|exec_task.php|264||[1] pid: 8347, start: 1, stop: 1

2015-11-23 19:42:01|INFO|exec_task.php|147||fork child success: 8348
2015-11-23 19:42:01|INFO|exec_task.php|264||[2] pid: 8348, start: 2, stop: 2

2015-11-23 19:42:01|INFO|exec_task.php|147||fork child success: 8349
...

没有抢到任务的实例:

2015-11-23 19:42:01|INFO|function.inc.php|100||task_id 11 is locked by another process, get next task
2015-11-23 19:42:01|INFO|function.inc.php|100||task_id 11 is locked by another process, get next task
2015-11-23 19:42:01|INFO|function.inc.php|100||task_id 11 is locked by another process, get next task
2015-11-23 19:42:01|INFO|function.inc.php|100||task_id 11 is locked by another process, get next task
2015-11-23 19:42:01|INFO|function.inc.php|100||task_id 11 is locked by another process, get next task
2015-11-23 19:42:01|INFO|function.inc.php|100||task_id 11 is locked by another process, get next task
2015-11-23 19:42:01|INFO|function.inc.php|100||task_id 11 is locked by another process, get next task
2015-11-23 19:42:01|INFO|function.inc.php|100||task_id 11 is locked by another process, get next task
2015-11-23 19:42:01|INFO|function.inc.php|100||task_id 11 is locked by another process, get next task

可以看到没有抢到任务的实例进入了死循环.

原因分析

按照我们之前的设计, 如果第二条SQL锁定任务的时候失败了, 获取下一个任务. 应当不会死循环. 死循环的原因是因为没有抢到任务的实例, 在执行第一个SQL的时候, 一直返回了相同的记录(id=11,实际上当时也只有一条记录)

请注意, 抢到任务的实例抢到任务后, 会把状态更新并提交, 按说抢不到任务的实例会看到此状态更新,并导致第一条sql查不到数据,然后 正常退出.

而事实上抢不到任务的实例看不到此变化, 说明事务隔离级别(Transaction Isolation Level)不是"READ COMMITED", 而是其它. 经确认, 级别是"REPEATABLE-READ"

mysql> select @@TX_ISOLATION;
+-----------------+
| @@TX_ISOLATION  |
+-----------------+
| REPEATABLE-READ |

"REPEATABLE-READ" 看到的数据是事务启动时的样子,所以看不到抢到任务的实例对任务状态的修改. 进而导致死循环.

请注意执行第一个SQL查询满足条件的任务是在一个事务内进行的. 此事务实际上是业务的需要, 除了获取到任务,还需要获取其它资源,如果获取不到其它 资源, 则rollback任务,以便下次处理.

ORACLE相应的事务隔离级别是"Serializable Isolation Level", 如上描述的这个场景, 在ORACLE下的反应是抢不到任务的实例在试图更新任务状态的 时候,会返回一个"ORA-08177: Cannot serialize access for this transaction"错误, 程序也可以正常退出. 详见<> 第9章"Overview of Oracle Database Transaction Isolation Levels"

mysql在"REPEATABLE-READ"的事务隔离级别上的表现是不能让人满意的. 查询到的数据是事务启动时的样子,但更新的时候看到的数据又是其它事务提交 后的结果,并且update也没有错误提示.

而"SERIALIZABLE"更糟糕, 如果同时开了两个session, 干脆直接锁表了, 谁了更新不了. 这就势必造成另一个问题, 既然大家都更新不了,那就rollback事务, 重试呗. 但是重试也是很有可能大家再同时开了事务,又锁死了, 一直死循环. 为了解决这种情况,可能的做法是, 各自等待一个随机时间再重试,让随机打破这个僵局. 不知道是否有其它办法,欢迎指教.

解决方法

  1. 修改session的事务隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

1.不断查询满足条件的任务不要放到一个事务里. 发现"affected rows"为0,更新不到数据时, 事务rollback,重新启动事务. 即在循环里不断开启事务而不是在事务里不断循环.

  1. 还有一个办法是开事务然后select for update, 但是这种方法会导致锁表, 必须等待其它事务提交后才能返回. 当初我进行设计的时候,是计划使用select for update的方式的, 但是最终没有使用, 现在回想, 可能是没有开事务, 结果两个实例都查询到了相同的记录, 所以被我否定了. 但是看我另一个文章 <>又似乎可能是由于锁表而弃用了, 原因已经不可考了.

但从本个需求来说, 似乎使用select for update来让把表锁住会更简单.

另一个问题

你以为抢到任务的实例就可以高枕无忧了吗, 错了! 等他高高兴兴处理完任务, 要把任务状态置为成功时, 发现这个任务居然被没有抢到任务的实例给锁了, 自已只能得到一个锁超时的错误

2015-11-23 19:42:52|ERR|function.inc.php|113||SQL fail: Lock wait timeout exceeded; try restarting transaction

请期待下一个问题分析.

补充说明

今天回来确认了一下, 实际上ORACEL的update task set status = 1 where status = 0 and rownum = 1 returning taskid 这个SQL也会把表锁住.

所以可以用@flygogo 在30楼提出的方法模拟oracle 的returning

SET @update_id := 0;
update ae.task set status=1, id = (SELECT @update_id := id) where status=0 limit 1; SELECT @update_id;

而postgresql的update似乎没有limit 1之类的限定只更新一条的写法?

同时ORACLE和postgresql的select for update 也都会锁表.

所以就本文所讨论的范围来说, 似乎不能说是两个凡是. 叉!

再补充说明

差点被绕晕了. 其实本文所指出的mysql在"REPEATABLE-READ"事务隔离级别下的表现是奇怪的,不直观的,这点值得注意. 明明select出来的数据是可更新, 而更新时候又没有成功, 会让人非常疑惑. 而为oracel在"Serializable"级别下发现数据已经被更新了之后,抛出"ORA-08177"的做法才更直观更合适.

本文另一个意义是分享了一种不锁表实现队列的方法

文章转载自 开源中国社区[https://www.oschina.net]

时间: 2024-10-18 17:05:46

[用事实说明两个凡是]一个由mysql事务隔离级别造成的问题分析的相关文章

两个路由器 一个宽带号 怎么样都能无线

现在很多人家里都会有无线路由器,而有的时候,你路由器放在1楼而你的卧室在4楼的时候,你会发现你的手机基本上收不到无线信号,这时候你就可以通过多买一个路由器来实现两个路由器 一个宽带号 都能开头无线 .这个问题是有一个网友问我的,在这边把我跟他说的解决方法黏贴出来. ......(这个是小编的名字) 16:50:07 现在是你家有台路由器,你邻居家也有路由器,但是你们2个只有一个宽带号 百度问问(这个是给网友备注的名字) 16:50:21 对啊 ...... 16:50:47 你家路由器的线是接在

vs2008-mfc两个listcontrol一个是问题一个是答案

问题描述 mfc两个listcontrol一个是问题一个是答案 rt,一个是问题,一个是答案,都可以动态添加,每个问题都对应不同数量的答案,也就是说我点击问题list中不同问题时,答案list要显示不同的答案,请问这个该怎么做.... 解决方案 一个是阆苑仙葩,一个是美玉无瑕一个是阆苑仙葩,一个是美玉无瑕一个是阆苑仙葩,一个是美玉无瑕 解决方案二: 1.点击问题list项目,建立选择发生变化的消息处理函数,获得索引值n 2.设置答案list显示答案数组内容,假设char answer[50][1

端口- 求助,两台服务器一个ip的问题!

问题描述 求助,两台服务器一个ip的问题! 两台服务器1,192.168.1.3, windows2008,iis,域名解析到这台服务器的IP地址.2,192.168.1.2,linux,php,装了一个web应用程序. 现在想从外网访问192.168.1.3主站页面,并通过主页上连接访问1.2, 由于各种原因,无法做域名解析,无法设置路由器的端口映射,有没有什么别的办法,在1.3上做设置,让外网能访问1.2呢? 谢谢!!! 解决方案 不能做端口映射,这个办不到.如果1.3可以映射,那么可以在1

java中这两个构造方法一个队,一个错?感觉没有区别啊?

问题描述 java中这两个构造方法一个队,一个错?感觉没有区别啊? 第一种: public class point { private double x; private double y; public point(){ this(0,0); } public point(double x, double y){ this.x = x; this.y = y; } } 第二种 public class point { private double x; private double y; pu

两个网站 一个赚钱 一个亏 为什么?

中介交易 http://www.aliyun.com/zixun/aggregation/6858.html">SEO诊断 淘宝客 云主机 技术大厅 我只是一名菜鸟,不敢讲什么营销,优化之道,只是把我的两个网站经营过程说出来,以供大家讨论 云南三七网 2007年8月份注册域名www.yn2007.com,九月份网站上线,主要经营云南著名特产文山三七,昭通天麻,到现在关键字排名还不错[大家可以在百度搜索一下三七,三七花,三七粉看看,可是一直都是亏损,完全是个摆设. 过程: 一制作前的想法:家

oracle数据库-ORacle数据库两个会话一个在查询另一个在删除.

问题描述 ORacle数据库两个会话一个在查询另一个在删除. ORacle数据库两个会话一个在查询另一个在删除,在第一个会话还没有查询完毕时第二个会话删除数据,这样第一个会话完成时会不会查出第二个会话删除的数据? 解决方案 这个的话,应该读不到数据的. 数据库里面读和写还是不一样的. 写的话会有锁保护,读就没有了. 如果再查询之前已被删掉,那么是读不到了应该 解决方案二: 看有没有提交,提交了就看不到

助乔布斯成功的两个秘密 一个90后创业者的血和泪

助乔布斯成功的两个秘密 一个90后创业者的血和泪 时间:2014-07-31 22:08 来源:互联网的一些事 作者:佚名 7月31日 一些事 晚报 | 汇总今日最热资讯 1.[苹果陷"窃密门"遭集体诉讼!一些国家的政要已不再使用iPhone]十年前,在互联网萌芽的年代,人们常说,"你甚至无法确定坐在 对方电脑面前是不是一条狗".那时候,人们在互联网上是隐形的... 2.[互联网思维不是产品连上互联网]不知从何时起,我们来到了言必称"互联网思维"

防手机病毒记住“两个不要一个经常”

支招 防手机病毒 记住"两个不要一个经常" 国家计算机病毒应急处理中心副主任陈建民说,尽管手机病毒看起来很可怕,但消费者也不用过分担心.因为从全球范围看,我国的手机病毒暴发情况并不算特别严重,关键是要养成良好的手机使用习惯. 如何将手机病毒拒之门外?陈主任说,其实很简单,归纳起来就是"两个不要一个经常": 第一,不要轻易将手机借给别人.因为恶意软件都需要安装在手机里才能发挥作用,只要能确保自己的手机不离手,手机受到的威胁就会小很多: 第二,不要打开陌生号码发来的短信

一个用于MySQL的PHP XML类

mysql|xml 我承认我不是PHP的领导者.然而,在看了一些PHP的信息之后,我认为有一些功能需要添加到其中来处理数据库连接和整合XML.要做到这一点,我想我可以创建一个处理连接MySQL和使用PHP中的domxml功能来提供XML输出的类.然后我就可以在PHP脚本的任何地方声明这个类并且在需要使用它的时候可以提供XML功能. 我假设人们使用PHP是原因是他的标价:免费.MySQL为需要向系统中增加数据库功能的开发人员提供一个免费的数据库解决方案.这些解决方案的缺点是在设置和管理的时候有些复