美文网首页
Redis的基本使用(-) 分布式锁

Redis的基本使用(-) 分布式锁

作者: 缘木与鱼 | 来源:发表于2020-10-18 18:03 被阅读0次

Redis的基本使用(-) 分布式锁

1、Redis做分布式锁

分布式锁是Redis较常见的使用场景。

问题场景:

例如一个简单的用户操作,一个线程去修改用户的状态,首先从数据库中读出用户的状态,然后
在内存中进行修改,修改完成后,再存回去。在单线程中,这个操作没有问题,但是在多线程
中,由于读取、修改、存 这是三个操作,不是原子操作,所以在多线程中,这样会出问题。

对于这种问题,我们可以使用分布式锁来限制程序的并发执行。

1.1、基本用法

分布式锁实现的思路很简单,就是进来一个线程先占位,当别的线程进来操作时,发现已经有人占位了,就会放弃或者稍后再试.

在 Redis 中,占位一般使用 setnx 指令,先进来的线程先占位,线程的操作执行完成后,再调用 del 指令释放位子。

根据上面的思路,写出的代码如下:

public class LockTest {
    public static void main(String[] args) {
        Redis redis = new Redis();
        redis.execute(jedis -> {
            // setnx 不会覆盖已存在的值,且返回0,不存在的设置成功,返回1
            Long setnx = jedis.setnx("k1", "v1");
            if (setnx == 1){
                // 没人占位
                jedis.set("name", "lucky");
                String name = jedis.get("name");
                System.out.println(name);

                // 释放资源
                jedis.del("k1");
            } else {
                // 有人占位,停止/暂缓 操作
            }
        });
    }

}

上面的代码存在一个小小问题:如果代码业务执行的过程中抛异常或者挂了,这样会导致 del 指令没有被调用,这样,

k1 无法释放,后面来的请求全部堵塞在这里,锁也永远得不到释放。

要解决这个问题,可以给锁添加一个过期时间,确保锁在一定的时间之后,能够得到释放。改进后的代码如下:

public class LockTest {
    public static void main(String[] args) {
        Redis redis = new Redis();
        redis.execute(jedis -> {
            // setnx 不会覆盖已存在的值,且返回0,不存在的设置成功,返回1
            Long setnx = jedis.setnx("k1", "v1");
            if (setnx == 1){
                // 给锁添加一个过期时间,防止应用在运行过程中抛出异常导致锁无法及时得到释放
                jedis.expire("k1", 5);

                // 没人占位
                jedis.set("name", "lucky");
                String name = jedis.get("name");
                System.out.println(name);

                // 释放资源
                jedis.del("k1");
            } else {
                // 有人占位,停止/暂缓 操作
            }
        });
    }

}

这样改造之后,还有一个问题,就是在获取锁和设置过期时间之间如果服务器突然挂掉了,这个时候锁被占用,无法

及时得到释放,也会造成死锁,因为获取锁和设置过期时间是两个操作,不具备原子性。

为了解决这个问题,从 Redis2.8 开始,setnx 和 expire 可以通过一个命令一起来执行了,对上述代码再做改进:

public class LockTest {
    public static void main(String[] args) {
        Redis redis = new Redis();
        redis.execute(jedis -> {
            String set = jedis.set("k1", "v1", new SetParams().nx().ex(5));
            if (set != null && "OK".equals(set)){
                // 给锁添加一个过期时间,防止应用在运行过程中抛出异常导致锁无法及时得到释放
                jedis.expire("k1", 5);

                // 没人占位
                jedis.set("name", "lucky");
                String name = jedis.get("name");
                System.out.println(name);

                // 释放资源
                jedis.del("k1");
            } else {
                // 有人占位,停止/暂缓 操作
            }
        });
    }

}

1.2、解决超时问题

​ 为了防止业务代码在执行的时候抛出异常,给每一个锁添加了一个超时时间,超时之后,锁会被自动释放,但是这也带来了一个新的问题:如果要执行的业务非常耗时,可能会出现紊乱。举个例子:第一个线程首先获取到锁,然后开始执行业务代码,但是业务代码比较耗时,执行了 8 秒,这样,会在第一个线程的任务还未执行成功锁就会被释放了,此时第二个线程会获取到锁开始执行,在第二个线程刚执行了 3 秒,第一个线程也执行完了,此时第一个线程会释放锁,但是注意,它释放的第二个线程的锁,释放之后,第三个线程进来。

对于这个问题,我们可以从两个角度入手:

- 尽量避免在获取锁之后,执行耗时操作。
- 可以在锁上面做文章,将锁的 value 设置为一个随机字符串,每次释放锁的时候,都去比较随机字符串是否一致,如果一致,再去释放,否则,不释放。

对于第二种方案,由于释放锁的时候,要去查看锁的 value,第二个比较 value 的值是否正确,第三步释放锁,有三个步骤,很明显三个步骤不具备原子性,为了解决这个问题,我们得引入 Lua 脚本。

Lua 脚本的优势:

  • 使用方便,Redis 中内置了对 Lua 脚本的支持。
  • Lua 脚本可以在 Redis 服务端原子的执行多个 Redis 命令。
  • 由于网络在很大程度上会影响到 Redis 性能,而使用 Lua 脚本可以让多个命令一次执行,可以有效解决网络给Redis 带来的性能问题。

在 Redis 中,使用 Lua 脚本,大致上两种思路:

  1. 提前在 Redis 服务端写好 Lua 脚本,然后在 Java 客户端去调用脚本(推荐)。

  2. 可以直接在 Java 端去写 Lua 脚本,写好之后,需要执行时,每次将脚本发送到 Redis 上去执行。

首先在 Redis 服务端创建 Lua 脚本,内容如下:

if redis.call("get",KEYS[1])==ARGV[1] then
 return redis.call("del",KEYS[1])
else
 return 0
end

本段脚本的含义:

​ 取redis的第一个key(下标从1开始,可以取多个),和 ARGV[1] 比较,如果相等删除 第一个key,如果不相等,就返回0。然后结束。

接下来,可以给 Lua 脚本求一个 SHA1 和,命令如下:

[root@localhost redis-6.0.8]# cat lua/releaseRedis.lua | src/redis-cli  -a lucky script load --pipe

操作如下:

[root@localhost redis-6.0.8]# mkdir lua
[root@localhost redis-6.0.8]# cd lua/
[root@localhost lua]# vim releaseRedis.lua
[root@localhost lua]# cd ..
[root@localhost redis-6.0.8]# cat lua/releaseRedis.lua | src/redis-cli  -a lucky script load --pipe
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
"b8059ba43af6ffe8bed3db65bac35d452f8115d8"

script load 这个命令会在 Redis 服务器中缓存 Lua 脚本,并返回脚本内容的 SHA1 校验和,然后在Java 端调用时,传入 SHA1 校验和作为参数,这样 Redis 服务端就知道执行哪个脚本了。

接下来,在 Java 端调用这个脚本:

public class LuaTest {
    public static void main(String[] args) {
        Redis redis = new Redis();
        for (int i = 0; i < 2; i ++){
            redis.execute(jedis -> {
                // 1.获取一个随机字符串
                String value = UUID.randomUUID().toString();
                // 2.获取锁
                String k1 = jedis.set("k1", value, new SetParams().nx().ex(5));
                // 3.判断是否成功拿到锁
                if (k1 != null && "OK".equals(k1)){
                    // 4.具体的业务操作
                    jedis.set("site", "www.baidu.com");
                    String site = jedis.get("site");
                    System.out.println(site);
                    // 5.释放锁 -- 第一个参数字符串为上面的 SHA1校验和
                    jedis.evalsha("b8059ba43af6ffe8bed3db65bac35d452f8115d8", Arrays.asList("k1"), Arrays.asList(value));
                } else {
                    System.out.println("没有拿到锁");
                }
            });
        }
    }

}

从结果可以看出,第一次拿到锁,第二次没有拿到锁。

另一种使用java拼接Lua的方式使用 jedis.eval() 的方法。

相关文章

网友评论

      本文标题:Redis的基本使用(-) 分布式锁

      本文链接:https://www.haomeiwen.com/subject/ummcmktx.html