回顾

在上篇文章中我们谈到了如何实现一个分期支付的原理,大体上是用户点击支付按钮后会先去判断用户的余额和合同金额是否足够去进行支付
然后去发送到消息队列去扣减余额

问题

我将消息发送到消息队列去消费的原因是为了提高系统的吞吐量和同时使用异步的操作防止阻塞系统,
但是又如下的几个问题:

  1. 首先就是是否需要保证消息的顺序读写
  2. 其次就是我们是否消息重复怎么解决,如何保证幂等性
  3. 如何提高消费者的消费速率
  4. 消费失败了怎么办(消息丢失)
  5. 调用多个微服务为什么不使用分布式事务,为什么使用消息队列保证最终一致性

    当然这些都是我们后面需要探讨的,现在我们要进行探讨的是为什么需要使用redission分布式锁来防止同时修改 同一个商家会出问题

实现

如果我们不使用kafka进行拉高消费速率的话,我们可能不会遇到这个情况,因为延迟队列会保证只有一个线程更改资源

我们注意到

转账细节

在底下的发送的操作中我们使用了一个async的方法去进行处理转账
在这里我开辟了一个核心线程数为十的线程池充当消费者

线程池

接下来我们来看异步的过程处理了哪些任务

  1. 判断是否是热点用户
    1
    2
    3
    4
    5
    if(hotSpotAccountListener.isHotSpot(merchantId)) {
    JSONObject jsonObject = new JSONObject();
    jsonObject.put("contractId", merchantId);
    jsonObject.put("unitAmount", unitAmount);
    merchantServiceFeign.updateBalanceWithCAS(jsonObject);

    引出一个问题:该如何进行判断热点用户
    我们的解决方案是使用redis作为缓存,对于每一个商家进行转账的时候使用一个计数器进行+1的操作
    实际上我们使用了一个滑动窗口,如果每分钟操作速度超过十次我们可以判断他为热点用户
    detectHotAccount

如果不是热点用户,那么就使用redission分布式锁,其中redission有多种实现方式(可重入锁,读写锁)他的watchdog机制还能够实现锁的自动续期

watch dog 的自动延期机制
如果拿到分布式锁的节点宕机,且这个锁正好处于锁住的状态时,会出现锁死的状态,为了避免这种情况的发生,锁都会设置一个过期时间。这样也存在一个问题,加入一个线程拿到了锁设置了30s超时,在30s后这个线程还没有执行完毕,锁超时释放了,就会导致问题,Redisson给出了自己的答案,就是 watch dog 自动延期机制。
Redisson提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期,也就是说,如果一个拿到锁的线程一直没有完成逻辑,那么看门狗会帮助线程不断的延长锁超时时间,锁不会因为超时而被释放。
另外Redisson 还提供了可以指定leaseTime参数的加锁方法来指定加锁的时间。超过这个时间后锁便自动解开了,不会延长锁的有效期。
所以释放锁操作一定要放到 finally {} 中;

Q&A

  1. 从分布式锁到CAS+版本号的过程中,是否应该考虑边界问题
    在这个问题上,通过事务保证一致性,避免锁和CAS并行执行
  2. CAS有什么问题?
    CAS作为乐观锁在高并发场景下确实可能失败,因此我设置了三次重试,失败后降级到分布式锁,重试的时候加入退避指数,减少数据库压力。
  3. 分布式锁和cas结合使用,如何保证数据一致性
    这个问题比较少见,需要确保同一个时刻只有一个机制生效,比如切换到CAS之前,先要释放锁并且更新状态,数据库操作使用事务包裹,保证余额变更的原子性。
  4. 如果redis出现故障分布式锁会出现什么影响?如何解决
    redis发生故障会导致分布式锁不可用,这时候考虑降级方案:切换到数据库的悲观锁(select for update),同时可以考虑使用redis集群,减少故障概率。
  5. 为什么延时后的任务不使用线程池来直接进行消费,而是加入到kafka中
    留在下一篇去解决