Redis 做分布式锁及Lua 脚本使用笔记

Redis 做分布式锁及Lua 脚本使用

1. 基本用法

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

我们可以使用分布式锁来限制程序的并发执行

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

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

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

public class Redis {
    private JedisPool pool;

    public Redis(){
        GenericObjectPoolConfig config = new GenericObjectPoolConfig();
        //连接池最大空闲数
        config.setMaxIdle(300);
        //最大连接数
        config.setMaxTotal(1000);
        //连接最大等待时间,如果是 -1 表示没有限制
        config.setMaxWaitMillis(30000);
        //在空闲时检测有效性
        config.setTestOnBorrow(true);
        /**
         * 1.Redis 地址
         * 2.redis 端口
         * 3.连接超时时间
         * 4.密码
         */
        pool= new JedisPool(config,"192.168.1.132",6379,30000,"javaboy");

    }

    public void execute(CallwithJedis callwithJedis){
        try(Jedis jedis = pool.getResource()){
            callwithJedis.call(jedis);
        }
    }
}
public interface CallwithJedis {
    void call(Jedis jedis);
}
public class LockTest {
    public static void main(String[] args) {

        Redis redis = new Redis();
        redis.execute(jedis -> {
            String s = jedis.set("s1", "v1", new SetParams().nx().ex(5));
            if (s != null && "OK".equals(s)){
                //给锁添加一个过期时间,防止应用在运行过程中抛出异常导致锁无法及时得到释放
                jedis.expire("s1",5);
                //没人占位
                jedis.set("name","zhangsan");
                String name = jedis.get("name");
                System.out.println(name);
                jedis.del("s1");//释放资源
            }else{
                //有人占位,停止/暂缓 操作
            }
        });

    }
}

从 Redis2.8 开始,setnxexpire 可以通过一个命令一起来执行了。

2. 解决超时问题

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

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

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

2.1 Lua 脚本

对于第二种方案,由于释放锁的时候,要去查看锁的 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

调用 get 命令,KEYS[1] 表示只有1个,可以n个,下标从1开始;ARGV 除过key之外的其它参数,ARGV[1] 表示只有1个,可以n个。

接下来,可以给 Lua 脚本求一个 SHA1 和(相当于唯一标识符),命令如下:

cat lua/releasewherevalueequal.lua | redis-cli -a javaboy script load --pipe

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++) {//加for循环测试拿不到锁的情况,默认没有
            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", "拿到锁了");
                    String site = jedis.get("site");
                    System.out.println(site);
//                  jedis.eval() //lua脚本拼成字符串用此方法
                    //5.释放锁
                    jedis.evalsha("b8059ba43af6ffe8bed3db65bac35d452f8115d8",Arrays.asList("k1"), Arrays.asList(value));
                } else {
                    System.out.println("没拿到锁");
                }
            });
        }
    }
}

在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

李多肉同学

长得好看的人一般都喜欢发红包

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

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

打赏作者

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

抵扣说明:

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

余额充值