微恐连续剧
67.38M · 2026-02-05
本文分为上下两篇,这是上篇。
上篇内容:
下篇内容: 6. 核心代码实现 7. 完整交易流程 8. 并发控制机制 9. 部署与运维 10. 总结与展望
想象一下这样的场景:
小明在某电商平台有10000元余额。双十一那天,他同时参与了3个秒杀活动,3个订单几乎同时发出扣款请求。结果会怎样?
订单A:扣款100元 成功
订单B:扣款200元 失败(提示余额不足)
订单C:扣款50元 失败(提示余额不足)
小明的明明有10000元,为什么只能成功一笔?这就是并发扣款的典型问题。
在传统的钱包系统设计中,所有用户的余额都存储在一个账户里。当多个请求同时到来时,如果没有正确的并发控制,就会出现:
冷热钱包架构正是为了解决这个问题而诞生的。接下来,让我们一起深入了解这个方案的来龙去脉。
"冷热钱包"这个概念,最早来自于比特币和区块链行业。
在加密货币世界里,人们发现了一个两难问题:
┌─────────────────────────────────────────────────────────────┐
│ 两难选择 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 选项A:资金全部放线上钱包 │
│ ├─ 优点:交易方便、响应快速 │
│ └─ 缺点:容易被黑客攻击,一旦被盗,血本无归 │
│ │
│ 选项B:资金全部放离线冷存储 │
│ ├─ 优点:极其安全,黑客无法触达 │
│ └─ 缺点:每次交易都要手动操作,非常麻烦 │
│ │
└─────────────────────────────────────────────────────────────┘
于是,行业里诞生了冷热钱包分离的方案:
这个思路后来被传统金融和支付行业借鉴,演变成了我们今天要讲的冷热钱包分离架构。
冷热钱包的本质,是资金的安全性和流动性之间的权衡。
| 特性 | 冷钱包 | 热钱包 |
|---|---|---|
| 安全性 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| 流动性 | ⭐⭐ | ⭐⭐⭐⭐⭐ |
| 类比 | 银行的金库 | 收银台的现金抽屉 |
| 操作频率 | 低(按天/周) | 高(按秒) |
| 单笔限额 | 无限制 | 有限额 |
想象你开了一家奶茶店:
┌───────────────────────────────────────────────────────────┐
│ 奶茶店的资金管理 │
├───────────────────────────────────────────────────────────┤
│ │
│ 收银台的现金抽屉(热钱包) │
│ ├─ 存放:每天营业所需的零钱、找补现金 │
│ ├─ 特点:随时取用,交易方便 │
│ └─ 策略:每天晚上把钱转走,只留第二天够用的 │
│ │
│ 银行保险箱(冷钱包) │
│ ├─ 存放:店铺的主要利润和储备金 │
│ ├─ 特点:安全,但取用需要手续 │
│ └─ 策略:大部分钱都在这里,需要时再调拨到收银台 │
│ │
└───────────────────────────────────────────────────────────┘
这就是冷热钱包的核心思想:把大部分钱安全地存起来,只留一小部分在"前台"方便日常交易。
某电商平台在双十一大促期间,遇到了严重的扣款问题:
时间:2023年11月11日 00:00
场景:秒杀活动开启,百万用户同时抢购
现象:大量用户反馈"余额不足",但明明账户里有足够的钱
影响:GMV损失约2000万元
技术团队紧急排查,发现了问题所在:
// 传统的扣款逻辑
public void deduct(Long userId, Long amount) {
// 1. 查询用户余额
Account account = accountMapper.selectById(userId);
// 2. 判断余额是否充足
if (account.getBalance() < amount) {
throw new Exception("余额不足");
}
// 3. 扣款
account.setBalance(account.getBalance() - amount);
accountMapper.updateById(account);
}
问题出在哪里?
时间线分析:
T0: 用户余额 = 10000元
T1: 请求A读取余额 → 10000元
T2: 请求B读取余额 → 10000元(此时请求A还未完成扣款)
T3: 请求A扣款200元 → 写入余额9800元
T4: 请求B扣款300元 → 基于旧余额10000元,写入9700元(错误!)
T5: 数据库最终余额 = 9700元(应该是9500元,少扣了300元)
这是一个典型的并发控制问题,专业术语叫"丢失更新"(Lost Update)。
┌─────────────────────────────────────────────────────────────┐
│ 并发问题的本质 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 在高并发场景下,多个线程同时读写同一个数据,会产生: │
│ │
│ 1. 脏读:读到了未提交的数据 │
│ 2. 不可重复读:同一事务内两次读取结果不同 │
│ 3. 幻读:同一查询返回不同结果 │
│ 4. 丢失更新:后提交的事务覆盖前面的事务 │
│ │
│ 我们遇到的是第4种:丢失更新! │
│ │
└─────────────────────────────────────────────────────────────┘
在冷热钱包架构出现之前,业界常用的并发控制方案有:
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 数据库锁 | SELECT ... FOR UPDATE | 简单直接 | 性能差,死锁风险 |
| 悲观锁 | 加锁后再操作 | 数据绝对一致 | 并发度低,系统吞吐差 |
| 分布式锁 | Redis/Zookeeper锁 | 跨JVM有效 | 依赖外部组件,增加复杂度 |
这些方案都存在一个共同问题:为了保证数据一致性,牺牲了系统性能。
冷热钱包架构的核心思想是:用空间换时间,用架构换性能。
┌─────────────────────────────────────────────────────────────┐
│ 冷热钱包架构的智慧 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 传统方案: │
│ ┌─────────┐ │
│ │ 账户余额 │ ← 所有交易都竞争这唯一的一把锁 │
│ │ 10000元 │ │
│ └─────────┘ │
│ │
│ 冷热钱包方案: │
│ ┌──────────┐ ┌──────────┐ │
│ │冷钱包 │ │热钱包 │ ← 把交易分散到两个"账户" │
│ │9500元 │→ │500元 │ 热钱包交易无需加锁 │
│ └──────────┘ └──────────┘ │
│ │
│ 核心优势: │
│ 大部分交易只在热钱包操作,无需加锁 │
│ 冷热调拨可以异步进行 │
│ 并发能力提升10倍以上 │
│ │
└─────────────────────────────────────────────────────────────┘
关键在于:把"竞争"变成了"协作"
传统方案的竞争模型:
┌─────────────────────────────────────────────────────────────┐
│ │
│ 请求A ─┐ │
│ 请求B ─┼─→ [争抢同一把锁] → 排队执行 → 性能瓶颈 │
│ 请求C ─┘ │
│ │
└─────────────────────────────────────────────────────────────┘
冷热钱包的协作模型:
┌─────────────────────────────────────────────────────────────┐
│ │
│ 请求A ──→ [热钱包操作] ──→ 无需等待,直接完成 │
│ │
│ 请求B ──→ [热钱包操作] ──→ 无需等待,直接完成 │
│ │
│ 请求C ──→ [热钱包不足] ──→ [异步调拨] ──→ 完成 │
│ │
│ 所有请求都能并发执行,互不阻塞! │
│ │
└─────────────────────────────────────────────────────────────┘
系统采用经典的分层架构:
┌──────────────────────────────────────────────────────────────┐
│ 客户端层 │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Web前端 │ │ 移动App │ │ 小程序 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└──────────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────┐
│ API网关层 │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 负载均衡 │ │ 限流控制 │ │ 路由转发 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└──────────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────┐
│ 应用服务层 │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 账户服务 │ │ 交易服务 │ │ 调拨服务 │ │
│ ├─ 创建账户 │ ├─ 扣款/充值 │ ├─ 冷转热 │ │
│ ├─ 查询余额 │ ├─ 退款 │ ├─ 热转冷 │ │
│ └─ 账户状态 │ └─ 交易记录 │ └─ 自动补充 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└──────────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────┐
│ 中间件层 │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Redis │ │ MQ消息队列 │ │ 分布式锁 │ │
│ │ ├─ 缓存 │ │ ├─ 异步通知 │ │ ├─ 并发控制 │ │
│ │ └─ 分布式锁 │ │ └─ 削峰填谷 │ │ └─ 防重入 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└──────────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────────┐
│ 数据层 │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ MySQL主库 │ │ MySQL从库 │ │ 数据备份 │ │
│ │ ├─ 用户账户 │ │ ├─ 读写分离 │ │ ├─ 定时备份 │ │
│ │ ├─ 交易记录 │ │ └─ 查询分流 │ │ └─ 容灾恢复 │ │
│ │ └─ 调拨记录 │ │ │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└──────────────────────────────────────────────────────────────┘
-- 用户账户表
CREATE TABLE user_account (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL UNIQUE COMMENT '用户ID',
username VARCHAR(50) NOT NULL COMMENT '用户名',
-- 核心字段:冷热钱包分离
cold_balance BIGINT NOT NULL DEFAULT 0 COMMENT '冷钱包余额(单位:分)',
hot_balance BIGINT NOT NULL DEFAULT 0 COMMENT '热钱包余额(单位:分)',
-- 并发控制
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
-- 状态管理
status TINYINT NOT NULL DEFAULT 1 COMMENT '账户状态:1-正常 2-冻结 3-注销',
-- 审计字段
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除标记',
INDEX idx_user_id (user_id),
INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户账户表';
-- 交易记录表
CREATE TABLE transaction_record (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
transaction_no VARCHAR(32) NOT NULL UNIQUE COMMENT '交易流水号',
user_id BIGINT NOT NULL COMMENT '用户ID',
-- 交易信息
transaction_type TINYINT NOT NULL COMMENT '交易类型:1-充值 2-消费 3-退款',
amount BIGINT NOT NULL COMMENT '交易金额(单位:分)',
business_no VARCHAR(64) COMMENT '业务单号',
-- 余额快照
before_cold_balance BIGINT DEFAULT 0 COMMENT '交易前冷钱包余额',
after_cold_balance BIGINT DEFAULT 0 COMMENT '交易后冷钱包余额',
before_hot_balance BIGINT DEFAULT 0 COMMENT '交易前热钱包余额',
after_hot_balance BIGINT DEFAULT 0 COMMENT '交易后热钱包余额',
-- 状态
status TINYINT NOT NULL COMMENT '交易状态:1-处理中 2-成功 3-失败',
-- 审计
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_user_id (user_id),
INDEX idx_transaction_no (transaction_no),
INDEX idx_create_time (create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='交易记录表';
-- 钱包调拨记录表
CREATE TABLE wallet_transfer (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
transfer_no VARCHAR(32) NOT NULL UNIQUE COMMENT '调拨流水号',
user_id BIGINT NOT NULL COMMENT '用户ID',
-- 调拨信息
transfer_type TINYINT NOT NULL COMMENT '调拨类型:1-冷转热 2-热转冷',
amount BIGINT NOT NULL COMMENT '调拨金额(单位:分)',
-- 余额快照
before_cold_balance BIGINT DEFAULT 0 COMMENT '调拨前冷钱包',
after_cold_balance BIGINT DEFAULT 0 COMMENT '调拨后冷钱包',
before_hot_balance BIGINT DEFAULT 0 COMMENT '调拨前热钱包',
after_hot_balance BIGINT DEFAULT 0 COMMENT '调拨后热钱包',
-- 状态
status TINYINT NOT NULL COMMENT '状态:1-处理中 2-成功 3-失败',
-- 审计
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_user_id (user_id),
INDEX idx_transfer_no (transfer_no)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='钱包调拨记录表';
在上篇中,我们学习了:
下篇预告:
在下篇中,我们将深入代码实现层面,学习:
请继续阅读 《冷热钱包系统设计实战(下)》 !