V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
avadakur
V2EX  ›  程序员

关于数据库高并发插入的版本号问题

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

    我的数据库设计的是如下格式 userid | business_type | config_id | total_version

    (userid,business_type) 这两个的组合会控制 total_version 版本号字段,每次在对(userid,business_type)更新或者插入时,都会让 total_version 自增然后插入或者更新。 问题: 在高并发插入的情况下,我先获取(userid,business_type)的最大版本号,然后执行插入的这个过程,会导致多个插入记录同时都获取的 totoal_version= 1 然后两个都会执行 version + 1 = 2,导致用户版本号丢失一次更新记录。这个问题要怎么解决呢?

    第 1 条附言  ·  46 天前
    最终解决方案:使用 Redis 中存储 userid,business_type 的版本号,通过 Redis 单线程来获取最新版本号,冗余一张用户业务版本号表。
    49 条回复    2024-03-13 09:14:48 +08:00
    yjhatfdu2
        1
    yjhatfdu2  
       46 天前
    开事务
    coderxy
        2
    coderxy  
       46 天前   ❤️ 2
    用一个自增操作不就完事了?
    themostlazyman
        3
    themostlazyman  
       46 天前
    更新的时候:获取版本号,然后条件加版本号
    yjhatfdu2
        4
    yjhatfdu2  
       46 天前
    如果不是必须先获取当前 version 做业务操作然后再更新,那么直接 update version set version=version+1 where xxxx 就可以了,如果必须先获取 version 再做一些业务操作再更新,就得开事务了,然后高并发的情况还得考虑事务失败概率高的问题,可以根据你的数据库实际情况考虑使用显式锁来减少事务冲突导致的失败
    themostlazyman
        5
    themostlazyman  
       46 天前
    @themostlazyman 上锁的话,并发少行锁,高的话可以用 reids 上锁。
    avadakur
        6
    avadakur  
    OP
       46 天前
    @coderxy 数据库里的每个用户和业务类型 都有一份自己的总版本号 并不是这个表的 total_version 是一个自增的
    shinelamla
        7
    shinelamla  
       46 天前
    想了一下开不开事务都没什么用,除非你开的隔离级别是已提交读。

    应该用乐观锁,既然你是”先获取(userid,business_type)的最大版本号“,那你在更新或者插入之前就知道了最大版本号了,在更新操作的时候,update set total_version = total_version + 1 where userid =xxx and business_type=xxx and total_version = 你刚才获取的版本号
    avadakur
        8
    avadakur  
    OP
       46 天前
    @themostlazyman 更新的时候是没问题的 可以用 CAS 来控制,但是我现在在 Insert 的时候不知道要怎么控制
    shinelamla
        9
    shinelamla  
       46 天前
    @yjhatfdu2 请问开事务有什么作用吗?多个事务之间,该覆盖还不是一样会覆盖吗?
    avadakur
        10
    avadakur  
    OP
       46 天前
    @shinelamla 在高并发插入的时候怎么处理比较好呢
    avadakur
        11
    avadakur  
    OP
       46 天前
    @shinelamla 在高并发下两条 insert 语句同时获得了上一个最新的版本号,这样会导致版本号丢失一次自增
    yjhatfdu2
        12
    yjhatfdu2  
       46 天前
    @shinelamla 这个是个典型的不可重复读问题,在 RR 的隔离等级下,这种情况不被允许,一般来说,后一个提交的事务会失败,以避免数据不一致
    vikaptain
        13
    vikaptain  
       46 天前   ❤️ 1
    我怎么感觉像是个 XY 问题。要不你说说需求,看你这个数据库设计有点别扭
    sunjiayao
        14
    sunjiayao  
       46 天前
    userid business_type total_version 设置唯一索引 用 insert on update
    yjhatfdu2
        15
    yjhatfdu2  
       46 天前
    如果是 pg 的话,可以考虑用 advisory_lock,读之前针对 user_id 的值加锁,更新完解锁,这样不会对表或者行加高级的锁,避免影响其他业务,也可以避免引入 redis 带来的通讯开销,应该是性能非常高的方案了
    securityCoding
        16
    securityCoding  
       46 天前
    加分布式锁最安全
    yjhatfdu2
        17
    yjhatfdu2  
       46 天前
    @shinelamla 你这样的问题也是高并发下,失败概率会很高
    shinelamla
        18
    shinelamla  
       46 天前
    @avadakur 其实有办法处理的,你这种场景。
    1. 考虑你的版本号就不要使用需要自己处理自增的形式,换成毫秒甚至纳秒时间戳,请求必然有个先来后到的
    2. 考虑使用事务进行两次插入,先插入一次获取自增 id ,再结合自增 id 更新版本号
    3. 最简单,就还是前面几楼提到的:用一个自增操作不就完事了?
    justfindu
        19
    justfindu  
       46 天前
    一定要加 1 吗, 每个用户增量唯一的话, 是否可以使用 自增的 id 来作为最后的版本 number
    themostlazyman
        20
    themostlazyman  
       46 天前
    @themostlazyman 细化下,假设是 mysql 的 innodb 引擎。插入场景:1.userid,business_type 建立联合索引,数据库上锁 userid,business_type 锁范围
    themostlazyman
        21
    themostlazyman  
       46 天前
    @themostlazyman 2.redis 设置业务键+:userid+:business_type 为键来上锁。先上锁再读版本。
    avadakur
        22
    avadakur  
    OP
       46 天前
    @justfindu
    @shinelamla
    每个用户的不同 business_type 都是重新计算版本号的,不用业务有不同版本号,自增操作要通过触发器来更新,触发器现在公司尽量不能使用
    markgor
        23
    markgor  
       46 天前
    `
    INSERT INTO test_tbl (userid , business_type , config_id , total_version) VALUES (1,1,1,(SELECT maxVersion FROM (select IFNULL(max(total_version),0)+1 as maxVersion from test_tbl where userid =1 and business_type =1) as b))
    `

    沒試過高並發下效果,可以自己試試
    avadakur
        24
    avadakur  
    OP
       46 天前
    @themostlazyman 上锁可以解决所有并发问题,但是现在可能会对性能有影响,我一开始的方案也是上锁
    markgor
        25
    markgor  
       46 天前
    另外換一個角度,為什麼不使用纳秒呢?
    比如業務場景下,根據數據產生時間生成纳秒級別記錄,此時沒插入,丟入隊列;
    然後根據數據庫承受能力調控出列寫入速度;

    查詢的場景根據排序自己加上版本號,通過數據產生時的納秒當作排序;

    這樣會不會更好解決?
    zhengwenk
        26
    zhengwenk  
       46 天前
    虚心求教 同一个 userid,business_type 在什么场景下有高并发
    shinelamla
        27
    shinelamla  
       46 天前
    @markgor 正是我前面说到的第一点,所见略同
    markgor
        28
    markgor  
       46 天前
    @shinelamla 我在用毫秒,就是數據處理不過來,丟隊列裡面,處理完后根據接收時候的毫秒入庫。
    業務大概 20QPS 以內,毫秒級別暫時沒出現過重複。
    edward1987
        29
    edward1987  
       46 天前
    userid & total_version 设置成组合 uniq key,插入失败就重新插入就行了吧
    avadakur
        30
    avadakur  
    OP
       46 天前
    @shinelamla
    @markgor 这是一个好方法 我会尝试从此方面入手,因为有需求是根据某个版本号,返回全部的 config_id ,在查询时我要先将 userid,business_type 排序,获取对应的入参版本号的记录,获取时间戳,返回该小于该时间戳的所有 config_id
    avadakur
        31
    avadakur  
    OP
       46 天前
    @zhengwenk 同一个用户账号在多端同时操作这个业务的配置信息
    themostlazyman
        32
    themostlazyman  
       46 天前
    @avadakur 插入时用 redis 上锁对数据库性能没影响。你这个插入时的版本号为啥不是 1 ,不太理解插入为啥最大版本号,不建议把业务表当日志表。
    avadakur
        33
    avadakur  
    OP
       46 天前
    @themostlazyman 插入的版本号是用户的总版本号,比如用户插入了配置 A ,此时 version=1 ,用户再次插入配置 B ,此时 version=2 ,版本号记录了用户的所有操作记录
    yuyuf
        34
    yuyuf  
       46 天前
    你这是针对每个用户 id 的共享资源,再高并发,每个用户能同时有多少操作。更新用乐观锁,插入用悲观锁
    yuanwenpu00
        35
    yuanwenpu00  
       46 天前
    数据库记录锁。应该能解决。代码里的锁不保险。
    9fan
        36
    9fan  
       46 天前
    insert into article_views(url, views)
    values (#{url}, #{views})
    on duplicate key update views = views + #{views} 类似这种吗
    dog82
        37
    dog82  
       46 天前
    最简单的就是不要先查,而是在改的语句里查一下,不过这依然会出问题……
    可以用队列和分布式锁,不过比较麻烦。
    -----------
    其实楼主的问题根本不是问题,userid,business_type 做联合主键,天然唯一,根本不需要版本号
    aminaSucci
        38
    aminaSucci  
       46 天前
    我昨天也遇到了这个问题,我是把这张表的自增 id 赋值给 total_version ,因为我对 total_version 没有别的要求,只要 userid | business_type 确定时,total_version 递增不重复就行。
    AceGo
        39
    AceGo  
       46 天前
    如果不改变表结构,insert 貌似只能加锁
    先查询有没有记录,有则使用 version=version+1 更新;否则加锁分布式锁 redis 等,再查询记录,为空则 insert ,最后释放锁。使用行锁间隙锁记录锁等都不能解决多次 insert 的问题。
    MoYi123
        40
    MoYi123  
       46 天前
    直接用 mysql 的事务 id 怎么样?
    litguy
        41
    litguy  
       46 天前
    我们过去遇到类似的问题是这样处理的,仅供参考:
    定义队列,所有插入放入队列,并不直接操作数据库
    一个工作线程从队列取出来请求,插入数据库成功后把回调的 task 放到回调队列
    回调队列工作线程执行回调
    这样就不存在并发操作数据库的问题了
    0X00FFFF
        42
    0X00FFFF  
       46 天前
    触发器
    heiya
        43
    heiya  
       46 天前
    并发要求多少?这种共享变量的 读取-新增 操作不上锁都会有并发问题吧?可以试试把锁的粒度控制在最小,试试 select version from table where userid = ${} and business_type = ${} order by version limit 1 for update; where 条件字段要索引。
    vczyh
        44
    vczyh  
       46 天前
    我竟然没理解题目意思...
    thevita
        45
    thevita  
       46 天前
    加个 current_version(user_id, business_type, current_version) 表,锁这个表来实现

    inc current_version, 和 insert 放一个事务里,也不用去 max(version) 了-- 就是不知道业务有不有其他要求

    -- 当然本质上跟 分布式锁,或者 redis 锁是一样的,开场景吧,我觉得大部分用不着分布式锁
    heiya
        46
    heiya  
       46 天前
    @litguy 这个我感觉是相当于把所有的并发操作限制成了串行操作,是可以保证线程安全的,那效率怎么样呢?还有就是回调队列是干了什么事呢?
    wu00
        47
    wu00  
       46 天前
    [quote]同一个用户账号在多端同时操作这个业务的配置信息[quote]
    你这业务的粒度在 userid ,这怎么会有高并发呢,不要把并发和高并发一概而论哦。
    你这个场景直接用乐观锁就行了,并发场景下只能成功一个,各大数据库应该都有 row version 的字段类型。
    litguy
        48
    litguy  
       46 天前
    @heiya 你需要多高的插入性能,每秒 1W 够不够 ?如果可以接受,那就 OK 。回调是为了在别的线程处理剩下的事情,否则你需要的队列工作线程处理剩下的事情,会占据这个线程处理数据库写入的性能,放到队列,由回调处理线程处理就不存在这个问题了。
    cnhongwei
        49
    cnhongwei  
       45 天前
    userid 和 business_type 做唯一键,使用 INSERT INTO ON DUPLICATE KEY UPDATE 语句就行了。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   1020 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 48ms · UTC 18:49 · PVG 02:49 · LAX 11:49 · JFK 14:49
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.