Redis分布式锁的背景以及解决方案

为什么需要分布式锁

线程安全问题:多个线程同时共享一个全局变量,或者静态变量, 进行写的操作时, 可能会发生数据的冲突问题 ,也就是线程安全问题。
在单机服务架构中,我们可以使用同步机制, 使得在同一时间只能有一个线程修改共享数据,例如对代码块、方法等加锁,实现多线程触发写的操作时,只有一个线程能够进入指定区域实现写操作,但在分布式架构下,这样的处理方式是不能实现跨越JVM的锁机制,这时候,就需要一个方案来解决分布式架构下的锁问题。
要解决这个问题,就会出现与单机服务架构相似的加锁问题,在哪加锁,如何确定锁对象,以及解决分布式下出现的各种新的问题,例如说解决分布式下的非原子性操作等。

单机服务架构下的锁

特点:只支持单机服务下的并发访问安全问题
下面的例子,对num进行+1的操作,进行ab压测工具测试,5000次请求,修改结果235

@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
    public synchronized void testLock() {
        String value = this.stringRedisTemplate.opsForValue().get("num");
        if (StringUtils.isBlank(value)) {
            return;
        }
        // 有值就转成成int
        int num = Integer.parseInt(value);
        // 把redis中的num值+1
        this.stringRedisTemplate.opsForValue().set("num", String.valueOf(++num));
    }

synchronized的安全控制由JVM进程进行管理,速度快,但在集群下有安全问题
解决思想:跨越JVM的锁机制

由单机服务向转向集群服务

利用Redis的分布式锁机制,先上代码,再说代码细节问题

@Override
    public void testLock() {
        //设置唯一UUID,释放锁时 只能释放自己定义好的UUID
        String uuid = UUID.randomUUID().toString().replaceAll("-", "");
        //并设置超时时间
        Boolean lock = stringRedisTemplate.opsForValue()
                .setIfAbsent("lock", uuid, 7, TimeUnit.SECONDS);
        if (lock) {
            String value = stringRedisTemplate.opsForValue().get("num");
            if (StringUtils.isBlank(value)) {
                return;
            }
            // 有值就转成成int
            int num = Integer.parseInt(value);
            // 把redis中的num值+1
            stringRedisTemplate.opsForValue().set("num", String.valueOf(++num));

            String lua = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
                    "then\n" +
                    "    return redis.call(\"del\",KEYS[1])\n" +
                    "else\n" +
                    "    return 0\n" +
                    "end";
            DefaultRedisScript<Long> defaultRedisScript = new DefaultRedisScript<>();
            defaultRedisScript.setScriptText(lua);
            defaultRedisScript.setResultType(Long.class);
            stringRedisTemplate.execute(defaultRedisScript, Arrays.asList("lock"), uuid);
            //不是原子性操作
			// if (uuid.equals(stringRedisTemplate.opsForValue().get("lock"))) {
            //       stringRedisTemplate.delete("lock");
            //      }
        } else {
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
          testLock();//自旋
        }
    }
1.为什么要设置redis的超时时间
 Boolean lock = stringRedisTemplate.opsForValue()
                .setIfAbsent("lock", uuid, 7, TimeUnit.SECONDS);

setIfAbsent参数:key,value,过期时间,时间单位
setIfAbsent方法
作用1:预防网络抖动
客户端A中的一个线程获取到了锁,然后执行finally中的释放锁的代码时,这时候网络出问题了,导致客户端A没有成功释放锁。此时对于redis服务端来说,它会一直把锁给客户端A,这样的话其他客户端自然也就不能获取到这个锁。
作用2:redis宕机
客户端A获取到了锁,Redis服务器突然宕机,锁没有释放。等到Redis再次恢复的时候,Redis服务端还会把锁给到客户端A,这样也会发生锁死的情况。

2. 为什么要引入uuid?

我们先忽略uuid的以及lua脚本的代码,从if (lock)加锁开始到最后释放锁对象,有两种情况
情况1:在自动释放锁之前,任务执行完毕,由自己本身去释放锁
情况2:在自动释放锁之前,任务没有执行完毕,自动释放锁对象,等到任务执行完毕之后,调用del释放锁,这时释放的是并不是自己的锁,而是其他业务的锁,导致别人的锁被释放,出现没有上锁的情况
所以,我们要保证每次释放锁的时候,都是与自己匹配的锁对象,这时候uuid就是作为一个验证锁的身份对象

3. 为什么以下代码不是原子性操作?
if (uuid.equals(stringRedisTemplate.opsForValue().get("lock"))) {
                    stringRedisTemplate.delete("lock");
                   }

1.业务逻辑1执行删除时,查询到的lock值确实和uuid相等
uuid=v1
2.业务逻辑1执行删除前,lock刚好过期时间已到,被redis自动释放
在redis中没有了lock,没有了锁。
3.业务逻辑2获取了lock
index2线程获取到了cpu的资源,开始执行方法
uuid=v2
set(lock,uuid);
4.业务逻辑执行删除,此时会把业务逻辑2的lock删除
删除的业务逻辑2的锁
为什么以下代码不是原子性操作

4.lua脚本的使用解决原子性问题

不使用固定的字符串作为键的值,而是设置一个不可猜测(non-guessable)的长随机字符串,作为口令串(token)。
不使用 DEL 命令来释放锁,而是发送一个 Lua 脚本,这个脚本只在客户端传入的值和键的口令串相匹配时,才对键进行删除。
大家可以参考文档Redis使用文档来查看具体的使用方式

public <T> T execute(RedisScript<T> script, List<K> keys, Object... args) {
        return this.scriptExecutor.execute(script, keys, args);
    }

stringRedisTemplate.execute执行需要以上几个参数
1.script,lua脚本,类型为RedisScript
RedisScript下有一个对应的DefaultRedisScript实现类,里面有两个参数,一个是脚本setScriptText,一个是返回类型setResultType(这里好像用不到,但是这里没有测试可不可以不传)
RedisScript
2.keys释放的key List类型
3.args可变参数的uuid,… 代表可变参数

  String lua = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
                   "then\n" +
                   "    return redis.call(\"del\",KEYS[1])\n" +
                   "else\n" +
                   "    return 0\n" +
                   "end";
           DefaultRedisScript<Long> defaultRedisScript = new DefaultRedisScript<>();
           defaultRedisScript.setScriptText(lua);
           defaultRedisScript.setResultType(Long.class);
           stringRedisTemplate.execute(defaultRedisScript, Arrays.asList("lock"), uuid);
5.使用redission优化
public void testLock() {
        String s = "user:" + "1314" + ":Info";
        RLock lock = redissonClient.getLock(s);
        boolean tryLock = false;
        try {
            tryLock = lock.tryLock(101, 10, TimeUnit.SECONDS);
            if (tryLock) {
                String value = this.stringRedisTemplate.opsForValue().get("num");
                if (StringUtils.isBlank(value)) {
                    return;
                }
                int num = Integer.parseInt(value);
                this.stringRedisTemplate.opsForValue().set("num", String.valueOf(++num));
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        lock.unlock();
    }

Redisson把获取锁+设置等待时间+设置过期时间、释放锁的各种操作封装成原子操作,并提供了自动续期的功能,此外还提供了可重入锁的功能。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值