线程并行处理导致数据错乱-Lost Update 根因分析
前段时间世界杯活动期间出线了一些问题现在有时间好好整理下用户使用积分投注比赛然后每天定时任务执行如果投注成功则发放一定的积分奖励因为一天可能有多场比赛每天定时任务扫描所有比赛然后每场比赛晒出所有压中用户发放奖励。每场比赛都是一个线程并行处理。然后出现了问题某个用户反映压中了多场比赛但是只发放了一场比赛。于是排查问题发现压根没有调用积分发放服务所以问题出现在扫描比赛和处理用户压中信息的时候。进一步分析代码 for (GuessBallMatchSummary summary : pendingMatches) {FutureVoid future threadPoolFactory.submit(ThreadPoolFactory.WORLDCUP_SETTLE, () - {try {处理某场比赛逻辑return null;} catch (Exception e) {log.error([定时任务] 结算比赛异常, matchId{}, summary.getMatchId(), e);return null;}});futures.add(future);}发现是并行导致的问题因为多个线程并发修改同一个用户的积分余额产生了丢失更新Lost Update问题。场景还原同一用户在多场比赛中均猜中多场比赛**并行结算**并发调用积分服务。 时间线 → Thread-1 (比赛A结算) Thread-2 (比赛B结算) ═══════════════ ═══════════════ addPointsForWinUser( addPointsForWinUser( userId 10001, userId 10001, bizId bet_101, bizId bet_102, points 50 points 30 ) ) │ │ ▼ ▼ 调用 batchChangePoints 调用 batchChangePoints (同一用户、同一积分) (同一用户、同一积分)本质是「读-改-写」三步操作 sql -- 步骤1: 读取当前积分 SELECT points FROM t_stamp_cards WHERE user_id 10001 AND stamp_cards_id 1; -- 步骤2: 内存中计算新余额 newBalance currentBalance request.points; -- 步骤3: 写回新余额 UPDATE t_stamp_cards SET points newBalance WHERE user_id 10001 AND stamp_cards_id 1; 这三步不是原子操作并发执行时产生竞态 Thread-1 (50) Thread-2 (30) DB 中实际值 ───────────── ───────────── ──────────── 100 SELECT → 读到 100 SELECT → 读到 100 100 newBalance 100 50 150 newBalance 100 30 130 UPDATE SET points 150 150 UPDATE SET points 130 130 ← 覆盖!!! 期望结果: 100 50 30 180 实际结果: 130 Thread-1 的 50 被 Thread-2 静默覆盖积分丢失解决方案找到问题后分析处理数据有多少每天最多六场比赛几十万的数据还好改成串行处理