【开发经验】之记一次Redis分布式锁造成的事故

这篇具有很好参考价值的文章主要介绍了【开发经验】之记一次Redis分布式锁造成的事故。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一、事故场景

有次,运营和商家做了个限量抢购活动,限量100件,但活动当天却超卖了,最终卖出的数量是160多件。这种超卖是比较严重的事故,出现了的话基本上和分布式锁有关系。

二、问题分析

项目中的抢购订单使用了分布式锁,而分布式锁的是基于Redis实现的,下面是订单抢购核心代码(使用伪代码讲解):

String key = "key:" + request.getSeckillId;
Boolean lockFlag = null;
try {
	// 获取分布式锁
    Boolean lockFlag = redisTemplate.opsForValue().setIfAbsent(key, "val", 10, TimeUnit.SECONDS);
    if (lockFlag) {
        // HTTP请求调用其他服务接口
        ......
        // 库存校验
        Object stock = redisTemplate.opsForHash().get(key+":info", "stock");
        assert stock != null;
        if (Integer.parseInt(stock.toString()) <= 0) {
            // 异常
        } else {
        	// 扣减库存
            redisTemplate.opsForHash().increment(key+":info", "stock", -1);
            // 发送事件,异步生成订单
        }
    }
} finally {
    // 释放锁
    if (lockFlag) {
    	stringRedisTemplate.delete("key");
    }
}

代码中给分布式锁设置10秒超时时间来保障业务逻辑有足够的执行时间,并且也对库存进行了校验,整块逻辑采用 try-finally 语句块来保证锁一定会及时释放,代码看起来很安全,平时也没有出现问题。

但问题就在于,中间有调用其他服务接口,并且在抢购活动开始的一瞬间,因为流量过大,导致调用所依赖的服务超时而锁失效。这个时候就会发生一连串的连锁反应:一开始获得锁的线程还没有执行完毕,锁就被另一个线程获取了,而第一个线程执行业务逻辑完毕后执行释放锁的操作时就会把第二个线程的锁给释放了,然后第三个线程再次获取锁,就这样陷入了恶性循环。

当然,虽然锁失去了作用,但还有个库存校验逻辑,但是偏偏库存校验逻辑不是非原子性的,代码中库存校验方式是先从 Redis 中 get 出库存数量,然后判断库存是否还有,最后再进行库存的扣减。这种库存校验的方式在锁正常的情况下也是可以的,但一旦锁失效就是不安全了。

所以,问题的根本原因在于库存校验严重依赖了分布式锁最终才导致超卖。

三、解决问题

从上面的分析可以知道,问题就出现在分布式锁和库存校验那里,所以,我们可以对症下药。

1、使用相对安全的分布式锁

相对安全的定义就是:加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。但即使是这样也无法保障业务的绝对安全,因为锁的过期时间始终是有界的,除非不设置过期时间或者把过期时间设置的很长,但这样做也会带来其他问题,没有意义。

redisTemplate.opsForValue().setIfAbsent() 就是对应 redis 的命令 set key value [EX seconds] [PX milliseconds] [NX|XX],这是安全的同时也是原子性的。所以我们只需要实现安全的释放锁即可。

要想实现相对安全的释放分布式锁,必须依赖 key 的 value 值。在释放锁的时候,通过 value 值的唯一性来保证不会勿删。我们基于 LUA 脚本实现原子性的安全解锁,封装方法如下:

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<String> redisScript = RedisScript.of(luaScript);
    redisTemplate.execute(redisScript, Collections.singletonList(key), Collections.singleton(val));
}

2、实现安全的库存校验

如果我们对于并发有比较深入的了解的话,会发现想 Redis 的 get and compare/ read and save 等操作,都是非原子性的。如果要实现原子性,我们可以借助 LUA 脚本来实现。但就我们这个例子中,由于抢购活动一次只能购买一件,所以可以不用基于LUA脚本实现而是基于 redis 本身的原子性:

// redis 操作完数据并返回操作结果的整个过程是原子性的
Long currStock = redisTemplate.opsForHash().increment("key", "stock", -1);

所以,代码中的库存校验是多余的,下面是优化后结果:

String key = "key:" + request.getSeckillId();
String val = UUID.randomUUID().toString();
try {
	// 获取分布式锁
    Boolean lockFlag = redisTemplate.opsForValue().setIfAbsent(key, val, 10, TimeUnit.SECONDS);
    if (!lockFlag) {
        // 业务异常
    }
    // HTTP请求调用其他服务接口
    ......
    // 库存校验,基于redis本身的原子性来保证
    Long currStock = stringRedisTemplate.opsForHash().increment(key + ":info", "stock", -1);
    if (currStock < 0) { // 说明库存已经扣减完了。
        // 业务异常。
        log.error("[抢购下单] 无库存");
    } else {
        // 发送事件,异步生成订单
    }
} finally {
    safedUnLock(key, val);
}

四、方案优化

1、是否需要分布式锁

其实可以发现,我们借助于redis本身的原子性扣减库存,也是可以保证不会超卖的。对的。但是如果没有这一层锁的话,那么所有请求进来都会走一遍业务逻辑,由于依赖了其他系统,此时就会造成对其他系统的压力增大。这会增加的性能损耗和服务不稳定性,得不偿失。

2、分布式锁的选型

  • 可以使用 Redission 来解决锁的存续问题;
  • 也可以用 RedLock 来实现分布式锁。RedLock的可靠性更高,但其代价是牺牲一定的性能。在本场景,这点可靠性的提升远不如性能的提升带来的性价比高。如果对于可靠性极高要求的场景,则可以采用 RedLock 来实现。

3、能否用数据库做最终的防护

如果在 Redis 中扣减库存成功后进行数据库的同步操作,比如使用 set stock = stock - 1 where stock - 1 来保证不会超卖,将这做为最后的保障手段。但在高并发场景下操作数据库更新的话会有性能损耗,也会给数据库带来很大压力,但要论证多大才算大,以我的经验 mysql 简单字段的并发写 1000~2000 qps是完全扛得住的,这需要压测来论证,当然如果并发太高也可以只使用缓存操作,异步机制同步

在性能要求极高的场景下,一般数据以缓存为准,支付交易等也是如此,分布式场景下,大多数场景都是最终一致性。文章来源地址https://www.toymoban.com/news/detail-481403.html

到了这里,关于【开发经验】之记一次Redis分布式锁造成的事故的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • zookeeper实现分布式锁-curator,java面试项目经验案例

    //5.如果不是第一个节点,需要监听前一个节点 //用一个临时变量记录当前节点的上一个节点 String previousNode = firstNode; for(String node : children){ if(currentNode.endsWith(node)){ //如果当前节点是node节点 ,那么就监听它的上一个节点 :比如 currentNode 这里是 0003节点 ,那 node就是 0002节点

    2024年04月23日
    浏览(41)
  • Redis与分布式-分布式锁

    接上文 Redis与分布式-集群搭建 为了解决上述问题,可以利用分布式锁来实现。 重新复制一份redis,配置文件都是刚下载时候的不用更改,然后启动redis服务和redis客户。 redis存在这样的命令:和set命令差不多,但是它有一个机制,当指定的key不存在的时候,才能进行插入,实

    2024年02月07日
    浏览(46)
  • Redis分布式锁和分布式事务

    Redis分布式锁和分布式事务 一、Redis分布式锁 1.1 watch和事务实现分布式锁 原理是通过watch来观察一个变量,一个线程在操作的时候,其他线程会操作失败,相当于乐观锁。 1.2 setnx实现分布式锁 原理是通过setnx设置一个变量,设置成功的线程抢到锁,执行相关的业务,执行完毕

    2024年02月09日
    浏览(42)
  • 【redis】redis分布式锁

    一、为什么需要分布式锁 1.在java单机服务中,jvm内部有一个全局的锁监视器,只有一个线程能获取到锁,可以实现线程之间的互斥 2.当有多个java服务时,会有多个jvm,也会有多个锁监视器,这样没办法使得多个jvm之间的线程互斥,所以无法使用jvm内部的锁监视器,也就是s

    2023年04月25日
    浏览(42)
  • 分布式锁实现(mysql,以及redis)以及分布式的概念

    我旁边的一位老哥跟我说,你知道分布式是是用来干什么的嘛?一句话给我干懵了,我能隐含知道,大概是用来做分压处理的,并增加系统稳定性的。但是具体如何,我却道不出个1,2,3。现在就将这些做一个详细的总结。至少以后碰到面试官可以说上个123。 那么就正式进入

    2024年01月21日
    浏览(58)
  • 分布式系统面试全集通第一篇(dubbo+redis+zookeeper----分布式+CAP+BASE+分布式事务+分布式锁)

    什么是分布式 一个系统各组件分别部署在不同服务器。彼此通过网络通信和协调的系统。 也可以指多个不同组件分布在网络上互相协作,比如说电商网站 也可以一个组件的多个副本组成集群,互相协作如同一个组件,比如数据存储服务中为了数据不丢失而采取的多个服务备

    2024年04月11日
    浏览(46)
  • Redis分布式问题

      Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系Redis中可以使用SETNX命令实现分布式锁。当且仅当 key 不存在,将 key 的值设为 value。 若给定的 key 已经存在,则SETNX 不做任何动作SETNX 是『SET if Not eXists』(如果不

    2024年02月09日
    浏览(35)
  • Redis 分布式锁

    在一个分布式的系统中,也会涉及到多个节点访问同一个公共资源的情况。此时就需要通过锁来做互斥控制,避免出现类似于“线程安全”的问题。 而 java 的 synchronized 或者 C++ 的 std::mutex,这样的锁都是只能在当前进程中生效,在分布式的这种多个进程多个主机的场景下就无

    2024年02月21日
    浏览(26)
  • [Redis 分布式锁 ]

    目录 前言:   使用场景: 基于 Redis 实现分布式锁的详细示例: 使用示例: 依赖: Redis分布式锁控制并发访问:    记录一些小笔记 , 如果对你有帮助 那就更好了 Redis 实现分布式锁的使用场景包括: 防止重复操作:在分布式环境下,多个进程可能同时对同一个资源进行操作,

    2024年02月08日
    浏览(39)
  • redis与分布式

    主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(Master),后者称为从节点(Slave),数据的复制是单向的,只能由主节点到从节点。Master以写为主,Slave 以读为主(只读模式),当主节点关闭后,从节点依然可以读取数据,但是会报错 优点 :

    2024年02月12日
    浏览(36)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包