# 流水中增加变更前余额,很简单?

59 阅读4分钟

说简单也简单,说复杂也复杂(废话文学,哈哈~)

  • 老板:在商户余额流水中加一个 变更前余额 吧。

  • :好的,老板,分分钟搞定~

    // AddBalance 给商户增减余额
    func AddBalance(ctx context.Context, merchantId, addAmount int) error { 
      // 老板没球事干,流水中要记变更前余额
      // 那就添加一行获取变更前余额用于记流水
      beforeAmount := getBalance(ctx, merchantId)
      // 使用乐观锁给商户增加余额
      addBalance(ctx, merchantId, addAmount int)
      // 记录流水
      addBalanceLog(ctx, merchantId, beforeAmount, addAmount int)
    }
    
  • 老板:你的流水中变更前余额不对啊?

  • :原来是并发时,导致获取余额到写入流水之间被其它并行请求修改了。简单~,加一个锁就好了

    // AddBalance 给商户增减余额
    func AddBalance(ctx context.Context, merchantId, addAmount int) error { 
      // 加一个锁吧
      mutex.Lock()
      defer mutex.Unlock()
      // 获取变更前的余额用于记流水
      beforeAmount := getBalance(ctx, merchantId)
      // 使用乐观锁给门店增加余额
      addBalance(ctx, merchantId, addAmount int)
      // 记录流水
      addBalanceLog(ctx, merchantId, beforeAmount, addAmount int)
    }
    
  • 老板:还是不对啊?

  • :噫,为啥加了锁还不行呢?原来是多个Pod并行导致,简单~,用 github.com/go-redsync/redsync 加一个分布式锁就行了

    // AddBalance 给商户增减余额
    func AddBalance(ctx context.Context, merchantId, addAmount int) error { 
      // 那就换成分布式锁
      rs := redsync.New(pool)
      mutexname := "add-balance-lock"
      mutex := rs.NewMutex(mutexname)
      mutex.Lock()
      defer mutex.Unlock()
      // 获取变更前的余额用于记流水
      beforeAmount := getBalance(ctx, merchantId)
      // 使用乐观锁给门店增加余额
      addBalance(ctx, merchantId, addAmount int)
      // 记录流水
      addBalanceLog(ctx, merchantId, beforeAmount, addAmount int)
    }
    
  • 老板:变更前余额字段没问题了,但好多商户反馈下单一直转菊花,或提示超时

  • : 原来是锁的粒度太大,导致所有商户共用一把锁,简单,把锁粒度调整为商户级别

     // AddBalance 给商户增减余额
     func AddBalance(ctx context.Context, merchantId, addAmount int) error { 
       // 锁名称中将商户ID加进去
       mutexname := "add-balance-lock" + strconv.Itoa(merchantId)
       rs := redsync.New(pool)
       mutex := rs.NewMutex(mutexname)
       mutex.Lock()
       defer mutex.Unlock()
       // 获取变更前的余额用于记流水
       beforeAmount := getBalance(ctx, merchantId)
       // 使用乐观锁给门店增加余额
       addBalance(ctx, merchantId, addAmount int)
       // 记录流水
       addBalanceLog(ctx, merchantId, beforeAmount, addAmount int)
     }
    
  • 老板:变更前的余额对了,反馈超时的商户也少了,但还是有一些大商户经常提示超时

  • :原来是大商户并发大,导致其它协程一直在自旋取锁,先将自旋锁优化下吧,大量自旋请求redis效率太低,也容易造成redis资源的浪费,那就在分布式锁前面再加一把本地锁吧

    // merchantMutexMap 商户级本地锁
    // 商户数量不多,增加点内存可以接受
    var merchantMutexMap map[int]*sync.Mutex
    // AddBalance 给商户增减余额
    func AddBalance(ctx context.Context, merchantId, addAmount int) error { 
      // 先取本地锁
      // 这里省略对 merchantMutexMap加读写锁代码,否则并发写要panic
      localMutex[merchantId].Lock
      localMutex.Lock()
      defer localMutex.Unlock()
      // 取到本地锁后,再取分布式锁,有可能被其它Pod占有
      mutexname := "add-balance-lock" + strconv(merchantId)
      rs := redsync.New(pool)
      mutex := rs.NewMutex(mutexname)
      mutex.Lock()
      defer mutex.Unlock()
      // 获取扣款前的余额用于记流水
      beforeAmount := getBalance(ctx, merchantId)
      // 使用乐观锁给门店增加余额
      addBalance(ctx, merchantId, addAmount int)
      // 记录流水
      addBalanceLog(ctx, merchantId, beforeAmount, addAmount int)
    }
    
  • 老板:少了一些超时的反馈,但还是有,能再优化下吗?

  • :还有啥招呢?能取消redis的锁吗?行~,那就利用数据库的互斥锁来优化掉它吧

    // AddBalance 给商户增减余额
    func AddBalance(ctx context.Context, merchantId, addAmount int) error { 
      // 开启事务
      begin(ctx)
      // 使用乐观锁给门店增加余额
      addBalance(ctx, merchantId, addAmount int)
      // 此时事务中有写互斥,其它协程并发更新余额会阻塞,这里获取到的变更后的余额一定是准确
      afterAmount := getBalance(ctx, merchantId)
      // 变更前的余额 = 本次变更后的余额 - 本次变更的余额
      beforeAmount = afterAmount - addAmount
      // 记录流水
      addBalanceLog(ctx, merchantId, beforeAmount, addAmount int)
      // 提交事务
      commit(ctx)
    }
    
  • 老板:基本没什么超时反馈了,但大型商户还是偶尔有,能不能一次性彻底解决啊?

  • :只有放大招保饭碗了:

    1. 先将每个帐户流水的最后一条的 扣款前余额 刷正确
    2. 扣款时,只写操作了多少金额
    3. 异步计算扣款前余额:根据时间升序遍历未记录扣款前余额的数据,修改操作前的余额 = 上一条流水的余额 - 本次变更金额,如上一条流水为空,则为0

后记

如果并发不高,一般倒数第二种方案就可以了,不推荐最后一种,原因是:

  1. 业务要有一定实时性的容忍性
  2. 如果流水中某一次记录的 变更前余额 出错,将导致后面的全部出错,对开发的要求极高
  3. 金额可以不展示,但展示错误了,商户可要闹翻天,影响太大

这里其实也可以并并行两个方案,即默认走倒数第二个方案,大型商户直接不维护此字段,最后每天刷上去,或直接不刷,用对账单之类的来告知对方。