redis分布式锁

面试反馈

 1、redis除了缓存外其他用法

轻量级消息队列,点赞,抽奖,签到打卡,可能认识的人,附近的酒店,购物车,热点新闻、分布式锁,统计日活月活。

一、锁的种类

synchronized和lock是单机版JVM(即在同一个虚拟机上)使用的锁,对于分布式架构(存在多个不同的虚拟机)单机版的锁线程机制不再起作用

(1)折中的方案:使用setnx

多个订单模块去抢库存模块,这里就需要用到锁,setnx就是在库存模块和订单,模块之间加一个redis,在这个redis上执行setnx尝试获取锁 

 简单的方法

//使用redislock
setnx  redislock 1

//使用完后归还redislock
del redislock

存在的问题:不高可用,存在死锁情况, 可能存在“乱抢”现象

(2)设计锁的要求

独占性:onlyOne,任何时刻只有一个线程可以持有这把锁

高可用:在redis集群环境下,不能因为某一个节点挂了而出现获取锁和释放锁失败的情况;高并发情况下,性能依然良好。

防死锁:必须有个兜底的方案跳出死锁(如超时控制机制和撤销操作)

不乱抢:一个线程不能去释放别人的锁。

线程A获得锁A后,设置了过期时间10s,如果线程A出现了故障,10s内没有完成任务释放锁A,10s时锁过期失效被删除,第11s线程B又获得了锁A并设置过期时间,而此时线程A又完成了操作把锁A删除了,等线程B完成操作想要释放锁A,这时候发现锁A没有了,这样就会出现问题。

可重入性:同一个线程可以多次获得同一把锁 

(3)业务实现

@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String port;
private Lock lock = new ReentrantLock();    
public string sale{
    String retMessage="";
    lock.lock();
    try{
        //查询库存数量
        String result = stringRedisTemplate.opsForValue.get("inventory001");
        //数据类型转换
        Integer inventoryNumber = result==null ? 0 : Integer.parseInt(result);
        //根据库存执行业务
        if(inventoryNumber>0){
            stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber));
            retMessage = "成功卖出一个商品,库存剩余:"+inventoryNumber;
            System.out.println(resMessage+"\t"+"服务端口号"+port);
        }else{
            retMessage="商品卖完了";
        }
    }finally{
        lock.unlock();
    }
    
    return resMessage + "\t" + "服务端口号" + port;
}

二、分布式锁实现

(1)使用setnx实现分布式锁,保证不超卖(使用JMeter实现压测)

public String sale(){
    String retMessage = "";
    String key = "zzyyRedisLock";
    String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();

    Boolean flag = string RedisTemplate.opsForValue().setIfAbsent(key, uuidValue);
    if(!flag){
        //不断尝试
        try{TimeUnit.MILLISECONDS.sleep(20);}catch(InterruptedException e){
            e.printStackTrace();
            sale();
        }
    }else{
        try{
        //强锁成功后进行的业务代码
        }finally{
        //完成业务后一定要释放锁
        stringRedisTemplate.delete(key);
    }
    }
}

存在的问题:

①高并发下禁止用递归,堆和栈本身容易溢出,用递归容易导致stackoverflow

②使用if判断在高并发环境下可能会导致虚假唤醒,应用while替换if,达到自旋的效果

(2)用while优化,实现高可用

public String sale(){
    String retMessage = "";
    String key = "zzyyRedisLock";
    String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();

    //用while替换if,用自旋替换递归
    while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue)){
        //不断尝试
        try{TimeUnit.MILLISECONDS.sleep(20);}catch(InterruptedException e){
            e.printStackTrace();
        }
    }
    try{
        //强锁成功后进行的业务代码
    }finally{
        //完成业务后一定要释放锁
        stringRedisTemplate.delete(key);
    }

}

 存在的问题:setIfAbsent里的key没有加过期时间。如果A建成功了,但微服务挂掉,即代码运行到业务代码且finally里的内容没运行,不设置过期时间的key一直存在,其他的线程或者客户就无法获得这把锁,造成死锁现象。

(3)加入过期时间来优化,实现防死锁

public String sale(){
    String retMessage = "";
    String key = "zzyyRedisLock";
    String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();

    //加锁和设置过期时间必须同时进行保证原子性
    while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue,30,TimeUnit.SECONDS)){
        //不断尝试
        try{TimeUnit.MILLISECONDS.sleep(20);}catch(InterruptedException e){
            e.printStackTrace();
        }
    }
    //stringRedisTemplate(key, 30,TimeUnit.SECONDS);
    try{
        //强锁成功后进行的业务代码
    }finally{
        //完成业务后一定要释放锁
        stringRedisTemplate.delete(key);
    }

}

存在的问题:线程A的第30秒key过期,第31秒线程B新建同名的key,第33秒线程A误删线程B建的key,线程B执行完,就发现要删的key没有了。

(4)添加限制,线程只能删除自己建的锁,不能删除别人的

public String sale(){
    String retMessage = "";
    String key = "zzyyRedisLock";
    String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();

    //加锁和设置过期时间必须同时进行保证原子性
    while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue,30,TimeUnit.SECONDS)){
        //不断尝试
        try{TimeUnit.MILLISECONDS.sleep(20);}catch(InterruptedException e){
            e.printStackTrace();
        }
    }
    //stringRedisTemplate(key, 30,TimeUnit.SECONDS);
    try{
        //强锁成功后进行的业务代码
    }finally{
        if(stringRedisTemplate.opsForValue().get(key).equalsIgnoreCase(uuidValue)){
            //完成业务后一定要释放锁
            stringRedisTemplate.delete(key);
        }
    }

}

存在问题:先if判断key的id,然后删除,这不是原子操作,仍然可能导致误删别人的key

解决方法:使用lua脚本

redis调用lua通过eval命令保证代码执行的原子性,直接return返回执行后的结果

eval luascript numkeys key1 key2 ... arg1 arg2 ...

luascript指定脚本的内容,在脚本里如果想调用redis命令,就要使用redis,call(),在脚本里使用return返回脚本的返回值给redis

 双引号内要用单引号

除了最后一个业务分支外都要有then,if语句要用end作为结尾 

(5)用lua实现判断删除的原子性(执行lua脚本的过程是原子的),实现不乱删

public String sale(){
    String retMessage = "";
    String key = "zzyyRedisLock";
    String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();

    //加锁和设置过期时间必须同时进行保证原子性
    while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue,30,TimeUnit.SECONDS)){
        //不断尝试
        try{TimeUnit.MILLISECONDS.sleep(20);}catch(InterruptedException e){
            e.printStackTrace();
        }
    }
    //stringRedisTemplate(key, 30,TimeUnit.SECONDS);
    try{
        //强锁成功后进行的业务代码
    }finally{
        String luascript = "if redis.call('get',KEYS[1])==ARGV[1] then"+
                            "return redis.call('del', KEYS[1])" + 
                              "else" + 
                            "return 0" + 
                               "end";
        //DefaultRedisScript构造器的第二个参数要执行返回值类型,不添加会报错
        //execute()的第二个参数指定key列表,第三个指定val参数值
        stringRedisTemplate.execute(new DefaultRedisScript(luascript, Boolean.class), Arrays.asList(key),uuidValue);
    }
    return retMessage
}

 使用JMeter进行并发测试

存在的问题:锁不具有可重入性

public String sale(){
    String retMessage = "";
    String key = "zzyyRedisLock";
    String uuidValue = IdUtil.simpleUUID()+":"+Thread.currentThread().getId();

    while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue,30,TimeUnit.SECONDS)){
        try{TimeUnit.MILLISECONDS.sleep(20);}catch(InterruptedException e){
            e.printStackTrace();
        }
    }
    //stringRedisTemplate(key, 30,TimeUnit.SECONDS);
    try{
        //业务代码


        //此处如果调用了某个方法,而这个方法又需要访问redis的某个key作为锁,
        //如果不做额外处理,就会导致死锁,因为该key已经被外层方法获取了


    }finally{
        String luascript = "if redis.call('get',KEYS[1])==ARGV[1] then"+
                            "return redis.call('del', KEYS[1])" + 
                              "else" + 
                            "return 0" + 
                               "end";
        stringRedisTemplate.execute(new DefaultRedisScript(luascript, Boolean.class), Arrays.asList(key),uuidValue);
    }
    return retMessage
}

(6)实现可重入

JUC里的reentrantlock和synchronized都是可重入锁,使用reentranlock时lock几次就要unlock几次

详情参考:

 

为了记录每个线程对一把锁的重入次数,在redis中可以使用hash结构来记录

if redis.call('exists', 'key') == 0 then
    redis.call('hset','key','uuid:threadid',1)
    redis.call('expire','key',50)
    return 1
elseif redis.call('hexists','key','uid:thread') == 1 then
    redis.call('hincrby','key','uuid:threadid',1)
    redis.call('expire','key',50)
    return 1
else
    return 0
end
//用hincrby替换hset
if redis.call('exists', 'key') == 0 or redis.call('hexists','key','uid:thread') == 1 then
    redis.call('hincrby','key','uuid:threadid',1)
    redis.call('expire','key',50)
    return 1
else
    return 0
end
//动态传参
if redis.call('exists',KEYS[1]) == 0 or if redis.call('hexists',KEYS[1],ARGV[1]) == 1 then
    redis.call('hincrby',KEYS[1],ARGV[1],1)
    redis.call('expire',KEYS[1],ARGV[2])
    return 1
else
    return 0
end

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值