0
点赞
收藏
分享

微信扫一扫

Redis实现分布式锁

一. 概述

二. Redis分布式锁简单实用

2.1 导入Redisson依赖包

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.8.2</version>
</dependency>

2.2 使用

Config config = new Config();
config.useClusterServers()
    .setScanInterval(2000) // cluster state scan interval in milliseconds
    .addNodeAddress("redis://127.0.0.1:7000", "redis://127.0.0.1:7001")
    .addNodeAddress("redis://127.0.0.1:7002");
// 配置redis地址
RedissonClient redisson = Redisson.create(config);
// 获取实例
RLock lock = redisson.getLock("anyLock");
// 获取锁
lock.lock();

try {
    // 执行业务代码块
    ...
} finally {
    // 释放锁
    lock.unlock();
}

三. 分析原理, 手写一个Redis锁

3.1 声明接口

/**
 * 定义锁
 * @author shaolq
 *
 */
public interface DLock {
    /**
     * 获取锁
     * @param lock 锁名称
     */
    void lock(String lock);

    /**
     * 释放锁
     * @param lock 锁名称
     */
    void unlock(String lock);
}

3.2 接口实现类

/**
 * 通过redis实现分布锁
 * 
 * @author shaolq
 *
 */
public class DistributeLock implements DLock {
    private static final int LOCK_MAX_EXIST_TIME = 5;  // 单位s,一个线程持有锁的最大时间
    private static final String LOCK_PREX = "lock_"; // 作为锁的key的前缀
    
    private StringRedisTemplate redisTemplate;
    private String lockPrex; // 做为锁key的前缀
    private int lockMaxExistTime; // 单位s,一个线程持有锁的最大时间
    private DefaultRedisScript<Long> lockScript; // 锁脚本
    private DefaultRedisScript<Long> unlockScript; // 解锁脚本
    
    // 线程变量
    private ThreadLocal<String> threadKeyId = ThreadLocal.withInitial(() -> UUID.randomUUID().toString());
    
    public DistributeLock(StringRedisTemplate redisTemplate){
        this(redisTemplate, LOCK_PREX, LOCK_MAX_EXIST_TIME);
    }
    
    public DistributeLock(StringRedisTemplate redisTemplate, String lockPrex, int lockMaxExistTime){
        this.redisTemplate = redisTemplate;
        this.lockPrex = lockPrex;
        this.lockMaxExistTime = lockMaxExistTime;
        // init
        init();
    }
    
    /**
     * 生成
     */
    public void init() {
        // Lock script 导入获取锁的Lua脚本
        lockScript = new DefaultRedisScript<Long>();
        lockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lock/lock.lua")));
        lockScript.setResultType(Long.class);
        // unlock script 导入释放锁的Lua脚本
        unlockScript = new DefaultRedisScript<Long>();
        unlockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lock/unlock.lua")));
        unlockScript.setResultType(Long.class);
    }
    
    @Override
    public void lock(String lock2){
        Assert.notNull(lock2, "lock2 can't be null!");
        String lockKey = getLockKey(lock2);
        while(true){
            List<String> keyList = Lists.newArrayList();
            keyList.add(lockKey);
            keyList.add(threadKeyId.get());
            if(redisTemplate.execute(lockScript, keyList, String.valueOf(lockMaxExistTime * 1000)) > 0){
                break;
            }else{
                try {
                    // 短暂休眠,nano避免出现活锁 
                    Thread.sleep(10, (int)(Math.random() * 500));
                } catch (InterruptedException e) {
                    break;
                }
            }
        }
    }

    /**
     * 释放锁,同时要考虑当前锁是否为自己所有,以下情况会导致当前线程失去锁:线程执行的时间超过超时的时间,导致此锁被其它线程拿走; 此时用户不可以执行删除
     * 
     */
    @Override
    public void unlock(final String lock) {
        final String lockKey = getLockKey(lock);
        List<String> keyList = Lists.newArrayList();
        keyList.add(lockKey);
        keyList.add(threadKeyId.get());
        redisTemplate.execute(unlockScript, keyList);
    }

    /**
     * 生成key
     * @param lock
     * @return
     */
    private String getLockKey(String lock){
        StringBuilder sb = new StringBuilder();
        sb.append(lockPrex).append(lock);
        return sb.toString();
    }
    
}

3.3 编写获取Lua脚本

  • 获取锁Lua脚本 -> lock.lua
-- Set a lock
--  如果获取锁成功,则返回 1
local key     = KEYS[1] -- key
local content = KEYS[2] -- 线程id
local ttl     = ARGV[1] -- 过期时间
-- setnx: 表示将 key 的值设为 content,当且仅当 key 不存在。若给定的 key 已经存在,则 SETNX 不做任何动作。
-- 返回整数,具体为
--1,当 key 的值被设置
--0,当 key 的值没被设置
local lockSet = redis.call('setnx', key, content)
if lockSet == 1 then
  -- 设置key的过期时间, 时间单位为毫秒
  redis.call('pexpire', key, ttl)
--  redis.call('incr', "count")
else 
  -- 如果value相同,则认为是同一个线程的请求,则认为重入锁
  local value = redis.call('get', key)
  if(value == content) then
    lockSet = 1;
    redis.call('pexpire', key, ttl)
  end
end
return lockSet
  • 释放锁Lua脚本 -> unlock.lua
-- unlock key
local key     = KEYS[1] -- key
local content = KEYS[2] -- 线程id
local value = redis.call('get', key)
-- 判断该key的值是否当前线程的值, 是则释放锁, 否则不处理
if value == content then
--  redis.call('decr', "count")
  return redis.call('del', key);
end
return 0

3.4 注入bean

@Configuration
public class DistributedLockConfiguration {

    @Resource(name = "stringRedisTemplate")
    protected StringRedisTemplate stringRedisTemplate;

    @Bean
    public DistributeLock distributeLock() {
        return new DistributeLock(stringRedisTemplate, "lock_", 1200);
    }
}

四.不用LUA脚本实现分布式锁

public class RedisLockUtil {
    private Logger log = LoggerFactory.getLogger(RedisDao.class);

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    // 当前线程唯一常量
    private ThreadLocal<String> threadKeyId = ThreadLocal.withInitial(() -> UUID.randomUUID().toString());

    /**
     * redis获取锁,获取不到就会不断重试
     * @param key  锁名
     * @param time  锁的最大有效时间(秒)
     */
    public void lock(String key,int time){
        // 锁名
        String keyName = getLockName(key);
        while(true){
            // 争抢锁
            Boolean flag = setnxAndExpire(keyName, threadKeyId.get(), time);
            // 判断key是否存在
            if(!flag){
                // 判断是否重入锁
                if(threadKeyId.get().equals(String.valueOf(stringRedisTemplate.opsForValue().get(keyName)))){
                    break;
                }
                // 短暂休眠,nano避免出现活锁
                try {
                    Thread.sleep(500, 500);
                } catch (InterruptedException e) {
                    break;
                }
            }else {
                break;
            }
        }
    }

    /**
     * 尝试获取锁,立即返回结果
     * @param key
     * @param time
     * @return
     */
    public Boolean tryLock(String key,int time){
        // 锁名
        String keyName = getLockName(key);
        return setnxAndExpire(keyName, threadKeyId.get(), time);
    }

    /**
     * 释放锁
     * @param key
     */
    public void unlock(String key){
        // 锁名
        String keyName = getLockName(key);
        // 释放锁
        if (threadKeyId.get().equals(stringRedisTemplate.opsForValue().get(keyName))) {
            stringRedisTemplate.delete(keyName);
        }
    }

    /**
     * 获取锁的名字
     * @param key
     * @return
     */
    public String getLockName(String key){
        if(null != key){
            return "REDIS_LOCK_"+key;
        }else {
            return "DEFAULT_LOCK";
        }
    }
    
    /**
     * key不存在时设置value,一般用于争抢锁
     * 把setnx和expire合并,保证原子性,
     * @param key
     * @param value 值
     * @param time 秒
     * @return
     */
    public Boolean setnxAndExpire(String key ,String value,long time){
        /**
         * set key value [EX seconds] [PX milliseconds] [NX|XX]
         * EX seconds:设置失效时长,单位秒
         * PX milliseconds:设置失效时长,单位毫秒
         * NX:key不存在时设置value,成功返回OK,失败返回(nil)
         * XX:key存在时设置value,成功返回OK,失败返回(nil)
         *
         * 示例: set name p7+ ex 100 nx
         * SET操作成功后,返回的是OK,失败返回NIL
         */

        return stringRedisTemplate.execute((RedisCallback<Boolean>) connection -> {
            /**
             * 给定命令与给定参数一起的“本机”或“原始”执行。 该命令按原样执行,并且尽可能少地执行“解释”,这取决于调用方对参数或结果的任何处理
             */
            Object obj = connection.execute("set", SafeEncoder.encode(key), SafeEncoder.encode(value), SafeEncoder.encode("EX"), Protocol.toByteArray(time), SafeEncoder.encode("NX"));
            return obj != null;
        });
    }
}
举报

相关推荐

0 条评论