Redis实现分布式锁

Redis实现分布式锁的核心:

redis命令:setnx key value

SETNX key value

可用版本: >= 1.0.0

时间复杂度: O(1)

只在键 key 不存在的情况下, 将键 key 的值设置为 value 。

若键 key 已经存在, 则 SETNX 命令不做任何动作。

SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。

返回值

命令在设置成功时返回 1 , 设置失败时返回 0 。

代码示例

redis> EXISTS job                # job 不存在
(integer) 0

redis> SETNX job "programmer"    # job 设置成功
(integer) 1

redis> SETNX job "code-farmer"   # 尝试覆盖 job ,失败
(integer) 0

redis> GET job                   # 没有被覆盖
"programmer"

 

使用场景:

单机场景下redis预减库业务在高并发场景下会出现超卖问题:

代码逻辑:redis查询库存 → redis减库存

上述操作不是原子性的,当多个线程同时执行该方法时,会出现超卖问题:线程A查询库存但是还没减库存这一个时间差内,线程B来查询了库存,假设库存有50个,线程A查询了50,同时线程B查询了50,那么线程A减库存,库存剩下49,而线程B也减库存,库存也剩下49,这就造成了超卖问题,卖给了两个人,结果数据库就减了一份

@RequestMapping("/redis")
public class RedisDemoController {

    @Autowired
    private RedisTemplate<String,Integer> redisTemplate;


    @RequestMapping("/lock")
    public String deductStock(){
        int stock = redisTemplate.opsForValue().get("stock");
        if(stock < 0){
            int remainStock = stock - 1;
            redisTemplate.opsForValue().set("stock",remainStock);
            System.out.println("扣减成功,剩余库存:"+remainStock);
        }else{
            System.out.println("扣减失败,库存不足");
        }

        return "index";
    }
}

 

怎么解决?

用synchronized代码段把redis查库存到减库存这一段逻辑锁起来,让其执行起来是原子性的

@RequestMapping("/redis")
public class RedisDemoController {

    @Autowired
    private RedisTemplate<String,Integer> redisTemplate;


    @RequestMapping("/lock")
    public String deductStock(){
        
        synchronized (this){//加上synchronized锁
            int stock = redisTemplate.opsForValue().get("stock");
            if(stock < 0){
                int remainStock = stock - 1;
                redisTemplate.opsForValue().set("stock",remainStock);
                System.out.println("扣减成功,剩余库存:"+remainStock);
            }else{
                System.out.println("扣减失败,库存不足");
            }
        }
        return "index";
    }
}

上面的场景是单机场景下,那假如是分布式场景呢?分布式场景就出现问题了,synchronized是JVM级别的锁,假如是分布式场景下,synchronized就不起作用了,虽然在单机上不会出现线程问题,但是即便用了synchronized,相对于整个集群来说,也是线程不安全的,可以看下面这张图,即便Tomcat 1和2都部署了单机场景下线程安全的项目,假如有两个请求,一个是A,另一个是B,然后请求A被Nginx分发到Tomcat 1,请求B被Nginx分发到Tomcat 2 上,然后两个请求同时执行上面的这段代码,那么同样会出现超卖的问题,synchronized是JVM级别的,而锁不了整个集群

分布式下的秒杀项目(redis预减库存逻辑)

假设场景:秒杀项目分部在多个tomcat运行,如果恰好有两个请求同时落在redis预减库存逻辑上,那么就会出现超卖问题,比如商品有50个,线程A查询有50个,买了一个,然后库存就剩下49个,恰好线程B也查到有50个,也买了一个,结果库存还是49个,这就造成了超卖问题。

 

 

解决上述问题还是离不开加锁,给谁加锁?给redis加锁,这个时候就要用到setnx命令了

redis是单线程模型,并发请求落到redis上也只能是一个一个执行,假设线程A给redis上了锁setnx,key为商品id,那么当其他线程落到访问redis时,同样执行setnx操作,但是线程A已经给redis上了锁,setnx在key存在时的情况会执行失败,所以其他线程只能等待线程A释放锁

@RequestMapping("/lock3")
public String RedisDistributeLock(User user){
    String lockKey = "product_001";

    //分布式锁,setIfAbsent是对setnx的封装,setnx成功返回true,否则返回false
    Boolean LockSuccess = redisTemplate.opsForValue().setIfAbsent(lockKey, user.getId());
    if(!LockSuccess){
        return "error";
    }


    int stock = redisTemplate.opsForValue().get("stock");
    if(stock < 0){
        int remainStock = stock - 1;
        redisTemplate.opsForValue().set("stock",remainStock);
        System.out.println("扣减成功,剩余库存:"+remainStock);
    }else{
        System.out.println("扣减失败,库存不足");
    }

    //释放分布式锁:把lockKey删掉
    redisTemplate.delete(lockKey);
    return "index";
}

 

用了redis的setnx就万无一失了吗?

有加锁就必须有解锁(删除setnx种下的key),假如线程A加锁成功后,还没释放锁,结果抛异常了,就没办法释放锁了,那么线程A后面的所有线程都无法执行该逻辑

那怎么应对这个场景?

上述场景是由于发生异常造成没有执行释放锁,那么可以用try-finally机制,保证即使中间发生了异常,最后也能够执行释放锁这一步

@RequestMapping("/lock4")
public String RedisDistributeLock01(User user){
    String lockKey = "product_001";

    //分布式锁,setIfAbsent是对setnx的封装,setnx成功返回true,否则返回false
    try{
        Boolean LockSuccess = redisTemplate.opsForValue().setIfAbsent(lockKey, user.getId());
        if(!LockSuccess){
            return "error";
        }
        
        int stock = redisTemplate.opsForValue().get("stock");
        if(stock < 0){
            int remainStock = stock - 1;
            redisTemplate.opsForValue().set("stock",remainStock);
            System.out.println("扣减成功,剩余库存:"+remainStock);
        }else{
            System.out.println("扣减失败,库存不足");
        }
    }finally {
        //释放分布式锁:把lockKey删掉
        redisTemplate.delete(lockKey);
    }
    return "index";
}

 

假如执行过程中是web程序挂了,而不是出现异常呢?在执行try代码块的时候程序挂了是无法执行finally代码块的,那该怎么办?

redis有个key过期机制,给lockKey设置一个过期值,若干秒之后lockKey会失效,只要redis不挂,那么都能正常释放锁

但即使设置了过期时间也仍然会存在风险:

加锁和设置过期时间为两步操作,并非原子操作,有可能加了锁之后,程序就崩溃了,没有执行到设置过期时间的这一步,那么同样会发生错误

@RequestMapping("/lock4")
public String RedisDistributeLock01(User user){
    String lockKey = "product_001";
    
    try{
        //分布式锁,setIfAbsent是对setnx的封装,setnx成功返回true,否则返回false
        Boolean LockSuccess = redisTemplate.opsForValue().setIfAbsent(lockKey, user.getId());

        //锁加完但是还没设置过期时间就宕机了,怎么办

        redisTemplate.expire(lockKey,10, TimeUnit.SECONDS);//设置过期时间
        

        if(!LockSuccess){
            return "error";
        }

        int stock = redisTemplate.opsForValue().get("stock");
        if(stock < 0){
            int remainStock = stock - 1;
            redisTemplate.opsForValue().set("stock",remainStock);
            System.out.println("扣减成功,剩余库存:"+remainStock);
        }else{
            System.out.println("扣减失败,库存不足");
        }
    }finally {
        //释放分布式锁:把lockKey删掉
        redisTemplate.delete(lockKey);
    }
    return "index";
}

怎么解决?

使用原子性的api

@RequestMapping("/lock4")
public String RedisDistributeLock01(User user){
    String lockKey = "product_001";
    
    try{
        //分布式锁,setIfAbsent是对setnx的封装,setnx成功返回true,否则返回false
        //设置锁的同时设置过期时间,原子性的api
        Boolean LockSuccess = redisTemplate.opsForValue().setIfAbsent(lockKey, user.getId(), 10, TimeUnit.SECONDS);        

        if(!LockSuccess){
            return "error";
        }

        int stock = redisTemplate.opsForValue().get("stock");
        if(stock < 0){
            int remainStock = stock - 1;
            redisTemplate.opsForValue().set("stock",remainStock);
            System.out.println("扣减成功,剩余库存:"+remainStock);
        }else{
            System.out.println("扣减失败,库存不足");
        }
    }finally {
        //释放分布式锁:把lockKey删掉
        redisTemplate.delete(lockKey);
    }
    return "index";
}

 

那用原子性的操作在加锁的同时设置过期时间,是不是就真的没有问题了?

这里面还涉及到删除了不属于自己的锁的情况

场景:线程A执行业务在某些特殊情况下需要可能需要15s,但是业务代码默认设置锁过期时间为10s,线程A执行到第10s的时候,锁过期了,这时候线程B来了,线程B可能要执行8s,当线程B执行到第5s的时候,线程A里面的业务执行到15s,删除了锁,但此时线程B还没有执行完,锁就没有了,以至于后面可能还有线程C发现没有上锁,加锁后又被线程B把自己加的锁删了,出现了线程删除不是自己加的锁的情况,这把锁就会永久失效

 

怎么解决线程误删锁的情况?

上面的问题的本质是线程删除了一个其他线程加的锁,因为线程没有判断该锁是不是自己加的,这里的是解决方案的切入点,当一个线程加锁时,setnx 的value拼接上能够唯一标识该线程的的数据,key为商品Id,value能够唯一标识,然后每个线程在释放锁的时候需要判断该锁是不是自己加的

以上是自己通过看视频,博客总结的一些知识点,关于redis实现分布式锁这个知识点还有很多,在实际场景中要比上面写的更加复杂,自己总结的也只是一些皮毛。

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值