美文网首页javaWeb学习
Redis实现分布式锁

Redis实现分布式锁

作者: 任未然 | 来源:发表于2020-04-08 00:56 被阅读0次

一. 概述

在java开发中, 多线程环境下为了保证一个代码块在同一时间只能由一个线程访问, 我们一般可以使用synchronized语法和ReetrantLock去保证,这实际上是本地锁的方式。但是现在公司都是流行分布式架构,在分布式环境下,如何保证不同节点的线程同步执行呢?今天我们来介绍利用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锁

原理:

  1. 首先声明一个Key, 然后业务代码块执行前会找该key的值, 找不到就执行, 能找到就循环查找, 直到找不到再执行
  2. 找不都key值后, 先设置该key的值, 生成锁, 业务代码块执行完再删掉该key的值, 释放锁
  3. 为了避免死锁, 会给该key设一个有效时间
  4. 通常会把key的值设置为当前线程的唯一id, 当找到key的值, 再去比较该值与当前线程的唯一id是否相同, 相同则是重入锁可以执行, 否则等待

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);
    }
}

相关文章

网友评论

    本文标题:Redis实现分布式锁

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