V2EX = way to explore
V2EX 是一个关于分享和探索的地方
Sign Up Now
For Existing Member  Sign In
• 请不要在回答技术问题时复制粘贴 AI 生成的内容
chaleaochexist
V2EX  ›  程序员

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

  •  
  •   chaleaochexist · Jun 3, 2019 · 3939 views
    This topic created in 2526 days ago, the information mentioned may be changed or developed.
    不知道这样的问题,如何归纳成主题.

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

    table 大概是这样的

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

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

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

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

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


    update xxx set field=value,version = version + 1 where version = $version;
    }
    night98
        17
    night98  
       Jun 3, 2019
    新增一个唯一主键,按唯一主键 + version 确定单条记录。
    然后执行楼上说的:
    update b set version=version+1 where version=?
    再根据返回的修改条数的数据判断是否修改成功。
    或者就是你这种,联合主键 + version 确定单条记录,更新执行完毕后根据返回的更新条数判断是否更新成功。
    fantastM
        18
    fantastM  
       Jun 3, 2019
    不知道你的程序逻辑里,具体需要执行哪些 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  
       Jun 3, 2019
    需求是插入的时候,object_name 和 operation 相同的情况下,在原有最大的基础上+1,再插一条么……为啥要干这种事情。
    mysql 的话直接用单机事务吧……
    csys
        20
    csys  
       Jun 3, 2019 via Android   ❤️ 2
    低并发非分布式: 内存锁
    低并发分布式: 数据库行锁 /乐观锁 /分布式锁
    高并发非分布式: eventsourcing(queue+batch commit)
    高并发分布式: actor+eventsourcing
    NullErro
        21
    NullErro  
       Jun 3, 2019
    这种情况一般不都是在代码端处理的吗?最直接暴力的就是在后端更新 version 值的时候加锁
    gz911122
        22
    gz911122  
       Jun 3, 2019   ❤️ 1
    行锁就可以

    select for update 即可
    javlib
        23
    javlib  
       Jun 3, 2019
    我也有同样的问题,我的要求低一些,不要求严格递增,只要能自增就行。

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

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