高并发下的余额扣减,核心目标只有两个:

  • 不扣错钱
  • 不卡住系统

高并发扣减思路

常见路线:

  • 分库分表
  • 合并请求,减少锁与写
  • 拆分热点账户为多个子账户
  • 内存扣减 + 异步落库

每个方案都有代价:要么增加复杂度,要么牺牲一致性。

直接扣减的问题

钱包表:

CREATE TABLE `wallet_tab` (
  `wallet_id` bigint(20) unsigned NOT NULL,
  `wallet_type` tinyint(3) unsigned NOT NULL,
  `user_id` bigint(20) unsigned NOT NULL,
  `balance` bigint(20) NOT NULL,
  `create_time` bigint(20) unsigned NOT NULL,
  `update_time` bigint(20) unsigned NOT NULL,
  PRIMARY KEY (`wallet_id`),
  UNIQUE KEY `uniq_user_id_wallet_type` (`user_id`, `wallet_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

直接扣减:

SELECT * FROM wallet_tab WHERE uid=$uid;
UPDATE wallet_tab SET balance=balance-100 WHERE wallet_id=$wallet_id;

在并发下会出现“写覆盖”,余额可能被扣错。

方案一:悲观锁(TCC 思路)

frozen 字段:

CREATE TABLE `wallet_tab` (
  `wallet_id` bigint(20) unsigned NOT NULL,
  `wallet_type` tinyint(3) unsigned NOT NULL,
  `user_id` bigint(20) unsigned NOT NULL,
  `balance` bigint(20) NOT NULL,
  `frozen` bigint(20) NOT NULL,
  `create_time` bigint(20) unsigned NOT NULL,
  `update_time` bigint(20) unsigned NOT NULL,
  PRIMARY KEY (`wallet_id`),
  UNIQUE KEY `uniq_user_id_wallet_type` (`user_id`, `wallet_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

TCC 三步:

-- try
SELECT balance FROM wallet_tab WHERE uid=$uid FOR UPDATE;
UPDATE wallet_tab SET balance=$balance-20, frozen=$frozen+20 WHERE wallet_id=$wallet_id;

-- confirm
SELECT balance FROM wallet_tab WHERE uid=$uid FOR UPDATE;
UPDATE wallet_tab SET frozen=$frozen-20 WHERE wallet_id=$wallet_id;

-- cancel
SELECT balance FROM wallet_tab WHERE uid=$uid FOR UPDATE;
UPDATE wallet_tab SET balance=$balance+20, frozen=$frozen-20 WHERE wallet_id=$wallet_id;

优势:强一致性。代价:锁粒度高,吞吐受限。

方案二:乐观锁(CAS)

把“更新”变成“带条件更新”:

UPDATE wallet_tab
SET balance=$new_balance
WHERE wallet_id=$wallet_id AND balance=$old_balance;

受影响行数为 1 表示成功,为 0 表示失败。

ABA 问题

CAS 只比对值,会有 ABA 问题。

解决方式:引入版本号。

CREATE TABLE `wallet_tab` (
  `wallet_id` bigint(20) unsigned NOT NULL,
  `wallet_type` tinyint(3) unsigned NOT NULL,
  `user_id` bigint(20) unsigned NOT NULL,
  `balance` bigint(20) NOT NULL,
  `create_time` bigint(20) unsigned NOT NULL,
  `update_time` bigint(20) unsigned NOT NULL,
  `version` bigint(20) unsigned NOT NULL,
  PRIMARY KEY (`wallet_id`),
  UNIQUE KEY `uniq_user_id_wallet_type` (`user_id`, `wallet_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

更新时校验版本号:

SELECT balance, version FROM wallet_tab WHERE uid=$uid;

UPDATE wallet_tab
SET balance=38, version=$version_new
WHERE wallet_id=$wallet_id AND version=$version_old;

小结

  • 低并发可用锁
  • 高并发优先 CAS + 重试
  • 热点账户要拆分或限流
  • 任何方案都要配合幂等与补偿