Redis实现分布式锁的三种方案

在进入正题之前要搞清楚两个问题:一是为什么需要分布式锁,二是Redis为什么能够实现分布式锁。
假设现在有一个应用部署在了三台机器上,应用的某个资源需要进行加锁控制,如果用关键字synchronized加锁能控制住么?显然是不行的,因为synchronized是线程锁,只能作用在当前的JVM里,获取的锁是各自JVM主内存上的锁资源。就好比一个房间有三个门,不惯是打开哪个门上的锁都能进入这个房间。此时就需要一个统一的入口来提供锁资源,这就是分布式锁。能作为锁还必须满足一个条件,就是操作必须是原子性的,锁本身要能保证线程安全。能实现分布式锁的有Redis、ZooKeeper、数据库等,本文只介绍Redis。

1 setNX、get、getSet组合命令

setNX:SET if Not eXists,命令在指定的 key 不存在时,为 key 设置指定的值。设置成功,返回 1;设置失败,返回 0 。
get:获取指定key的值
getSet:设置指定 key 的值,并返回 key 的旧值。
很多人可能用了setNX和expire命令,这种操作不是原子操作,如果在调用expire设置过期时间之前机器挂了,这种情况会导致锁没法释放,即死锁。

public boolean lock(String key) throws InterruptedException {
        //自旋超时时间
        int timeout = 3000;
        while(timeout-- >= 0) {
            Jedis resource = jedisPool.getResource();
            //过期时间
            long expires = System.currentTimeMillis() + 1000L;
            try {
                Long result = resource.setnx(key, String.valueOf(expires));
                //通过setnx加锁成功
                if (result.equals(1L)) {
                    return true;
                }
                //通过setnx加锁失败,使用get和getSet组合命令加锁
                else {
                    String currentExpires = resource.get(key);
                    //判断锁是否过期,如果过期重新加锁
                    if (StringUtils.isNotBlank(currentExpires) && Long.parseLong(currentExpires) < System.currentTimeMillis()) {
                        String oldExpires = resource.getSet(key, String.valueOf(expires));
                        //判断值是否已被修改
                        if (StringUtils.isNotBlank(oldExpires) && oldExpires.equals(currentExpires)) {
                            return true;
                        }
                    }
                }
            } finally {
                if (resource != null)
                    resource.close();
            }
            Thread.sleep(100L);
        }
        return false;
    }

使 f i n a l l y \color{red}{锁使用完之后,记得在finally里将锁释放。}

2 set命令

先了解几个参数
XX:只有key存在时才设置
NX:与setNX类似,只有key不存在时才设置
PX:表示过期时间的单位为毫秒
EX:表示过期时间的单位为秒

public boolean lock(String key) throws InterruptedException {
        //自旋超时时间
        int timeout = 3000;
        while(timeout-- >= 0) {
            Jedis resource = jedisPool.getResource();
            SetParams params = new SetParams();
            //设置为setnx模式
            params.nx();
            //设置过期时间,单位为毫秒
            params.px(1000);
            try {
                String result = resource.set(key, key, params);
                //返回OK表示设置成功
                if ("OK".equals(result)) {
                    return true;
                }
            } finally {
                if (resource != null)
                    resource.close();
            }
            Thread.sleep(100L);
        }
        return false;
    }

用set命令,设置key和过期时间是一个命令,所以不会出现应用层导致的死锁问题。
使 f i n a l l y \color{red}{锁使用完之后,记得在finally里将锁释放。}

3 lua脚本

lua脚本可以将一组Redis命令放在一次请求里完成,Redis会将脚本作为一个整体执行,保证了原子性

public boolean lock(String key) throws InterruptedException {
        //自旋超时时间
        int timeout = 3000;
        while(timeout-- >= 0) {
            Jedis resource = jedisPool.getResource();
            try {
                //lua脚本
                String script = "if redis.call('set', " + key + "," + key + ",'nx','px',time)=='OK' then\n" +
                        "return 'OK'\n" +
                        "else\n" +
                        "    if redis.call('get'," + key + ") == " + key + " then\n" +
                        "       if redis.call('EXPIRE'," + key + ", 1000)==1 then\n" +
                        "       return 'OK'\n" +
                        "       end\n" +
                        "    end\n" +
                        "end";
                Object result = resource.eval(script);
                if ("OK".equals(result)) {
                    return true;
                }
            } finally {
                if (resource != null)
                    resource.close();
            }
            Thread.sleep(100L);
        }
        return false;
    }

使 f i n a l l y \color{red}{锁使用完之后,记得在finally里将锁释放。}

4 附

推荐使用Redisson,该组件提供了多种分布式锁实现机制,如可重入锁(Reentrant Lock)、公平锁(Fair Lock)、读写锁(ReadWriteLock)、信号量(Semaphore) 等。Redisson的加锁和释放锁都是使用的lua脚本来实现的。

猜你喜欢

转载自blog.csdn.net/weixin_45497155/article/details/107517830