Java小强个人技术博客站点    手机版
当前位置: 首页 >> 软件 >> 基于Redis的SETNX命令实现锁

基于Redis的SETNX命令实现锁

30830 软件 | 2022-5-30

Redis是一个key-value存储系统。和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)和hash(哈希类型)。这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。在此基础上,redis支持各种不同方式的排序。与memcached一样,为了保证效率,数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。



目前在图灵课堂,黑马课堂,都看到过关于Reids实现锁的讲解,虽然最后还是转向了Redisson,但是对于普通的系统,利用其代码来实现锁,或者根据其代码来理解锁,还是非常有用的。

这里根据其代码进行整理,然后留下代码以备后期学习


定义锁接口

package com.example.springboot.tool;
/**
 * Redis锁
 */
public interface ILock {
    /**
     * 尝试获取锁
     * @return true代表获取锁成功; false代表获取锁失败
     */
    boolean tryLock();
    /**
     * 释放锁
     */
    void unlock();
}


定义锁实现的代码

package com.example.springboot.tool;
import cn.hutool.core.lang.UUID;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
/**
 * Redis锁-基于setnx的实现
 * 问题1:不可重入
 * 问题2:不可重试
 * 问题3:超时释放
 * 问题4:主从一致
 */
public class SimpleRedisLock implements ILock {
    // 业务名称,通过不同业务来指定不同的锁
    private String name;
    // 锁持有的超时时间,过期后自动释放
    private long timeoutSec;
    // 锁超时时间单位
    private TimeUnit unit;
    private StringRedisTemplate stringRedisTemplate;
    public SimpleRedisLock(String name, long timeoutSec, TimeUnit unit, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.timeoutSec = timeoutSec;
        this.unit = unit;
        this.stringRedisTemplate = stringRedisTemplate;
    }
    // 锁前缀
    private static final String KEY_PREFIX = "LOCK:";
    // 锁内存储的值,UUID防止分布式时出现ID一样的Thread
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
    // 释放锁的LUA脚本,之所以要使用脚本,是保证判断和删除处于一个原子操作之内,否则Thread1判断之后产生JVM堵塞,删除暂时没有执行
    // 但是已经超时锁被释放,此时Thread2已经开始执行,Thread1堵塞之后因为已经判断过,所以直接删除此时删除的就是Thread2的锁
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    static {
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        // 脚本unlock.lua放到resources下
//        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        // 比较线程标示与锁中的标示是否一致,如果不一致,说明不是自己的锁,自己的锁已经超时被释放,无需操作
        UNLOCK_SCRIPT.setScriptText("" +
                " if(redis.call('get', KEYS[1]) ==  ARGV[1]) then " +
                "    return redis.call('del', KEYS[1]) " +
                " end " +
                " return 0 ");
        UNLOCK_SCRIPT.setResultType(Long.class);
    }
    @Override
    public boolean tryLock() {
        // 获取线程标示
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        // 获取锁
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, unit);
        return Boolean.TRUE.equals(success);
    }
    @Override
    public void unlock() {
        // 调用lua脚本
        stringRedisTemplate.execute(
                UNLOCK_SCRIPT, Collections.singletonList(KEY_PREFIX + name),
                ID_PREFIX + Thread.currentThread().getId());
    }
}


这里使用到LUA脚本,这里直接把脚本写到了代码中方便理解,也可以把脚本写到文件,然后放到工程中

脚本文件,unlock.lua

-- 比较线程标示与锁中的标示是否一致
if(redis.call('get', KEYS[1]) ==  ARGV[1]) then
    -- 释放锁 del key
    return redis.call('del', KEYS[1])
end
return 0


测试代码

package com.example.springboot;
import com.example.springboot.tool.ILock;
import com.example.springboot.tool.SimpleRedisLock;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.TimeUnit;
@SpringBootTest
public class RedisLockTest {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Test
    public void testLock() throws InterruptedException {
        ILock lock = new SimpleRedisLock("Blog", 20, TimeUnit.SECONDS, stringRedisTemplate);
        boolean isLock = lock.tryLock();
        if(isLock){
            System.out.println("第一次获取锁成功");
            {
                ILock lock2 = new SimpleRedisLock("Blog", 20, TimeUnit.SECONDS, stringRedisTemplate);
                boolean isLock2 = lock2.tryLock();
                if(isLock2){
                    System.out.println("第二次获取锁成功");
                    lock2.unlock();
                }else{
                    System.out.println("第二次获取锁失败");
                }
            }
            Thread.sleep(5000);
            lock.unlock();
            System.out.println("释放锁成功");
        }else{
            System.out.println("第一次获取锁失败");
        }
    }
}


当然,第二次获取锁肯定是失败的。

这里堵塞5秒钟,可以在RDM中看到值,但是20秒后该值会自动删除,但是在过期之前,程序会使用脚本将其删除。

该实现的弊端如下:

问题1:不可重入。问题2:不可重试。问题3:超时释放。问题4:主从一致。


END

推荐您阅读更多有关于“ 超时 redis set setnx ”的文章

上一篇:简易Zookeeper客户端管理工具 下一篇:Aspect声明式事物解决Spring事物内部调用不生效

猜你喜欢

发表评论: