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);
    }
}

四、方案成果与技术亮点

  1. 资源利用率提升:同维度地址申请限制使重复申请率从35%降至0,不同维度自动回收使地址复用率提升60%;
  2. 时效性优化:结合"延时MQ监控+区块事件驱动",地址回收延迟从2小时缩短至30分钟,确认数更新时效<10秒;
  3. 弹性支撑能力:动态扩容机制确保高并发场景下地址池无枯竭,支撑地址申请峰值;
  4. 金融级可靠性:通过分布式锁、幂等设计及TokenView API二次校验,实现资金入账零误差,符合Card项目"线上0资损"目标。
posted @ 2025-08-15 17:04  ffffox  阅读(24)  评论(0)    收藏  举报