V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
• 请不要在回答技术问题时复制粘贴 AI 生成的内容
chaleaochexist
V2EX  ›  程序员

不懂就问系列, web + 并发 + 锁 问题,具体情况在正文

  •  
  •   chaleaochexist · 2019-06-03 10:08:11 +08:00 · 3289 次点击
    这是一个创建于 2019 天前的主题,其中的信息可能已经有所发展或是发生改变。
    不知道这样的问题,如何归纳成主题.

    数据库写操作.
    其中一个字段`version`需要+1 操作.
    在高并发情况下,如何确保 version 唯一.

    table 大概是这样的

    | version | object_name | operation |
    | :-----: | :---------: | :-------: |
    | 1 | 小明 | 吃饭 |
    | 2 | 小明 | 吃饭 |
    | 1 | 小红 | 吃饭 |
    | 1 | 小红 | 睡觉 |

    version 只在 object_name 和 operation 相同的情况下+1.
    1. 自增不行.
    2. 可以加一个唯一约束.
    3. 有没有在后端的代码里面加锁控制? 求最佳实践和通用解决方案.
    30 条回复    2019-06-06 16:14:27 +08:00
    chaleaochexist
        1
    chaleaochexist  
    OP
       2019-06-03 10:09:05 +08:00
    v2markdown 不支持表格吗...
    Maboroshii
        2
    Maboroshii  
       2019-06-03 10:12:42 +08:00
    redis ?
    zhengxiaowai
        3
    zhengxiaowai  
       2019-06-03 10:15:39 +08:00
    行锁了解一下
    chaleaochexist
        4
    chaleaochexist  
    OP
       2019-06-03 10:19:51 +08:00
    @zhengxiaowai +1 操作是在代码里执行. 行锁如何控制? 没想明白.
    liuzhedash
        5
    liuzhedash  
       2019-06-03 10:21:33 +08:00
    2、唯一约束不能直接解决问题:当发现重复记录的时候,如何处理?
    3、理论上可以,但是假设未来需要多台服务器进行负载均衡,这个方法就失效了。
    3L 的行锁应该可以,但是我没有试过。
    之前我遇到类似的问题是通过把请求先缓存到 redis 队列中,然后逐个插入数据库解决的。
    NaVient
        6
    NaVient  
       2019-06-03 10:22:18 +08:00
    redis 实现分布式锁不行吗 以{table_name}_{id}的方式做键值
    NewDraw
        7
    NewDraw  
       2019-06-03 10:24:23 +08:00 via Android
    没看明白你说的啥意思。
    是说,在现有表的基础上再插入一条 小明,吃饭,该记录 version 的值要是 3 嘛?

    如果是上面的问题,那么就是问题就可以转化为如何解决幻读,四种事务隔离级别可以了解一下,这种情况只能用表锁了。
    chaleaochexist
        8
    chaleaochexist  
    OP
       2019-06-03 10:25:20 +08:00
    @liuzhedash
    谢谢.
    2. 唯一约束可以解决问题, 代码里面捕获异常,然后+2,+3...
    3. 您说的对.谢谢.
    4. 行锁您能从理论上解释一下吗?我没反应过来,虽然我知道行锁是啥.也许对行锁了解不扎实.给点提示?
    5. redis 可行. 我在想想.
    airfling
        9
    airfling  
       2019-06-03 10:25:28 +08:00
    高并发应该是读的高并发吧,那就是加个写的锁,当写入完毕释放锁
    guiling
        10
    guiling  
       2019-06-03 10:26:30 +08:00
    这不是乐观锁么?
    update b set version=version+1 where version=?
    更新之前先查一下版本,需要其他限制加在 where 后面
    chaleaochexist
        11
    chaleaochexist  
    OP
       2019-06-03 10:26:33 +08:00
    @NewDraw
    对两个线程同时对数据库 小明 吃饭 +1 写了两条 version=3 的数据,这是不可以被接受的.
    NewDraw
        12
    NewDraw  
       2019-06-03 10:28:50 +08:00 via Android   ❤️ 1
    额,是我说的意思啊,两个线程并发写入,就会有幻读的情况,可以让数据库隔离级别设为最高级。
    chaleaochexist
        13
    chaleaochexist  
    OP
       2019-06-03 10:29:21 +08:00
    @airfling 考虑这种情况
    两个线程 已经读到 version = 2
    各自+1...

    写锁的约束会约束到代码吗?
    这块不懂.
    请多指教.
    chaleaochexist
        14
    chaleaochexist  
    OP
       2019-06-03 10:30:22 +08:00
    @NewDraw 我刚才脑袋抽了.我在想想,好像你说的对.
    mooncakejs
        15
    mooncakejs  
       2019-06-03 10:30:36 +08:00   ❤️ 1
    太追求数据库层面解决不是一个好的实践,锁缓存是个好主意,#6 楼说的分布式锁。
    liukanshan
        16
    liukanshan  
       2019-06-03 10:35:12 +08:00
    每一次更新操作之前 检查下版本号

    def fun(){
    def version = select version from xxx;
    //do something


    update xxx set field=value,version = version + 1 where version = $version;
    }
    night98
        17
    night98  
       2019-06-03 10:38:57 +08:00
    新增一个唯一主键,按唯一主键 + version 确定单条记录。
    然后执行楼上说的:
    update b set version=version+1 where version=?
    再根据返回的修改条数的数据判断是否修改成功。
    或者就是你这种,联合主键 + version 确定单条记录,更新执行完毕后根据返回的更新条数判断是否更新成功。
    fantastM
        18
    fantastM  
       2019-06-03 10:52:22 +08:00
    不知道你的程序逻辑里,具体需要执行哪些 SQL......粗略看下来,似乎是「幻读」的问题。你可以对着 MySQL 的文档看看,如果是「幻读」的话,那可以用 MySQL 的隔离级别来解决。当然也可以用分布式锁解决

    参考资料:
    https://dev.mysql.com/doc/refman/5.7/en/innodb-next-key-locking.html
    https://dev.mysql.com/doc/refman/5.7/en/innodb-transaction-isolation-levels.html
    https://www.cnblogs.com/zhoujinyi/p/3437475.html
    sleepiforest
        19
    sleepiforest  
       2019-06-03 10:54:00 +08:00
    需求是插入的时候,object_name 和 operation 相同的情况下,在原有最大的基础上+1,再插一条么……为啥要干这种事情。
    mysql 的话直接用单机事务吧……
    csys
        20
    csys  
       2019-06-03 11:19:02 +08:00 via Android   ❤️ 2
    低并发非分布式: 内存锁
    低并发分布式: 数据库行锁 /乐观锁 /分布式锁
    高并发非分布式: eventsourcing(queue+batch commit)
    高并发分布式: actor+eventsourcing
    NullErro
        21
    NullErro  
       2019-06-03 11:19:05 +08:00
    这种情况一般不都是在代码端处理的吗?最直接暴力的就是在后端更新 version 值的时候加锁
    gz911122
        22
    gz911122  
       2019-06-03 11:21:25 +08:00   ❤️ 1
    行锁就可以

    select for update 即可
    javlib
        23
    javlib  
       2019-06-03 12:05:05 +08:00
    我也有同样的问题,我的要求低一些,不要求严格递增,只要能自增就行。

    如果用 redis 做分布式锁,感觉太麻烦,而且降低了写入性能,不知道大佬门有没有简单的只用 sql 的方法。

    我目前的做法是用乐观锁,给每个集合加了一个单独的行,比如(小明,吃饭,version ),这一行的 version 专门用来做小明吃饭的自增,每次自增用乐观锁,每次插入新的数据,先更新这一行的 version,如果更新成功,就用新的 version 作为插入的 version,如果失败,则退回重试。
    w7938940
        24
    w7938940  
       2019-06-03 12:21:05 +08:00
    在这块业务的代码加个锁,保证多个进程不会同时执行这块代码,锁可以用 redis 实现
    chmaple
        25
    chmaple  
       2019-06-03 12:53:58 +08:00
    @chaleaochexist
    两个都读到 2,然后各自+1,执行 update set ...version = where id= and version=到数据库的时候,只有一条能执行成功,另一条 update 的返回 int 是 0,没有匹配的记录。
    数据库 MySQL 默认级别是 repeatableRead。
    就是要做好事务回滚的准备。
    chmaple
        26
    chmaple  
       2019-06-03 12:57:31 +08:00   ❤️ 1
    @javlib 用 redis 还快一点,乐观锁重试失败不如竞争 redis 锁的性能吧
    其实也要看业务的复杂程度,如果本身失败的可能性不高,冲突的几率很小的话,乐观锁也够了,实现起来还简单
    但是如果并发的概率比较大,业务的要求还比较高的话,redis 分布式锁更好些。
    leon0903
        27
    leon0903  
       2019-06-03 14:37:07 +08:00
    我有一个功能和你这个差不多, 我就是用的唯一索引, 每次更新前 先查出当前的最大 version,然后加 1,插入到数据库。 我们对并发的要求比较低,插入失败的时候直接返回错误了,可以重新执行一次就可以了。
    conn4575
        28
    conn4575  
       2019-06-03 21:55:11 +08:00 via Android
    你这种情况要在 mysql 上处理要使用最高级别的串行化事务级别,很容易出现死锁,最好直接用 redis 锁来做
    pisc
        29
    pisc  
       2019-06-03 22:49:36 +08:00
    v2 上对数据库的理解都这么次么。。。一个 select for update 就能解决的事情,扯这么多有的没的真是误导人
    wejaylyn
        30
    wejaylyn  
       2019-06-06 16:14:27 +08:00
    unique index + select for update
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   906 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 20:07 · PVG 04:07 · LAX 12:07 · JFK 15:07
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.