我的数据库设计的是如下格式 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
yjhatfdu2 285 天前
开事务
|
2
coderxy 285 天前 2
用一个自增操作不就完事了?
|
3
themostlazyman 285 天前
更新的时候:获取版本号,然后条件加版本号
|
4
yjhatfdu2 285 天前
如果不是必须先获取当前 version 做业务操作然后再更新,那么直接 update version set version=version+1 where xxxx 就可以了,如果必须先获取 version 再做一些业务操作再更新,就得开事务了,然后高并发的情况还得考虑事务失败概率高的问题,可以根据你的数据库实际情况考虑使用显式锁来减少事务冲突导致的失败
|
5
themostlazyman 285 天前
@themostlazyman 上锁的话,并发少行锁,高的话可以用 reids 上锁。
|
7
shinelamla 285 天前
想了一下开不开事务都没什么用,除非你开的隔离级别是已提交读。
应该用乐观锁,既然你是”先获取(userid,business_type)的最大版本号“,那你在更新或者插入之前就知道了最大版本号了,在更新操作的时候,update set total_version = total_version + 1 where userid =xxx and business_type=xxx and total_version = 你刚才获取的版本号 |
8
avadakur OP @themostlazyman 更新的时候是没问题的 可以用 CAS 来控制,但是我现在在 Insert 的时候不知道要怎么控制
|
9
shinelamla 285 天前
@yjhatfdu2 请问开事务有什么作用吗?多个事务之间,该覆盖还不是一样会覆盖吗?
|
10
avadakur OP @shinelamla 在高并发插入的时候怎么处理比较好呢
|
11
avadakur OP @shinelamla 在高并发下两条 insert 语句同时获得了上一个最新的版本号,这样会导致版本号丢失一次自增
|
12
yjhatfdu2 285 天前
@shinelamla 这个是个典型的不可重复读问题,在 RR 的隔离等级下,这种情况不被允许,一般来说,后一个提交的事务会失败,以避免数据不一致
|
13
vikaptain 285 天前 1
我怎么感觉像是个 XY 问题。要不你说说需求,看你这个数据库设计有点别扭
|
14
sunjiayao 285 天前
userid business_type total_version 设置唯一索引 用 insert on update
|
15
yjhatfdu2 285 天前
如果是 pg 的话,可以考虑用 advisory_lock,读之前针对 user_id 的值加锁,更新完解锁,这样不会对表或者行加高级的锁,避免影响其他业务,也可以避免引入 redis 带来的通讯开销,应该是性能非常高的方案了
|
16
securityCoding 285 天前
加分布式锁最安全
|
17
yjhatfdu2 285 天前
@shinelamla 你这样的问题也是高并发下,失败概率会很高
|
18
shinelamla 285 天前
@avadakur 其实有办法处理的,你这种场景。
1. 考虑你的版本号就不要使用需要自己处理自增的形式,换成毫秒甚至纳秒时间戳,请求必然有个先来后到的 2. 考虑使用事务进行两次插入,先插入一次获取自增 id ,再结合自增 id 更新版本号 3. 最简单,就还是前面几楼提到的:用一个自增操作不就完事了? |
19
justfindu 285 天前
一定要加 1 吗, 每个用户增量唯一的话, 是否可以使用 自增的 id 来作为最后的版本 number
|
20
themostlazyman 285 天前
@themostlazyman 细化下,假设是 mysql 的 innodb 引擎。插入场景:1.userid,business_type 建立联合索引,数据库上锁 userid,business_type 锁范围
|
21
themostlazyman 285 天前
@themostlazyman 2.redis 设置业务键+:userid+:business_type 为键来上锁。先上锁再读版本。
|
22
avadakur OP |
23
markgor 285 天前
`
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)) ` 沒試過高並發下效果,可以自己試試 |
24
avadakur OP @themostlazyman 上锁可以解决所有并发问题,但是现在可能会对性能有影响,我一开始的方案也是上锁
|
25
markgor 285 天前
另外換一個角度,為什麼不使用纳秒呢?
比如業務場景下,根據數據產生時間生成纳秒級別記錄,此時沒插入,丟入隊列; 然後根據數據庫承受能力調控出列寫入速度; 查詢的場景根據排序自己加上版本號,通過數據產生時的納秒當作排序; 這樣會不會更好解決? |
26
zhengwenk 285 天前
虚心求教 同一个 userid,business_type 在什么场景下有高并发
|
27
shinelamla 285 天前
@markgor 正是我前面说到的第一点,所见略同
|
28
markgor 285 天前
@shinelamla 我在用毫秒,就是數據處理不過來,丟隊列裡面,處理完后根據接收時候的毫秒入庫。
業務大概 20QPS 以內,毫秒級別暫時沒出現過重複。 |
29
edward1987 285 天前
userid & total_version 设置成组合 uniq key,插入失败就重新插入就行了吧
|
30
avadakur OP @shinelamla
@markgor 这是一个好方法 我会尝试从此方面入手,因为有需求是根据某个版本号,返回全部的 config_id ,在查询时我要先将 userid,business_type 排序,获取对应的入参版本号的记录,获取时间戳,返回该小于该时间戳的所有 config_id |
32
themostlazyman 285 天前
@avadakur 插入时用 redis 上锁对数据库性能没影响。你这个插入时的版本号为啥不是 1 ,不太理解插入为啥最大版本号,不建议把业务表当日志表。
|
33
avadakur OP @themostlazyman 插入的版本号是用户的总版本号,比如用户插入了配置 A ,此时 version=1 ,用户再次插入配置 B ,此时 version=2 ,版本号记录了用户的所有操作记录
|
34
yuyuf 285 天前
你这是针对每个用户 id 的共享资源,再高并发,每个用户能同时有多少操作。更新用乐观锁,插入用悲观锁
|
35
yuanwenpu00 285 天前
数据库记录锁。应该能解决。代码里的锁不保险。
|
36
9fan 285 天前
insert into article_views(url, views)
values (#{url}, #{views}) on duplicate key update views = views + #{views} 类似这种吗 |
37
dog82 285 天前
最简单的就是不要先查,而是在改的语句里查一下,不过这依然会出问题……
可以用队列和分布式锁,不过比较麻烦。 ----------- 其实楼主的问题根本不是问题,userid,business_type 做联合主键,天然唯一,根本不需要版本号 |
38
aminaSucci 285 天前
我昨天也遇到了这个问题,我是把这张表的自增 id 赋值给 total_version ,因为我对 total_version 没有别的要求,只要 userid | business_type 确定时,total_version 递增不重复就行。
|
39
AceGo 285 天前
如果不改变表结构,insert 貌似只能加锁
先查询有没有记录,有则使用 version=version+1 更新;否则加锁分布式锁 redis 等,再查询记录,为空则 insert ,最后释放锁。使用行锁间隙锁记录锁等都不能解决多次 insert 的问题。 |
40
MoYi123 285 天前
直接用 mysql 的事务 id 怎么样?
|
41
litguy 284 天前
我们过去遇到类似的问题是这样处理的,仅供参考:
定义队列,所有插入放入队列,并不直接操作数据库 一个工作线程从队列取出来请求,插入数据库成功后把回调的 task 放到回调队列 回调队列工作线程执行回调 这样就不存在并发操作数据库的问题了 |
42
0X00FFFF 284 天前
触发器
|
43
heiya 284 天前
并发要求多少?这种共享变量的 读取-新增 操作不上锁都会有并发问题吧?可以试试把锁的粒度控制在最小,试试 select version from table where userid = ${} and business_type = ${} order by version limit 1 for update; where 条件字段要索引。
|
44
vczyh 284 天前
我竟然没理解题目意思...
|
45
thevita 284 天前
加个 current_version(user_id, business_type, current_version) 表,锁这个表来实现
inc current_version, 和 insert 放一个事务里,也不用去 max(version) 了-- 就是不知道业务有不有其他要求 -- 当然本质上跟 分布式锁,或者 redis 锁是一样的,开场景吧,我觉得大部分用不着分布式锁 |
47
wu00 284 天前
[quote]同一个用户账号在多端同时操作这个业务的配置信息[quote]
你这业务的粒度在 userid ,这怎么会有高并发呢,不要把并发和高并发一概而论哦。 你这个场景直接用乐观锁就行了,并发场景下只能成功一个,各大数据库应该都有 row version 的字段类型。 |
48
litguy 284 天前
@heiya 你需要多高的插入性能,每秒 1W 够不够 ?如果可以接受,那就 OK 。回调是为了在别的线程处理剩下的事情,否则你需要的队列工作线程处理剩下的事情,会占据这个线程处理数据库写入的性能,放到队列,由回调处理线程处理就不存在这个问题了。
|
49
cnhongwei 284 天前
userid 和 business_type 做唯一键,使用 INSERT INTO ON DUPLICATE KEY UPDATE 语句就行了。
|