mysql 死锁的问题,求优化意见

2017-06-21 22:01:51 +08:00
 letitbesqzr

遇到的问题

假设拿医院系统的药品部分举例, 药品表结构大概如下:

现在有多个地方可能会去操作药品的数量:

  1. 护士生成请领单 (减存库数量,加冻结数量),可能会涉及到一堆表,那么这些操作都会在一个事务里,在操作存库的时候会把操作的行自动锁定起来,等待事务结束才会释放
  2. 手工划价药品 (直接减库存数量) 同上,会操作很多表的数据,一样会锁操作的行
  3. 药房发药 (减冻结数量) 同上,会操作很多表的数据,修改请领单状态等等,一样会锁操作的行
  4. ......

因为在系统里可能某些药的使用频率是非常高的,比如一些生理盐水、葡萄糖之类的。那么就经常会遇到,护士在生成请领单事务还没完成,药房正在发的药中也含有这些药,这样互相等待直接就被 mysql 的死锁检测机制检测到。

想请问大家遇到这种问题一般会采用什么样的方案来解决?

自己想到的方法以及问题

  1. 大事务拆各个小事务,比如药房发药,新事务->首先修改请领单状态->提交。新事务->再去减存库->提交。但是这样如果在减存库的地方异常了,不就不会回滚到 请领单状态
  2. 使用同步,我们首先尝试在生成请领单的方法上加上同步,这样多个护士在同时生成请领单的时候的确是不会遇到问题了,但是生成请领单又可能和发药死锁。如果加到减库存的方法,那似乎没什么用?就算减存库同步执行了,但是上层事务还没跑完 也会遇到这问题吧?
  3. 使用队列,因为我们现在多项目同时运行,并且都可能去操作存库,所以考虑过使用队列,把所有操作存库都丢到队列去单线程执行。但是改动速度有点大。

不知大家是否还有其他比较好的解决方案

3917 次点击
所在节点    MySQL
20 条回复
henry19890701
2017-06-21 22:20:50 +08:00
保证加锁顺序就可以了,代码应该不怎么需要改
letitbesqzr
2017-06-21 22:23:23 +08:00
@henry19890701 那其实也就只有让他同步,单线程的去减存库?
Mirana
2017-06-21 22:56:27 +08:00
加锁失败回滚 然后重试
wind3110991
2017-06-21 23:10:05 +08:00
感觉这个最主要的问题并不是事务或者是锁,而是你的流程问题:
为什么操作失败就要 回滚到 请领单状态?
建议如果并发量大容易锁,流程中可以穿插多个原子事务,失败回滚到一个中间状态,而不是后台一个接口大包大揽。
事务不是万能的,要符合你的当前使用场景才行
billlee
2017-06-21 23:12:37 +08:00
@letitbesqzr #2 这个肯定是要同步的。别说是数据库,就算是内存里的变量,多线程操作也要加锁进临界区啊。
cjyang1128
2017-06-21 23:18:21 +08:00
一般死锁问题都是通过保证加锁顺序实现的。除了楼主提的几个方案之外,可以把某些数据存储在 redis 中,因为 redis 是单线程的,所以不存在竞争问题。因为你本质上是 id=>数量的一个映射,所以也可以考虑一下。
letitbesqzr
2017-06-21 23:20:52 +08:00
@wind3110991 就比如发药
1. 将请领单状态设置为已发药
2. 各种计费改状态
3. 减少存库
4. 写存库流水
5. 记录日志

那么某一步失败肯定需要回滚到第一个状态,实际情况一个事务里还会做更多的操作,业务非常复杂
letitbesqzr
2017-06-21 23:32:19 +08:00
@wind3110991 意思就是,其实大事务拆成一个个短事务是比较常见的做法?
3dwelcome
2017-06-21 23:50:29 +08:00
如果是我的话,就用单线程队列。拆分事务只能让逻辑变复杂。代码应该多遵循 KISS 原则,能简单处理的问题,别复杂化。
ebony0319
2017-06-21 23:57:52 +08:00
如果是我我会采取队列方式。谁先就应该给谁,不应该抢资源。
letitbesqzr
2017-06-22 00:04:32 +08:00
@ebony0319
@3dwelcome
如果放队列的话 也就相当于 操作存库的每条 sql 都不会在事务里了吧?那其实没办法失败后回滚了?
ryd994
2017-06-22 01:06:51 +08:00
一种药一个事务
A 药开不出和 B 药没有联系,不需要锁一起
reus
2017-06-22 08:21:30 +08:00
手工上锁 + 事务
msg7086
2017-06-22 10:01:20 +08:00
上锁超时跳过,回头来重试呗。
万一某个药没货了,病人别的药也不让吃了么……
jianzhiyao020
2017-06-22 15:29:57 +08:00
如果,
并发量不是特别大的话,
可以选择序列化事务隔离级别,
绝对不会死锁。
但是伴随来说,
速度会相应降低,
但是应该总比死锁好。
letitbesqzr
2017-06-22 15:31:42 +08:00
@jianzhiyao020 试过,但是预算了一下,病人得排队到马路上。
jianzhiyao020
2017-06-22 15:40:32 +08:00
@letitbesqzr 那就要概念 hack 了,
例如葡萄糖放在一个地方,
大家都去拿,那是否是会增加堵塞的概率,
可以护士那里放一点,
药房那里放一点,
是不是就不会那么堵塞了,
好了,
我说那么多,
其实就是将葡萄糖分开几个记录存取。
wind3110991
2017-06-22 22:13:37 +08:00
@letitbesqzr 如你说的,回滚没有必要从滚到 1 之前啊
比如你在 5 出错了,滚到 4 不就好了,记录下当前状态,在队列等待就好了啊
ebony0319
2017-07-06 23:31:56 +08:00
最近又查了一些资料,好像都不满意,想问问你这个问题你们目前的思路么。
letitbesqzr
2017-07-06 23:36:19 +08:00
@ebony0319
1. 减短事务
2. 使用了 mq 队列进行增删存库
目前采用上面两种方案已经能够满足目前的请求量了,下面一个预备的方案
3. 存库的数量都丢到 redis 中 程序只是操作 redis 里面的存库 定期同步到数据库

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/370160

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX