数据库 关于乐观锁的重试

nainc · 2016年03月25日 · 最后由 zamia 回复于 2016年03月27日 · 4016 次阅读

### 关于乐观锁的重试

锁相关的知识可以参考 @zamia 的这篇文章 https://ruby-china.org/topics/28963

这篇文章算是一点小小的补充

### 如何本地复现乐观锁冲突

在访问量大的线上环境,使用了乐观锁的地方是有可能出现 StaleObjectError 的异常的,本地如何复现呢?这个有个小方法(可能比较 low)

  1. 使用 byebug 这个 gem,在锁的事务中加上 byebug 调试,例:

    假设这是一个库存的对象,库存比较容易出现抢锁的情况

    def unforzen
        self.with_lock do
            byebug
            self.frozen_num -= 1
            self.save!
        end
    end
    
  2. 开两个 console,获取两个对象(stock_a 和 stock_b)

  • 调用 stock_a.unforzen,会进入到如下调试模式,可以看到已经进入到事务中了:

  • 然后调用 stock_b.unforzen,跟 a 相同,也进入到了事务,这时将 a 的代码执行完,next 是单步执行,continue 是执行到下一个断点。

  • 执行 b 的代码,这时你会发现 b 的 unforzen 方法抛出了 StaleObjectError 的异常

### 准备工作结束,如何重试

值得注意的一点是,reload 需要给参数 lock: true,否则 lock_version 是不会更新的

def unforzen
    optimistic_lock_retry_times = 3
    begin
        self.with_lock do
            byebug
            self.frozen_num -= 1
            self.save!
        end
    rescue ActiveRecord::StaleObjectError => e
        optimistic_lock_retry_times -= 1
        if optimistic_lock_retry_times > 0
            self.reload(lock: true)
            retry
        else
                raise e
        end
    rescue => e
        raise e
    end
end

### 一点疑问

其实实际的代码要比例子复杂一点,这个释放库存的功能是通过一个 service 调用的,异常统一在 service 中处理,但是 service 不能直接获取到出现锁异常的 record,我尝试过 error.record.reload(lock: true),如下:

rescue ActiveRecord::StaleObjectError => e
    p e.record.lock_version
    e.record.reload(lock: true)
    p e.record.lock_version
    retry
end

发现 lock_version 是变了的,但是重新 retry 的代码中的 record 的 lock_version 还是原来的。这块还没仔细研究原理,懂得同学也可以分享一下~

赞! 感觉 retry 的时候调用的 record 可能还是老的对象,得再测试一下~

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册