专题五 Redis高并发场景

介绍

Redis高并发场景,如果直接去学会比较抓不住头绪,因此本文将一步步介绍Redis的高并发的步骤演进。

首先解释synchronized不适合在分布式场景,因为synchronized只适用自身的JVM,因此在分布式场景下多台机器的情况下,可能会出现同时操作一个key,从而会出现两个服务同时进行商品购买后,商品数量只减1的情况。

分布式测试环境

为了模拟分布式场景,模拟电商库存售卖的场景,每次调用接口相当于输出货物然后库存减1。

下面搭建一个简易的分布式系统。配置一个Nginx进行负载均衡、启动两个服务去连接Redis

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ogP7th7I-1655106882784)(F:\技术积累\Redis学习.assets\image-20220613110032523.png)]

Nginx主要配置如下

upstream redislock{
		server 10.175.87.148:8080 weight=1;
		server 10.175.87.148:8090 weight=1;
	}
	server{
		listen       8000;
        server_name  localhost;
		
		location / {
			root html;
			index index.html index.htm;
			proxy_pass http://redislock;
		}
	}

接下来再开两个服务,服务主要提供对面提供操作Redis的接口。服务分别开启8090、8080端口

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SHBSJU4Y-1655106882791)(F:\技术积累\Redis学习.assets\image-20220613145548382.png)]

核心接口程序

    @RequestMapping("/deduct_stock_syn")
    public String deductStockSyn(){
        //version 1
        synchronized (this){ //synchronized 只在一个JVM进程中生效,分布式集群环境下不可以执行
            //逻辑块

            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";
    }

为了测试出synchronized不适用再分布式场景,我们采用Jmeter进行压测

1、在Path上添加上请求的路径 2、配置压测参数,当前设置的是并发200次请求,循环执行4次

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9wbVvaK8-1655106882793)(F:\技术积累\Redis学习.assets\image-20220613150334985.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T10rc6ce-1655106882795)(F:\技术积累\Redis学习.assets\image-20220613150546713.png)]

结果展示

从结果上来看,两台服务上有相同的剩余库存,意味着货物ID为46的货物被售卖了两次,出现了超卖的情况。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iOfY351o-1655106882796)(F:\技术积累\Redis学习.assets\image-20220613150828191.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ku2wVAkE-1655106882797)(F:\技术积累\Redis学习.assets\image-20220613150853081.png)]

分布式优化一

采用Redis的SetNX命令

SETNX  key  value 		//存入一个不存在的字符串键值对,如果已经存在就不能设置了

使用该命令,可以很好的使用分布式场景下多线程的竞争,但是加锁容易,删锁就容易出问题,容易出现以下问题。

删锁问题

问题1、如果程序运行逻辑突然失控,陷入死循环,就不能主动删除锁,其他线程就获取不到该锁会一直阻塞住

问题2、如果程序运行中,系统突然宕机,也无法主动删除锁

解决方法

针对问题1,可以在程序逻辑执行中添加 try、catch、finally 这类异常检测的内容,在finally中添加删除锁的操作,避免程序进入死循环后,其他线程无法拿到锁的情况。

针对问题2,添加锁的过期机制,在系统宕机后,锁自动过期,不影响其他用户获取该锁

实操程序
    @RequestMapping("/deduct_stock_setnx")
    public String deductStockSetnx(){
        //version 2
        String lockKey = "lockKey";
        String clientID = UUID.randomUUID().toString();
        //逻辑块
        try{
            Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "test"); //相当于 jdeis.setnx(key, value)
            stringRedisTemplate.expire(lockKey, 30 , TimeUnit.SECONDS); //锁过期设置
            if(!result){
                return "error 1001";
            }
   
            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("扣减失败,库存不足");
            }
            stringRedisTemplate.delete(lockKey);
        }finally { //如果程序跑飞了删除key,但是在分布式环境下,可能出现自己的锁被别人删掉了,因为程序逻辑(锁失效的情况)
            stringRedisTemplate.delete(lockKey);
        }
        return "end";
    }
分布式优化二

上述程序还存在问题

问题

1、如果系统在程序刚加完锁后,就立马宕机,意味着还来不及设置过期机制,其他程序也获取不当该锁

2、如果程序的执行时间过长,超过了锁失效的时间,可能会出现锁失效的问题。锁失效具体是指,线程1执行时间过长,导致锁已经过期,这时候线程2获取到锁并运行程序,但是此时线程1执行结束主动释放锁,但是此时是线程2上的锁,因此线程3就会获取锁,从而导致了锁失效问题,配上我拙劣的图。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8VsBFqGT-1655106882799)(F:\技术积累\Redis学习.assets\image-20220613154416749.png)]

解决方法

1、问题1的解决方法,是将获取锁指令和设置过期操作的指令和二为一,变成一条指令就具有原子性。

set key value [expiration EX seconds|PX milliseconds] [NX|XX]
参数说明:
EX seconds:将键的过期时间设置为 seconds 秒。
	SET key value EX seconds 等同于 SETEX key seconds value
PX millisecounds:将键的过期时间设置为 milliseconds 毫秒。
	SET key value PX milliseconds 等同于 PSETEX key milliseconds value
NX:只在键不存在的时候,才对键进行设置操作。
	SET key value NX 等同于 SETNX key value
XX:只在键已经存在的时候,才对键进行设置操作

2、问题2的解决方法,是将获取的锁Value设置为当前客户端的ID,每次主动删除的时候先判断是否为自身的锁,如果不是自己加的锁就不能删除。除了这一种解决方案还有锁续命的办法,每次程序执行一段时间会自己是否还持有锁,如果持有就给锁续上时间。

实操程序
    @RequestMapping("/deduct_stock_setnx")
    public String deductStockSetnx(){
        //version 2
        String lockKey = "lockKey";
        String clientID = UUID.randomUUID().toString();
        //逻辑块
        try{
            //Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "test"); //相当于 jdeis.setnx(key, value)
            //stringRedisTemplate.expire(lockKey, 30 , TimeUnit.SECONDS);
            Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientID, 10, TimeUnit.SECONDS);//相当于set key value [expiration EX seconds|PX milliseconds] [NX|XX]
            if(!result){
                return "error 1001";
            }
            //解决方案: 锁续命
            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("扣减失败,库存不足");
            }
            stringRedisTemplate.delete(lockKey);
        }finally { //如果程序跑飞了删除key,但是在分布式环境下,可能出现自己的锁被别人删掉了,因为程序逻辑(锁失效的情况)
//            stringRedisTemplate.delete(lockKey);
            //添加这句,表示只删除自身的锁
            if(clientID.equals(stringRedisTemplate.opsForValue().get(lockKey))){
                stringRedisTemplate.delete(lockKey);
            }
        }
        return "end";
    }
分布式优化三

采用无敌工具Redission,其底层是利用lua脚本实现的,Redission就包含锁续命的过程,使用起来十分方便。

@RequestMapping("/deduct_stock")
    public String deductStock() {
        String lockKey = "product_101";
        String clientId = UUID.randomUUID().toString();
        RLock redissonLock = redisson.getLock(lockKey); //redisson 获取锁对象
        try {
            
            //加锁,实现续命操作
            redissonLock.lock();  //setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS);
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }

        } finally {
            //解锁命令
            redissonLock.unlock();
            /*if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
                stringRedisTemplate.delete(lockKey);
            }*/
        }

运行程序在我的资源中下载

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Nginx和Redis都是在高并发场景下常用的工具。Nginx是一个高性能的Web服务器和反向代理服务器,而Redis是一个高性能的键值存储数据库。 当涉及到高并发时,Nginx可以用作负载均衡器,将请求分发给多个后端服务器,从而提高系统的吞吐量和性能。Nginx使用事件驱动的异步架构,可以处理大量并发连接,同时具有较低的资源消耗。 Redis作为一个内存数据库,具有快速读写的能力,适合处理大量的并发请求。它支持多种数据结构和丰富的功能,例如缓存、消息队列和分布式锁等,可以帮助应对高并发场景下的数据存储和处理需求。 在高并发环境中,可以通过以下几点来优化Nginx和Redis的性能: 1. Nginx: - 合理配置Nginx的工作进程数和连接数,使其适应并发请求的压力。 - 使用Nginx的缓存功能,减轻后端服务器的压力。 - 配置合适的超时时间,避免请求长时间占用连接资源。 - 使用Nginx的Gzip压缩功能,减小传输数据量,提高响应速度。 - 使用Nginx的Keepalive机制,减少TCP连接的建立和关闭开销。 2. Redis: - 合理设计数据模型,减少数据存储和检索的复杂度。 - 使用Redis集群或主从复制,提高读写性能和数据的可用性。 - 配置适当的内存策略,例如使用LRU算法来淘汰冷数据。 - 使用Redis的Pipeline和批量操作来减少网络延迟和降低通信开销。 - 避免频繁的大批量写入操作,可以通过异步或者缓存策略来优化性能。 综上所述,Nginx和Redis高并发场景下可以发挥其优势,通过合理的配置和优化可以提高系统的性能和稳定性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值