Redis高并发分布式锁



高并发场景秒杀抢购超卖Bug

  在今天的数字化世界中,高并发已经成为了许多在线应用的常态,尤其是在电商平台的秒杀活动、票务系统的抢票环节,或者任何需要处理大量用户请求的场景中。然而,高并发也带来了一系列的挑战,其中最常见的就是超卖问题。

  想象一下,你正在举办一个大型的秒杀活动,数万名用户在同一时间抢购同一款限量的商品。在理想的情况下,系统应该能够正确地处理所有的请求,确保商品的库存数量不会被超额扣减。然而,现实情况往往并非如此。如果没有有效的并发控制机制,你的系统可能会在短时间内接收到大量的购买请求,导致商品的库存数量被超额扣减,即出现超卖现象。这不仅会影响到你的业务运营,也会对用户体验产生负面影响。

高并发场景秒杀抢购Demo

以下是一个高并发场景场景秒杀抢购的一个Demo:

这里我用了一个子项目继承了父项目

父pom.xml

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>3.1.1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

子pom.xml

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
    </dependencies>

下面就是代码了:

StockController.class

@RestController
@RequestMapping("/stock")
public class StockController {

    @Autowired
    StockService stockService;

    @RequestMapping("/deduct")
    public String deductStock(){
        return stockService.deductStock();
    }
}

StockService.class

@Service
public class StockService {

    @Autowired
    StringRedisTemplate stringRedisTemplate;
    public String deductStock(){
        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
        if(stock>0){
            int realStock = stock -1;
            stringRedisTemplate.opsForValue().set("stock",realStock+"");
            System.out.println("扣减成功,剩余库存:"+realStock);
        }else {
            System.out.println("扣减失败,库存不足");
        }
        return "end";
    }
}

这里使用了redis,给redis中stock的值是200
在这里插入图片描述
然后用Apache JMeter压测工具进行压测,我这里是让500个线程在1s之内启动然后去争抢200个库存

在这里插入图片描述

为了更直观的看出到底售卖出了多少商品,我在redis中存放了一个 keyokvalue0 的值,然后修改StockService,每次售卖出一件商品的时候给redis中的ok +1。
在这里插入图片描述

在这里插入图片描述

测试结果

从控制台的输出打印可以看出出现了大量的超卖问题,同一个商品被卖出了多次,200个商品卖完,竟然卖出了313件。
在这里插入图片描述
在这里插入图片描述在这里插入图片描述

那么,如何解决这个问题呢?

JVM级别锁

学过并发,会想到使用synchronized或者ReentrantLock,这样的JVM级别的锁来解决这个问题。
在这里插入图片描述
进行压测会发现问题解决了,200件商品的确是卖出去了200次,并没有出现超卖问题。但是如果在高并发场景下,使用了分布式架构。有多个tomcat,JVM级别的锁还有用吗。

使用nginx对本地服务进行负载均衡

这里我启动了两个服务器,使用了不同端口,一个8080,一个8090
在这里插入图片描述
然后本地启动了一台虚拟机,这台虚拟机用来做本地的负载均衡使用
在这里插入图片描述
虚拟机已经安装好了nginx,首先ipconfig找到本机的局域网ip地址,在nginx.conf 配置上对该ip地址的8080端口和8090端口进行负载均衡。
在这里插入图片描述

在这里插入图片描述
最后访问虚拟机的局域网地址和nginx.conf 配置的端口号即可。虚拟机的局域网地址可以通过ifconfig命令查询
在这里插入图片描述
在这里插入图片描述
不停的访问该地址可以看到两个服务器上都有打印输出,现在对该接口进行压测发现200个商品被卖出去了245次。
在这里插入图片描述
JVM级别的锁只能管理本tomcat的线程,其他服务器的线程是没有办法管理的。那应该使用什么呢?

Redis实现分布式锁

Redis提供了一种名为SETNX的命令,可以用来实现分布式锁。
在这里插入图片描述

Redis分布式锁实现Demo

@Service
public class StockService {

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    public String deductStock() {
        String lockKey = "lock:product_1";
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "root");
        if (!result) {
            return "error_code";
        }
        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
        if (stock > 0) {
            int realStock = stock - 1;
            stringRedisTemplate.opsForValue().set("stock", realStock + "");
            stringRedisTemplate.opsForValue().increment("ok");
            System.out.println("扣减成功,剩余库存:" + realStock);
        } else {
            System.out.println("扣减失败,库存不足");
        }
        stringRedisTemplate.delete(lockKey);
        return "end";
    }
}

Redis分布式锁有关问题

当然这里还有很多问题
问题1:通过SETNX上锁以后,中间代码如果出现异常,导致后面的删除锁代码无法执行,导致死锁
解决方法:添加try-catch,并将删除锁代码放在finally中,这样无论有没有抛异常都会释放锁。
在这里插入图片描述
问题2:通过SETNX上锁以后,运行过程出现 宕机 ,系统被 重启 ,同样将导致 死锁
解决方法:给锁 设置超时时间,过了一段时间以后,锁将自动释放。
在这里插入图片描述
问题3:通过SETNX上锁以后 出现异常 ,导致无法去expire设置锁的超时时间,也没有办法去手动释放锁,导致 死锁
解决方法:保证上锁和设置超时时间原子性,使用Boolean setIfAbsent(K key, V value, long timeout, TimeUnit unit);方法。

在这里插入图片描述
问题4:线程A中 上锁设置过期时间 完成以后,系统出现阻塞,导致锁已经到了过期时间并自动删除了,这时候还没执行释放锁的操作,这时候线程B上锁成功,并执行任务,结果线程A反应过来了,继续执行释放锁的操作,把线程B上的锁给释放了,后面的线程上的锁都被前面的线程释放。
解决方法:这个问题的根本点在于,线程可以释放其他线程所加的锁,可以给锁添加uuid让线程只能释放自己加的锁
在这里插入图片描述
问题5:线程A 上锁设置过期时间并执行任务后,希望判断比较锁的id之后去释放锁,判断通过以后系统出现阻塞,阻塞到 锁已经过期了,但是此时并未执行释放锁的操作,此时线程B上锁成功,并去执行任务。线程A反应了过来,然后将线程B上的锁给释放了,这时候又将出现上面的问题。
解决方法:这个问题的根本原因在于 最后判断锁id的时候和释放锁的操作没有保证 原子性 。使用redis执行LUA脚本,保证 能同时执行判断锁和释放锁。这个redis并没有提供方法。
问题6锁续命,线程A任务还没执行完,锁已经过期了,此时其他线程也会执行任务,就会出现并发安全问题。
解决方法:可以使用WatchDog,也就是给任务执行线程添加守护线程,守护线程负责对锁的expire时间进行监控,每当到过期前一秒就对过期进行判断,如果任务还在进行且锁马上过期,就对过期时间进行设置。
上面的问题5问题6都可以通过redis组件,redisson解决
在这里插入图片描述

在这里插入图片描述

分布式锁性能的提升

减少锁的粒度

上述场景可以使用分段锁。基于加锁的分段机制,可以给分布式锁的性能提升几十倍。将一个商品分成好几个key。比如一个商品1000个库存,拆成10个key,每个key放100个库存。
实际实现还是有许多细节,比如对key的轮询选择,如果一个key库存为0了,就不应该再选择这个库存了。如果每个key库存都只有1,要减少5个库存的解决。实现可以参考ConcurrentHashMap1.7的源码。

使用异步处理

你可以使用消息队列等技术,将请求的处理过程异步化。当一个请求到达时,你只需要将它放入消息队列,然后立即返回。这样,你的系统就可以快速地处理大量的并发请求。然后,你可以在后台有一个或多个工作线程,从消息队列中取出请求并处理。


  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Redis是一个开源的高性能键值对存储数据库,它支持多种数据结构和功能,其中包括分布式锁。所以,如果需要在分布式环境中使用锁来保证数据的一致性和并发操作的正确性,那么学习和掌握Redis分布式锁是很有必要的。 分布式锁是用来解决分布式系统中资源竞争、数据一致性和并发控制问题的一种机制。在分布式环境中,不同节点之间可能同时对同一个资源进行读写操作,这就需要使用分布式锁来保证资源的原子性和独占性,避免多个节点同时操作导致的数据不一致问题。 Redis分布式锁通过使用SETNX命令来实现,即当某个节点获取到锁时,将一个特定的key设置为1,其他节点会发现该key已经存在而无法获取锁。当获取到锁后,节点需要在执行完操作后手动释放锁,以供其他节点获取。通过使用Redis分布式锁,可以有效地实现资源的并发控制和数据的一致性。 学习和掌握Redis分布式锁可以帮助我们解决分布式环境下的并发访问问题,确保系统的可靠性和性能。同时,Redis分布式锁还可以用于实现一些常见的分布式算法,如分布式任务分配、分布式队列、分布式限流等,对于构建高可用、高性能的分布式系统具有重要作用。 因此,为了能够更好地应对分布式环境下的并发控制和数据一致性问题,以及利用Redis分布式锁实现其他分布式算法,学习和掌握Redis分布式锁是非常有必要的。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值