V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
MySQL 5.5 Community Server
MySQL 5.6 Community Server
Percona Configuration Wizard
XtraBackup 搭建主从复制
Great Sites on MySQL
Percona
MySQL Performance Blog
Severalnines
推荐管理工具
Sequel Pro
phpMyAdmin
推荐书目
MySQL Cookbook
MySQL 相关项目
MariaDB
Drizzle
参考文档
http://mysql-python.sourceforge.net/MySQLdb.html
leejinhong
V2EX  ›  MySQL

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

  •  
  •   leejinhong · 129 天前 · 2267 次点击
    这是一个创建于 129 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

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

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

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

    32 条回复    2024-07-24 13:44:42 +08:00
    sun1991
        1
    sun1991  
       129 天前
    好奇还有什么跳出乐观锁悲观锁的解决方案...
    dzdh
        2
    dzdh  
       129 天前
    虽然但是。 难道不是所有人都这么干的吗。

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

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

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

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

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

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

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

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

    你这个例子就不可能在 MySQL 中发生. 一个事务不结束, 其他无法事务修改这个 record.
    leejinhong
        22
    leejinhong  
    OP
       124 天前
    @louettagfh MYSQL 的隔离级别除非是串行化或者是你在查询的时候加上锁,不的话正常情况其他的事务都可以改当前的这条记录
    louettagfh
        23
    louettagfh  
       124 天前
    @leejinhong 你自己试一下呢? 看看能不能两个未 commit 的事务改同一个 record?
    leejinhong
        24
    leejinhong  
    OP
       124 天前
    @louettagfh #23 一个事务查询出来是 30 ,进行业务操作先不 commit ,另外一个事务直接变更 25 进行 commit ,是可以的。实际模拟很简单:A 脚本开启一个事务,进行查询那条记录记录为 30 ,然后 sleep 5 秒钟,这个时候开启另外一个脚本 B 里面更新那条记录,更新为 25 ,然后进行 commit ,这个时候脚本 A 里面查询 30 这个库存它就是错误的
    louettagfh
        25
    louettagfh  
       124 天前
    @leejinhong 你直接使用隔离级别 RR 不就行了
    leejinhong
        26
    leejinhong  
    OP
       124 天前
    @louettagfh #25 RR 隔离级别不就是可重复读? A 脚本在这个事务读取的就是开启事务时候的快照,这个时候的库存不就永远都是 30 吗?
    louettagfh
        27
    louettagfh  
       123 天前
    @leejinhong 使用 RR , 你的第二个事务不就改不了么, 这不就是你要的效果吗, 否则你又要别的事务可以改, 你又要第一个事务始终读取正确的值, 这不是数据库能保证的.
    asmile1993
        28
    asmile1993  
       121 天前
    @leejinhong 如果只是简单的 select ,在 RR 隔离级别下看到的的确是事务开始时的值,但当对记录进行修改操作时,用的是当前读—读最新的已提交记录,而不是一开始 select 得到的结果,想要了解更深的话,搜索下快照读和当前读。
    leejinhong
        29
    leejinhong  
    OP
       121 天前
    @asmile1993 你当前都最新提交的记录(这里的最新是相对来说的最新),如果你读到最新之后又有人修改了该条记录,它就不是最新的记录了
    asmile1993
        30
    asmile1993  
       118 天前
    @leejinhong 你为什么要以 select 出来的结果来做变更呢?上面已经有人提过了,你先用 select 查出来数据,判断是否能变更,如果不能,直接返回;如果能,那么就以 update t set value = value - 变化量 where ...,这样不就是当前读了吗?读的就是最新的数据啊,然后根据更新的行数来判断是否更新成功,0 则表示没有满足条件的记录,非 0 表示更新成功。
    leejinhong
        31
    leejinhong  
    OP
       118 天前
    @asmile1993 #30 首先:这个帖子主题的本质是想要了解有没有更好更优先非入侵业务的方案可供选择。 其次你以上说的这个 update t set value = value - 变化量 where ...这个是没问题的,但是有些业务是需要插入变动前、变动后的记录。
    asmile1993
        32
    asmile1993  
       116 天前
    @leejinhong 明白你的意思了,我记得流水表只记录差值,变更前后的值要靠当前值和差值构建出来。你要想记录,只能用悲观锁,变更前用 select ... for update/share 来读出最新值。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3569 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 30ms · UTC 04:45 · PVG 12:45 · LAX 20:45 · JFK 23:45
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.