Redis分布式锁

分布式锁

概念

对某个操作上了锁后,这个锁对所有节点都有效,也就是保证所有节点互斥访问。

传统的锁只对自己的节点有效。


基于Redis实现分布式锁

原理

使用Redis的setnx命令,setnx在设置键值对的时候,先判断键值对是否存在,存在返回0,不存在返回1,根据这个原理,需要加锁的时候使用setnx设置一个lock,在解锁之前,其他人上锁lock会返回0,表示上锁失败,也就是这个锁已经存在。在解锁的时候使用del删除这个锁即可。这就达成了加锁解锁的目的。

产生的问题

如果上锁后,上锁的服务器挂掉了,其他服务器就没办法解锁,就导致死锁。

解决方案

上锁后,再设置锁的过期时间,过了期限这个锁自动失效。

使用setnx和expire命令。

setnx <key> <value>

expire <key> <seconds>


方案的优化——原子操作

需要优化原因

这涉及到原子操作,若上锁操作执行后,还没设置过期时间,此时服务器挂掉了,也会导致死锁。

所以需要上锁的同时设置过期时间。

解决方案

可以使用命令set <key><value>nx ex <time>


方案的继续优化——UUID防止误删

需要优化原因

假设A上锁后,做了操作,然后服务器卡顿了,A的锁过期后A还没反应过来。

此时B上锁做操作,还没等到B解锁,此时A反应过来进行解锁,这时候解锁解的是B的锁。

 

解决方案

使用uuid表示不同的操作,首先做操作的时候自己随机生成一个自己的uuid

set lock uuid nx ex 10

释放锁的时候,首先判断自己的uuid和要释放锁的uuid是否一致

代码

@RestController
@RequestMapping("/redisTest")
public class RedisTestController {
    @Autowired
    private RedisTemplate redisTemplate;

    @GetMapping("testLock")
    public void testLock(){
        String uuid = UUID.randomUUID().toString();  //随机生成当前操作的uuid
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock",uuid,3, TimeUnit.SECONDS); //上锁,lock保存返回信息
        if(lock){  //如果上锁成功

//----------------------对数据操作------------------------------------------
            Object value = redisTemplate.opsForValue().get("num"); 
            if(StringUtils.isEmpty((String)value)) return; 
            int num = Integer.parseInt(value+"");     
            redisTemplate.opsForValue().set("num",++num);

//----------------------开始判定自己的uuid与锁的uuid是否一致---------------------------------------
          String lockUuid=  (String) redisTemplate.opsForValue().get("lock");
          if(lockUuid.equals(uuid))         //如果uuid一致,则解锁
            redisTemplate.delete("lock");
        }
        else{  //如果上锁失败,则等一等再试着上锁
            try {
                Thread.sleep(100);
                testLock();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}


方案最终优化——LUA保证删除的原子性

需要优化原因

A上锁,做了具体操作,在正准备删除的时候(比较了uuid,且一致),A刚好过期,此时B上锁,A再删除时删除的是B的锁。

解决方案

可以使用LUA脚本保证删除操作的原子性

代码

@RestController
@RequestMapping("/redisTest")
public class RedisTestController {
    @Autowired
    private RedisTemplate redisTemplate;

    @GetMapping("testLock")
    public void testLock(){
        String uuid = UUID.randomUUID().toString(); //随机生成uuid
        String skuId= "25";  
        String locKey = "lock:"+skuId;

        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock",uuid,3, TimeUnit.SECONDS);
        if(lock){

//-----------------------------对数据操作--------------------------------------
            Object value = redisTemplate.opsForValue().get("num");
            if(StringUtils.isEmpty((String)value)) return;
            int num = Integer.parseInt(value+"");
            redisTemplate.opsForValue().set("num",++num);

//------------------------------删除操作--------------------------------------
            String script="if redis.call('get',KEYS[1]) == ARGV[1] " +
                    "then return redis.call('del',KEYS[1]) else return 0 end";   //脚本内容

            DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
            redisScript.setScriptText(script);   //加载脚本
            redisScript.setResultType(Long.class);
            redisTemplate.execute(redisScript, Arrays.asList(locKey),uuid);  //执行
        }
        else{
            try {
                Thread.sleep(100);
                testLock();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}

posted @ 2022-05-10 20:27  Laplace蒜子  阅读(63)  评论(0)    收藏  举报