U卡客户入金地址池方案:事件驱动与补偿机制的可靠性设计
U卡客户入金地址池方案:基于链+币种维度的全链路优化实践(Card项目落地)
代码部分仅为简易实现用于提高技术实现细节的理解,具体使用场景需要额外的优化 比如: 分配记录ID需要使用分布式ID生成、 分布式redis锁一般都是通过切面实现、 业务逻辑抽离尽可能封装多态、 事务与分布式锁的配合使用避免事务失效、 超时重试机制、 极端场景的处理考虑······
针对用户地址申请校验、资源高效复用及动态扩容需求,设计以下方案,覆盖地址申请、使用监控、回收全流程。
一、方案背景与核心优化目标
在Card数字货币借记卡系统中,客户入金需通过临时地址完成链上转账,初期存在地址资源滥用(用户重复申请)、回收不及时(闲置地址占用资源)、高并发下地址枯竭等问题。基于"链+币种"维度的优化目标为:
- 同链+币种下,用户仅保留1笔未完成分配记录,杜绝重复申请;
- 地址分配后30分钟未使用自动回收,回收率100%;
- 空闲地址低于阈值时动态扩容,支撑日活20w+用户峰值需求。
二、整体架构设计:三维度协同机制
架构以"链+币种"为核心维度,整合申请校验、延时监控、动态扩容能力,分正向流程(地址申请-使用-入账)、逆向流程(地址回收-状态重置)、补偿流程(异常处理-资源兜底)三部分。
2.1 正向流程:地址申请与交易处理链路
graph TD
subgraph 用户层
A[用户发起入金申请] --> B[指定链+币种(如ETH-USDT)]
end
subgraph 申请校验层
B --> C[维度校验服务]
C --> D[查询用户未完成分配记录(Redis哈希)]
D -->|同维度存在未完成记录| E[返回已有地址]
D -->|同维度无记录| F[回收用户不同维度未使用地址]
F --> G[生成新分配记录]
end
subgraph 地址池管理层
H[按链+币种划分的Redis空闲队列] -->|如address:idle:eth:usdt| I[分配地址]
I --> J[发送30分钟延时MQ(监控使用状态)]
G --> I
end
subgraph 交易处理层
K[TokenView地址监控] -->|推送入账交易| L[交易校验(地址在池内+有效)]
L --> M[区块事件监听(更新确认数)]
M -->|确认数达标| N[TokenView API最终校验]
N --> O[资金入账+标记地址使用完成]
end
2.2 逆向流程:地址回收与状态重置
graph TD
subgraph 延时监控触发
A[30分钟延时MQ到期] --> B[校验地址使用状态]
B -->|未产生交易| C[回收地址至空闲队列]
B -->|已产生交易| D[标记分配记录为'已使用']
end
subgraph 异常回收触发
E[用户取消申请] --> C
F[交易回滚/失败] --> C
end
subgraph 状态同步
C --> G[更新地址池DB状态(空闲)]
C --> H[删除用户未完成分配记录]
D --> I[更新分配记录状态]
end
2.3 补偿流程:动态扩容与漏单处理
graph TD
subgraph 动态扩容机制
A[监控空闲队列长度] -->|低于20%阈值| B[触发扩容任务]
B --> C[多节点并行生成新地址(HD钱包BIP-32)]
C --> D[补充至对应链+币种空闲队列]
end
subgraph 漏单补偿机制
E[定时任务(每小时)] --> F[TokenView批量查询地址交易]
F -->|发现未处理交易| G[补入交易处理链路]
end
三、核心模块实现细节
3.1 地址申请逻辑校验(链+币种维度管控)
基于Redis哈希存储用户未完成分配记录,确保同维度唯一、不同维度及时释放。
@Service
public class AddressApplyService {
// Redis键:用户未完成分配记录(hash结构:key=user:alloc:{userId},field=chain:token,value=allocationId)
private static final String USER_ALLOC_HASH_KEY = "user:alloc:%s";
// 链+币种分隔符
private static final String DIMENSION_SEP = ":";
@Autowired
private RedissonClient redissonClient;
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private AddressPoolService addressPoolService;
@Autowired
private RocketMQTemplate rocketMQTemplate;
/**
* 用户申请地址(核心校验逻辑)
*todo 前端交互上优化:提示用户有未使用的地址,是否取消上笔入金申请
*/
public AddressVO applyAddress(String userId, String chain, String token) {
String dimension = chain + DIMENSION_SEP + token;
String userAllocKey = String.format(USER_ALLOC_HASH_KEY, userId);
// 1. 加分布式锁,防止并发申请导致状态不一致
RLock lock = redissonClient.getLock("lock:apply:" + userId + ":" + dimension);
lock.lock(10, TimeUnit.SECONDS);
try {
// 2. 校验同维度是否有未完成记录
String existingAllocId = redisTemplate.opsForHash().get(userAllocKey, dimension).toString();
if (StringUtils.isNotBlank(existingAllocId)) {
// 同维度存在未完成记录,返回已有地址
return addressPoolService.getAddressByAllocId(existingAllocId);
}
// 3. 回收用户不同维度的未使用地址(释放资源)
Object dim = redisTemplate.opsForHash().get(userAllocKey);
String otherDimension = dim.toString();
if (!otherDimension.equals(dimension)) {
String allocId = redisTemplate.opsForHash().get(userAllocKey, otherDimension).toString();
addressPoolService.recycleUnusedAddress(allocId); // 回收未使用地址
redisTemplate.opsForHash().delete(userAllocKey, otherDimension); // 删除记录
}
// 4. 从对应链+币种的空闲队列分配地址
String address = addressPoolService.allocateFromIdleQueue(chain, token);
// 5. 生成分配记录
String allocId = UUID.randomUUID().toString();
addressPoolService.saveAllocationRecord(allocId, userId, address, chain, token, 30); // 30分钟有效期
// 6. 记录用户-维度-分配记录映射
redisTemplate.opsForHash().put(userAllocKey, dimension, allocId);
// 7. 发送30分钟延时MQ,监控地址使用状态
sendDelayMonitorMsg(allocId, 30);
return new AddressVO(address, LocalDateTime.now().plusMinutes(30), chain, token);
} finally {
lock.unlock();
}
}
/**
* 发送延时监控消息
*/
private void sendDelayMonitorMsg(String allocId, int delayMinutes) {
MonitorMsg msg = new MonitorMsg(allocId);
// RocketMQ延时级别:LEVEL_18=30分钟(适配业务需求)
rocketMQTemplate.syncSend(
"topic:address:monitor",
MessageBuilder.withPayload(msg).build(),
3000,
18
);
}
}
3.2 地址池管理(按链+币种分队列+动态扩容)
按"链+币种"维度划分Redis空闲队列,结合动态扩容确保地址供应。
@Service
public class AddressPoolService {
// 空闲地址队列键:address:idle:{chain}:{token}
private static final String IDLE_QUEUE_KEY = "address:idle:%s:%s";
// 地址池容量阈值(低于20%触发扩容)
private static final double EXPAND_THRESHOLD = 0.2;
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private AddressMapper addressMapper;
@Autowired
private HDWalletService hdWalletService; // 基于BIP-32的HD钱包服务
/**
* 从指定链+币种的空闲队列分配地址
*/
public String allocateFromIdleQueue(String chain, String token) {
String queueKey = String.format(IDLE_QUEUE_KEY, chain, token);
// 1. 从Redis队列获取地址(右弹出,FIFO机制)
String address = redisTemplate.opsForList().rightPop(queueKey, 5, TimeUnit.SECONDS);
if (address == null) {
throw new BusinessException("当前链+币种地址池繁忙,请稍后再试");
}
// 2. 检查剩余空闲地址是否低于阈值,触发扩容
Long remaining = redisTemplate.opsForList().size(queueKey);
Long total = addressMapper.countTotalByChainAndToken(chain, token);
if (remaining * 1.0 / total < EXPAND_THRESHOLD) {
expandAddressPool(chain, token, total.intValue() / 2); // 扩容当前容量的50%
}
return address;
}
/**
* 动态扩容地址池
*/
@Async // 异步扩容,不阻塞主流程
public void expandAddressPool(String chain, String token, int count) {
log.info("开始扩容地址池:{}:{},需新增{}个地址", chain, token, count);
// 1. 生成新地址(基于HD钱包,确保私钥安全)
List<String> newAddresses = hdWalletService.generateAddresses(chain, count);
// 2. 批量入库(状态:0-空闲)
addressMapper.batchInsert(newAddresses.stream().map(addr ->
AddressDO.builder()
.address(addr)
.chain(chain)
.token(token)
.status(0)
.build()
).collect(Collectors.toList()));
// 3. 补充至Redis空闲队列
String queueKey = String.format(IDLE_QUEUE_KEY, chain, token);
redisTemplate.opsForList().leftPushAll(queueKey, newAddresses);
log.info("地址池扩容完成:{}:{},新增{}个地址", chain, token, newAddresses.size());
}
/**
* 回收未使用的地址
*/
public void recycleUnusedAddress(String allocId) {
AllocationRecordDO record = allocationMapper.selectById(allocId);
if (record == null || record.getStatus() != 1) { // 状态1-未使用
return;
}
// 1. 更新地址状态为空闲
addressMapper.updateStatus(record.getAddress(), 0);
// 2. 放回对应链+币种的空闲队列
String queueKey = String.format(IDLE_QUEUE_KEY, record.getChain(), record.getToken());
redisTemplate.opsForList().leftPush(queueKey, record.getAddress());
// 3. 更新分配记录状态为"已回收"
allocationMapper.updateStatus(allocId, 3); // 状态3-已回收
}
}
3.3 延时监控与交易处理闭环
通过延时MQ监控地址使用状态,结合区块事件驱动确认数更新,确保交易有效后入账。
// 1. 延时监控消息消费(检查地址是否使用)
@Service
@RocketMQMessageListener(topic = "topic:address:monitor", consumerGroup = "monitor_consumer")
public class AddressMonitorConsumer implements RocketMQListener<MonitorMsg> {
@Autowired
private AllocationRecordMapper allocationMapper;
@Autowired
private AddressPoolService addressPoolService;
@Override
public void onMessage(MonitorMsg msg) {
String allocId = msg.getAllocId();
AllocationRecordDO record = allocationMapper.selectById(allocId);
if (record == null) {
return;
}
// 地址分配后30分钟内未产生交易,回收地址
if (record.getStatus() == 1 && StringUtils.isBlank(record.getTxHash())) {
addressPoolService.recycleUnusedAddress(allocId);
log.info("地址{}未使用,已回收(分配记录:{})", record.getAddress(), allocId);
}
}
}
// 2. 区块事件驱动的确认数更新(确保交易不可逆)
@Service
public class BlockEventService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private TransactionService transactionService;
// 监听新区块事件,更新确认数
public void handleNewBlock(BlockEvent event) {
String chain = event.getChain();
long latestHeight = event.getHeight();
// 缓存最新区块高度(1分钟过期)
redisTemplate.opsForValue().set("block:latest:" + chain, latestHeight, 1, TimeUnit.MINUTES);
// 触发确认数检查(仅处理接近阈值的交易)
transactionService.checkNearThresholdTransactions(chain, latestHeight);
}
}
四、方案成果与技术亮点
- 资源利用率提升:同维度地址申请限制使重复申请率从35%降至0,不同维度自动回收使地址复用率提升60%;
- 时效性优化:结合"延时MQ监控+区块事件驱动",地址回收延迟从2小时缩短至30分钟,确认数更新时效<10秒;
- 弹性支撑能力:动态扩容机制确保高并发场景下地址池无枯竭,支撑地址申请峰值;
- 金融级可靠性:通过分布式锁、幂等设计及TokenView API二次校验,实现资金入账零误差,符合Card项目"线上0资损"目标。
本文来自博客园,作者:ffffox,转载请注明原文链接:https://www.cnblogs.com/ffffox/p/19040011

浙公网安备 33010602011771号