Redis实现分布式锁

分布式锁

随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!

分布式锁主流的实现方案:

  1. 基于数据库实现分布式锁

  2. 基于缓存(Redis等)

  3. 基于Zookeeper

每一种分布式锁解决方案都有各自的优缺点:

  • 性能:redis最高

  • 可靠性:zookeeper最高

使用redis实现分布式锁

其实,redis实现分布式锁就是基于 setnx、delexpire 三个指令实现。其中,setnx 用于上锁,del 用于释放锁、expire 用于设置锁的过期时间。

但使用这种方式实现分布式锁不具有原子性,可能会出现刚上完锁,刚准备设置锁的过期时间但却又还没设置的时候,客户端因为某些原因挂了,这时上的锁就变成了一个死锁,别的客户端无法再拿到这个锁。

所以可以使用 set <key> <value> nx ex <seconds> 指令来实现分布式锁,使用该指令会在上锁的同时设置锁的过期时间,也就将两个指令合并为一个指令,变成一个原子操作,就不会出现死锁的问题。

image-20220917135032134

Jedis实现分布式锁

在这里我们使用Springboot整合Redis来操作,代码如下:

package cn.pigman.redistransactiondemo.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.UUID;
import java.util.concurrent.TimeUnit;

@RestController
@RequestMapping("/testRedis")
public class JedisController {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @GetMapping("/testLock")
    public void testLock() {
        String uuid = UUID.randomUUID().toString();
        //1获取锁,setne
        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid, 10, TimeUnit.SECONDS);
        //2获取锁成功、查询num的值
        if(lock){
            Object value = stringRedisTemplate.opsForValue().get("num");
            //2.1判断num为空return
            if(value == null){
                return;
            }
            //2.2有值就转成成int
            int num = Integer.parseInt(value+"");
            //2.3把redis的num加1
            stringRedisTemplate.opsForValue().set("num", String.valueOf(++num));
            //2.4释放锁,del
            //判断当前uuid和锁的uuid是否相同
            String lockUUID = (String) stringRedisTemplate.opsForValue().get("lock");
            if (lockUUID.equals(uuid)) {
                //如果相同,再释放锁
                stringRedisTemplate.delete("lock");
            }

        }else{
            //3获取锁失败、每隔0.1秒再获取
            try {
                Thread.sleep(100);
                testLock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

Lua解决原子操作问题

在前面我们使用Jedis删除锁的时候,其实存在一个问题:就是无法保证删除锁操作的原子性,可能会造成客户端A在快要执行删除锁操作但却还没执行的时候,锁因为前面设置的过期时间自动释放了,而锁释放了之后被客户端B抢到锁了,但客户端B此时却又执行了删除锁的操作,就会把客户端B加的锁给删除了。在这里,我们可以通过将删除代码写在Lua脚本中的方式来解决该问题,Redis会将Lua脚本作为一个整体进行执行,Lua脚本的执行过程不会被其他请求打断。

代码如下:

@GetMapping("testLockLua")
public void testLockLua() {
    //1 声明一个uuid ,将做为一个value 放入我们的key所对应的值中
    String uuid = UUID.randomUUID().toString();
    //2 定义一个锁:lua 脚本可以使用同一把锁,来实现删除!
    String skuId = "25"; // 访问skuId 为25号的商品 100008348542
    String locKey = "lock:" + skuId; // 锁住的是每个商品的数据

    // 3 获取锁
    Boolean lock = redisTemplate.opsForValue().setIfAbsent(locKey, uuid, 3, TimeUnit.SECONDS);

    // 第一种: lock 与过期时间中间不写任何的代码。
    // redisTemplate.expire("lock",10, TimeUnit.SECONDS);//设置过期时间
    // 如果true
    if (lock) {
        // 执行的业务逻辑开始
        // 获取缓存中的num 数据
        Object value = redisTemplate.opsForValue().get("num");
        // 如果是空直接返回
        if (StringUtils.isEmpty(value)) {
            return;
        }
        // 不是空 如果说在这出现了异常! 那么delete 就删除失败! 也就是说锁永远存在!
        int num = Integer.parseInt(value + "");
        // 使num 每次+1 放入缓存
        redisTemplate.opsForValue().set("num", String.valueOf(++num));
        /*使用lua脚本来锁*/
        // 定义lua 脚本
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        // 使用redis执行lua执行
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptText(script);
        // 设置一下返回值类型 为Long
        // 因为删除判断的时候,返回的0,给其封装为数据类型。如果不封装那么默认返回String 类型,
        // 那么返回字符串与0 会有发生错误。
        redisScript.setResultType(Long.class);
        // 第一个要是script 脚本 ,第二个需要判断的key,第三个就是key所对应的值。
        redisTemplate.execute(redisScript, Arrays.asList(locKey), uuid);
    } else {
        // 其他线程等待
        try {
            // 睡眠
            Thread.sleep(1000);
            // 睡醒了之后,调用方法。
            testLockLua();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

总结

为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件

  • 互斥性。在任意时刻,只有一个客户端能持有锁。

  • 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。

  • 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

  • 加锁和解锁必须具有原子性。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小嵌_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值