高并发下的余额扣减,核心目标只有两个:
- 不扣错钱
- 不卡住系统
高并发扣减思路
常见路线:
- 分库分表
- 合并请求,减少锁与写
- 拆分热点账户为多个子账户
- 内存扣减 + 异步落库
每个方案都有代价:要么增加复杂度,要么牺牲一致性。
直接扣减的问题
钱包表:
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 + 重试
- 热点账户要拆分或限流
- 任何方案都要配合幂等与补偿