专题五 Redis高并发场景

介绍

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

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

分布式测试环境

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

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

专题五 Redis高并发场景_第1张图片

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端口

专题五 Redis高并发场景_第2张图片

核心接口程序

    @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次

专题五 Redis高并发场景_第3张图片

专题五 Redis高并发场景_第4张图片

结果展示

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

专题五 Redis高并发场景_第5张图片

专题五 Redis高并发场景_第6张图片

分布式优化一

采用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就会获取锁,从而导致了锁失效问题,配上我拙劣的图。

解决方法

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);
            }*/
        }

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

你可能感兴趣的:(Redis,redis,java,分布式)