突击Redis重大事故现场,又是“分布式锁”惹的祸( 二 )
相对安全的定义:set、del是一一映射的 , 不会出现把其他现成的锁del的情况 。 从实际情况的角度来看 , 即使能做到set、del一一映射 , 也无法保障业务的绝对安全 。 因为锁的过期时间始终是有界的 , 除非不设置过期时间或者把过期时间设置的很长 , 但这样做也会带来其他问题 。 故没有意义 。要想实现相对安全的分布式锁 , 必须依赖key的value值 。 在释放锁的时候 , 通过value值的唯一性来保证不会勿删 。 我们基于LUA脚本实现原子性的get and compare , 如下:
public void safedUnLock(String key, String val) {String luaScript = "local in = ARGV[1] local curr=redis.call('get', KEYS[1]) if in==curr then redis.call('del', KEYS[1]) end return 'OK'"";RedisScript redisScript = RedisScript.of(luaScript);redisTemplate.execute(redisScript, Collections.singletonList(key), Collections.singleton(val));}
我们通过LUA脚本来实现安全地解锁 。
实现安全的库存校验
如果我们对于并发有比较深入的了解的话 , 会发现想 get and compare/ read and save 等操作 , 都是非原子性的 。 如果要实现原子性 , 我们也可以借助LUA脚本来实现 。 但就我们这个例子中 , 由于抢购活动一单只能下1瓶 , 因此可以不用基于LUA脚本实现而是基于redis本身的原子性 。 原因在于:
// redis会返回操作之后的结果 , 这个过程是原子性的Long currStock = redisTemplate.opsForHash().increment("key", "stock", -1);
发现没有 , 代码中的库存校验完全是“画蛇添足” 。
改进之后的代码
经过以上的分析之后 , 我们决定新建一个DistributedLocker类专门用于处理分布式锁 。
public SeckillActivityRequestVO seckillHandle(SeckillActivityRequestVO request) {SeckillActivityRequestVO response;String key = "key:" + request.getSeckillId();String val = UUID.randomUUID().toString();try {Boolean lockFlag = distributedLocker.lock(key, val, 10, TimeUnit.SECONDS);if (!lockFlag) {// 业务异常}// 用户活动校验// 库存校验 , 基于redis本身的原子性来保证Long currStock = stringRedisTemplate.opsForHash().increment(key + ":info", "stock", -1);if (currStock < 0) { // 说明库存已经扣减完了 。// 业务异常 。log.error("[抢购下单] 无库存");} else {// 生成订单// 发布订单创建成功事件// 构建响应}} finally {distributedLocker.safedUnLock(key, val);// 构建响应}return response;}
深度思考分布式锁有必要么
改进之后 , 其实可以发现 , 我们借助于redis本身的原子性扣减库存 , 也是可以保证不会超卖的 。 对的 。 但是如果没有这一层锁的话 , 那么所有请求进来都会走一遍业务逻辑 , 由于依赖了其他系统 , 此时就会造成对其他系统的压力增大 。 这会增加的性能损耗和服务不稳定性 , 得不偿失 。 基于分布式锁可以在一定程度上拦截一些流量 。
分布式锁的选型
有人提出用RedLock来实现分布式锁 。 RedLock的可靠性更高 , 但其代价是牺牲一定的性能 。 在本场景 , 这点可靠性的提升远不如性能的提升带来的性价比高 。 如果对于可靠性极高要求的场景 , 则可以采用RedLock来实现 。
再次思考分布式锁有必要么
由于bug需要紧急修复上线 , 因此我们将其优化并在测试环境进行了压测之后 , 就立马热部署上线了 。 实际证明 , 这个优化是成功的 , 性能方面略微提升了一些 , 并在分布式锁失效的情况下 , 没有出现超卖的情况 。然而 , 还有没有优化空间呢?有的! 由于服务是集群部署 , 我们可以将库存均摊到集群中的每个服务器上 , 通过广播通知到集群的各个服务器 。 网关层基于用户ID做hash算法来决定请求到哪一台服务器 。 这样就可以基于应用缓存来实现库存的扣减和判断 。 性能又进一步提升了!
// 通过消息提前初始化好 , 借助ConcurrentHashMap实现高效线程安全private static ConcurrentHashMap
通过以上的改造 , 我们就完全不需要依赖redis了 。 性能和安全性两方面都能进一步得到提升! 当然 , 此方案没有考虑到机器的动态扩容、缩容等复杂场景 , 如果还要考虑这些话 , 则不如直接考虑分布式锁的解决方案 。
- 折叠屏|iPhone 外观迎重大更新,还得等两年
- 高通骁龙888正式发布,5G重大升级更加完美
- 郑州移动5G通信建设 青年突击队
- 3天时间,我是如何解决redis bigkey 删除问题的?
- 好消息!在这个领域,中国首次超越美国成为世界第一,意义重大
- 并发容器ConcurrentHashMap
- 内蒙古自治区云计算领域启动两个科技重大专项
- Django实战016:django中使用redis详解
- 你不知道的Redis:入门?数据结构?常用指令?
- Python操作Redis大全