关于在业务中 MYSQL 事务查询的一个疑惑

164 天前
 leejinhong

我有一个疑问,在实际业务中,当我们使用 MySQL 进行查询时,某一瞬间得到的值在下一瞬间可能会发生变化。这是否意味着在某一瞬间拿到的值不够可靠?

举个例子,在仓库库存管理中,如果在查询时库存是 30 ,但在查询完成后,其他业务操作改变了库存,导致库存变为 25 。然而,业务处理时依旧基于查询时的库存 30 进行操作,这样就会导致记录流水的值出现误差。

目前我能想到的解决办法是:

使用乐观锁,通过在记录中添加版本号或时间戳字段,在更新数据时进行校验,确保数据的一致性。 使用悲观锁,在查询时锁定数据行,直到事务结束,防止其他事务修改数据。 除此之外,还有其他方式可以解决这个问题吗?例如,在确保数据一致性和处理准确性的前提下,有没有更高效或更适合实际业务场景的方法?

2319 次点击
所在节点    MySQL
32 条回复
sun1991
164 天前
好奇还有什么跳出乐观锁悲观锁的解决方案...
dzdh
164 天前
虽然但是。 难道不是所有人都这么干的吗。

select 出来的值永远都不可靠,仅作为临时显示用。只有再更新的时候使用 field = field +-*/ val 判断是否成功。
leejinhong
164 天前
@sun1991 这里所谓的锁是通过业务去实现, 比如在仓库记录里面加上一个 version 或者其他的一个标识。比如当前你获取的 version 是 1 ,这个时候你处理业务后,进行的时候加上 version 条件,如果期间有人变动库存了,这个 version 就会发生变化,你更新的时候就更新失败了
timethinker
164 天前
本质原因就在于数据竞争。数据竞争就是同时有多个写入者对同一个状态进行修改,就会造成覆盖,想要得到预期的结果,避免错误的覆盖,前提就需要 CompareAndSet 这种语义操作( SET value = newValue WHERE value = oldValue ),这样就可以根据操作结果(影响行数)来决定下一步该做什么(回滚事务、取消订单 balabala ),这就是乐观锁。

至于悲观锁,本质上它不是解决并发问题,而是避免/阻止并发问题。
leejinhong
164 天前
@dzdh 是啊,但是就是在想有没有其他对业务侵入不会很大的方案 哈哈
leejinhong
164 天前
@dzdh 是这样子没问题,但是如果记录变化的很频繁的时候,这个更新就经常会出现失败
leejinhong
164 天前
@timethinker 数据竞争这个说的很对,想表达就类似于这个意思
fkname
164 天前
用悲观锁吧,尽量减小锁的范围
dzdh
164 天前
@leejinhong #6

中间件。重试放在中间件里做。go 写个 fake mysql server 。配置表+字段。哪个表的哪个字段自动重试直到成功。最终无法成功输出 error 错误
tool2dx
164 天前
我也倾向于 8 楼的悲观锁,多线程编程,大部分都是先来后到原则。

既然轮到我,查询出来的库存是 30 ,那就等我全部处理完,入库结束,后面的人都排队等着。
woodfizky
164 天前
在 MySQL 里 update table set value = value +/- something where condition xxx 这个操作是否原子级的呢?是的话应该就不用考虑竞争的问题吧。

你如果担心竞争,其实解决方案之一就是保证原子操作,且操作不会因为值变化而被干扰。

如果需要考虑最终值是否满足某条件,则在 where 条件里再加一个条件,最终观察 updated 影响的行数来判断是否 update 成功。但是这个在 MySQL 好像不适用,因为有些情况一个表设置了某些时间字段 on_update 会更新,这个时候好像 updated 也会有值。
me1onsoda
163 天前
负载不高就把重复读开起来
timethinker
163 天前
补充一下,什么时候使用悲观锁,什么时候使用乐观锁,取决于当前业务的更新频率。如果更新频率不高,使用乐观锁有助于提升读取性能(读多写少)。反之,如果乐观更新大概率会失败的情况下,使用悲观锁的性能可能比重试乐观锁要好得多,但是要注意死锁等问题。
sagaxu
163 天前
用 update xxx set foo = foo + bbb ,就算中间变了,终值也是准确的

必须读取,加工再写回的,先过一遍分布式锁,拿到锁了再 SELECT FOR UPDATE WHERE id=xxx
catamaran
163 天前
既然只要并发就要重新处理,干脆串行就完了,既简单,又不复杂。说白了,就是排队。14 楼的方案在限制超发的场景下不能用。
leejinhong
163 天前
@woodfizky 非原子级的,事务里面是有业务操作的,当前的做法就是加一个 version 字段,只要更新就会自动 version=version+1 这个,处理业务的时候先查询出来 version 加到 where 条件里面判断。如果影响函数为 0 ,证明处理业务期间该数据发生变化了。
kele1997
162 天前
@leejinhong mysql 有默认提交的话,每一行 sql 都是有事务保证原子性吧

而且默认 mysql 隔离级别不就是可重复读嘛,不就保证了一个事务多次读获得同一个值
vishun
162 天前
总结下:
- 数据库层面:
- 要么 field=field+xxx 来保证原子性,比较简单,但是无法阻止超发等场景。
- version 乐观锁,大概率不会重复的情况下用。
- select for update 悲观锁,经常重复的情况下用或不太在乎性能下用。
- 极端的隔离级别设置为序列化,额,貌似很少用。
- 应用方面
- 各种应用锁、redis 锁,分布式锁等。
mbeoliero123
162 天前
行 sql 更新是会加行锁吧?这里并发冲突主要是你的 where 能不能找到原来的记录,如果是 where id = xxx ,这种并发度再高也是对那条记录进行串行操作,如果是 where version = xxx ,version 随时会变这种,感觉就是 4 楼说的乐观锁处理
wenxueywx
160 天前
乐观锁策略(读多写少场景):
- 查询库存值
- 更新库存时带上之前查询的库存数据:update xxx set stock = stock-1 where id = xxx and stock = xx;
- 更新成功才记流水;更新失败就重试
悲观锁策略(写多读少场景):
- 查询库存时就加锁,select * from xxx where xxx for update;
- update
- 记流水

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

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

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

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

© 2021 V2EX