乐观锁重试3次最稳妥,超3次大概率非并发问题;必须检查ROW_COUNT()或getUpdateCount()判断是否更新成功;重试需加随机抖动;version宜用INT而非TIMESTAMP或UUID。
乐观锁重试次数设为 3 次最稳妥
超过 3 次重试,大概率不是并发问题,而是业务逻辑卡点或数据竞争设计不合理。盲目加到 5 次、10 次,只会掩盖真实瓶颈,还可能把瞬时冲突拖成超时异常。
失败后必须检查
或影响行数
很多同学只捕获 SQL 异常(比如
),但乐观锁更新失败通常不抛异常——它安静地返回 0 行影响。不检查这个值,就等于没做乐观锁。
MySQL 用
判断是否更新成功
PostgreSQL 用
或客户端获取
JDBC 中务必调用
,别只看
是否抛异常
重试间隔不能固定,要加随机抖动(jitter)
所有线程在失败后立刻重试,会形成“重试风暴”,反而加剧冲突。比如 100 个请求同时读到
,全部失败后立刻查最新
并再试——结果又撞在一起。
推荐策略:
避免使用
这种固定值
如果用 Spring Retry,配置
,别用
version 列类型选
而非
或
做 version 看似能兼做更新时间,但会导致两个问题:一是高并发下精度不够(MySQL
最小粒度是秒,
才微秒,但 ORM 不一定自动带精度);二是无法检测「无实质变更的重复提交」——两次更新内容一样,但时间戳变了,version 也变,锁就失效了。
足够支撑千万级更新(最大值 42 亿)
别用
或哈希值模拟 version,那不是乐观锁,是乱序校验
确保 ORM 正确映射该字段为乐观锁控制列(如 MyBatis-Plus 的
,Hibernate 的
)
真正难的不是设几次重试,而是分清:这次更新失败,到底是两个人真在改同一行,还是因为上游查了旧数据、中间被别人改过、或者自己逻辑漏刷新 version —— 这些场景,重试次数再合理也救不回来。
UPDATE ... SET version = version + 1ROW_COUNT()SQLExceptionROW_COUNT()RETURNING *updateCountPreparedStatement.getUpdateCount()executeUpdate()version = 5versionThread.sleep((long) (50 + Math.random() * 100))Thread.sleep(100)RandomBackOffPolicyFixedBackOffPolicyINTBIGINTTIMESTAMPTIMESTAMPdatetimedatetime(6)INT UNSIGNEDUUID@Version@Version