关于数据库高并发插入的版本号问题 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在回答技术问题时复制粘贴 AI 生成的内容
yevXxHmg
V2EX    程序员

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

  •  
  •   yevXxHmg 2024-03-12 10:26:50 +08:00 3814 次点击
    这是一个创建于 577 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

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

    高下效果,可以自己
    yevXxHmg
        24
    yevXxHmg  
    OP
       2024-03-12 11:07:48 +08:00
    @themostlazyman 上锁可以解决所有并发问题,但是现在可能会对性能有影响,我一开始的方案也是上锁
    markgor
        25
    markgor  
       2024-03-12 11:14:32 +08:00
    另外一角度,什不使用纳秒呢?
    比如景下,根生生成纳秒,此插入,入列;
    然後根承受能力控出列入速度;

    查的景根排序自己加上版本,通生的秒作排序;

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

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

    -- 当然本质上跟 分布式锁,或者 redis 锁是一样的,开场景吧,我觉得大部分用不着分布式锁
    heiya
        46
    heiya  
       2024-03-12 17:55:23 +08:00
    @litguy 这个我感觉是相当于把所有的并发操作限制成了串行操作,是可以保证线程安全的,那效率怎么样呢?还有就是回调队列是干了什么事呢?
    wu00
        47
    wu00  
       2024-03-12 18:13:51 +08:00
    [quote]同一个用户账号在多端同时操作这个业务的配置信息[quote]
    你这业务的粒度在 userid ,这怎么会有高并发呢,不要把并发和高并发一概而论哦。
    你这个场景直接用乐观锁就行了,并发场景下只能成功一个,各大数据库应该都有 row version 的字段类型。
    litguy
        48
    litguy  
       2024-03-12 18:57:13 +08:00
    @heiya 你需要多高的插入性能,每秒 1W 够不够 ?如果可以接受,那就 OK 。回调是为了在别的线程处理剩下的事情,否则你需要的队列工作线程处理剩下的事情,会占据这个线程处理数据库写入的性能,放到队列,由回调处理线程处理就不存在这个问题了。
    cnhongwei
        49
    cnhongwei  
       2024-03-13 09:14:48 +08:00
    userid 和 business_type 做唯一键,使用 INSERT INTO ON DUPLICATE KEY UPDATE 语句就行了。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     1242 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 29ms UTC 17:22 PVG 01:22 LAX 10:22 JFK 13:22
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86