Redis分布式锁的实现

在使用Redis实现分布式锁的时候,通常向Redis插入一条key-value数据,key为需要上锁的资源,value可以存放使用该资源的用户等信息。

1.方案一

首先传入需要被锁的资源id和当前操作的用户userId,先判断当前Redis中是否有key为id的数据,如果存在,直接返回false代表该资源已经被人使用;如果不存在再插入键为id,值为userId,有效时间30s的数据,并且返回true。

/**
* 用户进入获取某个key的锁
* 返回false说明获取失败,返回true说明成功
* @return
*/
@RequestMapping("getLock1")
@ResponseBody
public boolean getLock1(Integer id, String userId) {
    //如果不存在key为id的数据,则返回false
    if (stringRedisTemplate.opsForValue().get(id.toString()) != null) {
        return false;
    }
    //在redis中插入一条键为id,值为userId,有效时间30s的数据
    stringRedisTemplate.opsForValue().set(id.toString(), userId, 30, TimeUnit.SECONDS);
    return true;
}

该方法其实存在漏洞,比如当两个用户同时进入抢占同一资源,在同一时间查询到Redis中不存在键为id的数据,即认为没有人在使用当前资源,所以都去set数据,而set是可以覆盖的,导致两个用户都看起来上锁成功了,都会返回true。

模拟一下这种情况:

public class Test implements Runnable{
    public static ConcurrentLinkedQueue<String>  stateQuene= new ConcurrentLinkedQueue<>();
    public static void main(String[] args) {
        stateQuene.add("user1");
        stateQuene.add("user2");
        Test test = new Test();
        Thread thread1 = new Thread(test);
        Thread thread2 = new Thread(test);
        thread1.start();
        thread2.start();
    }

    @Override
    public void run() {
        String userId = stateQuene.poll();
        String result = HttpClientUtil.doGet("http://localhost:8080/getLock1?userId="+ userId +"&id=1");
        System.out.println(result);
    }
}

在以上代码中为了模拟两个用户,创建了user1和user2,并且放入到ConcurrentLinkedQueue中,在线程中使用poll()方法逐个取出。
执行结果:
均返回true,两个线程都拿到了锁
输出两个true,说明都拿到了锁,解决方法见方案二

2.方案二

Redis本身在set数据时有一个方法是如果不存在才能插入成功,否则会插入失败,查阅stringRedisTemplate的API发现有一个setIfAbsent()方法,修改后的代码:

@RequestMapping("getLock2")
@ResponseBody
public boolean getLock2(Integer id, String userId) {
    //如果不存在key为id的数据,则返回false
    if (stringRedisTemplate.opsForValue().get(id.toString()) != null) {
        return false;
    }
    //在redis中插入一条键为id,值为userId,有效时间30s的数据,使用setIfAbsent可以在不存在该key的情况下完成插入
    boolean res = stringRedisTemplate.opsForValue().setIfAbsent(id.toString(), userId, 30, TimeUnit.SECONDS);
    return res;
}

测试结果:
在这里插入图片描述
在拿到锁真正执行逻辑代码时,一般会在执行结束后将Redis中的数据进行删除,达到释放锁的目的,详见方案三。

3.方案三

我们在操作之前先调用方案二中的getLock方法。成功后,再执行逻辑代码,我使用sleep()模拟逻辑代码操作所需要的时间,在finally代码块中将锁释放。

@RequestMapping("option1")
@ResponseBody
public void option1(Integer id, String userId) {
    //获取锁
    boolean flag = getLock2(id, userId);
    if (!flag){
        System.out.println("用户"+ userId +"获取"+ id +"锁失败");
    }else {
        System.out.println("用户"+ userId +"获取"+ id +"锁成功");
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    //模拟操作过程需要10秒
                    for (int i = 10; i > 0; i--){
                        Thread.sleep(1000);
                        System.out.println("锁剩余时间"+ i +"秒");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    if (stringRedisTemplate.delete(id.toString())){
                        System.out.println("用户"+ userId +"释放"+ id +"锁成功");
                    }else {
                        System.out.println("用户"+ userId +"释放"+ id +"锁成功");
                    }
                }
            }
        }).start();
    }
}

使用上面的多线程测试工具,将访问路径替换为option1,访问后控制台打印如下:
在这里插入图片描述

其实这样做还有问题,因为真正逻辑进行操作时不一定是10s,有可能是20s,30s,甚至是超过开始设置的redis有效时间(上面设置了30s),所以就存在逻辑代码运行的时间超过了有效时间,自动释放了锁,其他人在此时就可以拿到锁,而在逻辑代码运行完之后又执行了finally代码块,把别人的锁释放掉了,解决方案见方案四。

4.方案四

在加锁的时候设置随机值,并存放到redis中,释放锁的时候匹配到该随机值才可以释放锁。要加入随机值的话,redis的value使用hash格式进行存储较为合理,所以修改后的getLock()方法如下:

public boolean getLock3(Integer id, String userId, String radom) {
    //如果不存在key为id的数据,则返回false
    if (stringRedisTemplate.opsForValue().get(id.toString()) != null) {
        return false;
    }
    //在redis中插入一条键为id,值为userId和随机值组成的hash,有效时间30s的数据,使用putIfAbsent可以在不存在该key的情况下完成插入
    if (stringRedisTemplate.opsForHash().putIfAbsent(id.toString(), "userId", userId)){
        stringRedisTemplate.opsForHash().putIfAbsent(id.toString(), "radom", radom);
        stringRedisTemplate.expire(id.toString(), 30, TimeUnit.SECONDS);
        return true;
    }
    return false;
}

Redis可视化工具查看效果:
在这里插入图片描述

获得锁的时候将随机值保存成变量,最后在finally中判断变量和Redis中存储的随机值是否相同,不相同的话不允许释放锁,将线程中的sleep时间设置成40s,锁自动失效时间是30s,就会发生上述的情况,修改后的代码如下:

@RequestMapping("option2")
@ResponseBody
public void option2(Integer id, String userId) {
    //获取锁
    String radom = UUID.randomUUID().toString();
    boolean flag = getLock3(id, userId, radom);
    if (!flag){
        System.out.println("用户"+ userId +"获取"+ id +"锁失败");
    }else {
        System.out.println("用户"+ userId +"获取"+ id +"锁成功");
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    //模拟操作过程需要40秒
                    for (int i = 40; i > 0; i--){
                        Thread.sleep(1000);
                        System.out.println("锁剩余时间"+ i +"秒");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    System.out.println("用户"+ userId +"尝试释放"+ id +"锁...");
                    if (radom.equals(stringRedisTemplate.opsForHash().get(id.toString(), "radom"))){
                        if (stringRedisTemplate.delete(id.toString())){
                            System.out.println("用户"+ userId +"释放"+ id +"锁成功");
                        }else {
                            System.out.println("用户"+ userId +"释放"+ id +"锁成功");
                        }
                    }else {
                        System.out.println("锁已不属于用户"+userId);
                    }
                }
            }
        }).start();
    }
}

测试之后控制台打印如下:
在这里插入图片描述

锁已经不属于当前用户,但是逻辑代码还没执行完,所以需要加一个判断来延续锁,见方案五。

5.方案五

在逻辑代码中加入每隔10s检测是否超时时间小于等于20s,是的话重新设置超时时间30s,防止锁失效,在这里借用上面倒计时代码,判断i是否是10的整数倍来实现10s判断一次

try {
	//模拟操作过程需要40秒
	for (int i = 40; i > 0; i--){
    	Thread.sleep(1000);
        System.out.println("锁剩余时间"+ i +"秒");
        //每隔十秒查询有效时间是否小于20秒
        if (i % 10 == 0 && i != 40){
        	if (stringRedisTemplate.getExpire(id.toString(), TimeUnit.SECONDS) <=20 ){
            	stringRedisTemplate.expire(id.toString(), 30, TimeUnit.SECONDS);
            }
        }
     }
}

在这里插入图片描述

锁延续成功,并且最后锁也是属于自己的,释放成功。
至此,Redis分布式锁的实现就较为完善了。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值