redis的分布式锁的几种实现

2,087 阅读5分钟

注:此文章,为总结的学习笔记。

分布式锁是什么

  • 分布式锁是控制分布式系统或不同系统之间共同访问共享资源的一种锁实现
  • 如果不同的系统或同一个系统的不同主机之间共享了某个资源时,往往通过互斥来防止彼此干扰。

分布锁设计目的

  • 可以保证在分布式部署的应用集群中,同一个方法在同一操作只能被一台机器上的一个线程执行。

  • 设计要求

    • 这把锁要是一把可重入锁(避免死锁)
    • 这把锁有高可用的获取锁和释放锁功能
    • 这把锁获取锁和释放锁的性能要好

  • 分布锁实现方案分析

    • 获取锁的时候,使用 setnx(SETNX key val:当且仅当 key 不存在时,set 一个 key 为 val 的字符串,返回 1;
    • 若 key 存在,则什么都不做,返回 【0】加锁,锁的 value 值为当前占有锁服务器内网IP编号拼接任务标识
    • 在释放锁的时候进行判断。并使用 expire 命令为锁添 加一个超时时间,超过该时间则自动释放锁。
    • 返回1则成功获取锁。还设置一个获取的超时时间, 若超过这个时间则放弃获取锁。setex(key,value,expire)过期以秒为单位
    • 释放锁的时候,判断是不是该锁(即Value为当前服务器内网IP编号拼接任务标识),若是该锁,则执行 delete 进行锁释放
  • 分布锁满足两个条件,一个是加有效时间的锁,一个是高性能解锁

  • 采用redis命令setnx(set if not exist)、setex(set expire value)实现

  • 【千万记住】解锁流程不能遗漏,否则导致任务执行一次就永不过期

  • 将加锁代码和任务逻辑放在try,catch代码块,将解锁流程放在finally

SETNX命令实现分布式锁

public void lockJob() {
        String lock = LOCK_PREFIX + "LockNxExJob";
        try{
            //redistemplate setnx操作
            boolean nxRet = redisTemplate.opsForValue().setIfAbsent(lock,"XXX");
            Object lockValue = redisService.get(lock);

            //获取锁失败
            if(!nxRet){
                String value = (String)redisService.get(lock);
                //打印当前占用锁的服务器IP
                logger.info("get lock fail,lock belong to:{}",value);
                return;
            }else{
                redisTemplate.opsForValue().set(lock,getHostIp(),3600);

                //获取锁成功
                logger.info("start lock lockNxExJob success");
                Thread.sleep(5000);
            }
        }catch (Exception e){
            logger.error("lock error",e);

        }finally {
            redisService.remove(lock);
        }
    }

SETNX的缺陷

  1. 服务器宕机
    • 程序刚获取到锁还没有释放,服务器宕机,会导致锁无法释放,其他服务端永远获取不到锁。
  2. redis 宕机
    • redis获取到锁之后,redis服务挂了,也会导致锁无法被释放。
  • 所以要保证SETNX和SETEX(设置过期时间)这2个命令一起执行,要么都成功,要么都失败。保证其原子性。
  • redis官网文档的描述,使用下面的命令加锁
    • SET key value NX PX 30000
      • value是由客户端生成的一个随机字符串,相当于是客户端持有锁的标志

      • NX表示只有key值不存在的时候才能SET成功,相当于只有第一个请求的客户端才能获得锁

      • PX 30000表示这个锁有一个30秒的自动过期时间。

  1. 解锁,为了防止客户端1获得的锁,被客户端2给释放,采用下面的Lua脚本来释放锁
if redis.call("get",KEYS[1]) == ARGV[1] then
   return redis.call("del",KEYS[1])
else
   return 0
end

锁的优化

某线程成功得到了锁,并且设置的超时时间是30秒。 如果某些原因导致线程B执行的很慢很慢,过了30秒都没执行完,这时候锁过期自动释放,线程B得到了锁。

随后,线程A执行完了任务,线程A接着执行del指令来释放锁。但这时候线程B还没执行完,线程A实际上删除的是线程B加的锁。

怎么避免这种情况呢?可以在del释放锁之前做一个判断,验证当前的锁是不是自己加的锁。

至于具体的实现,可以在加锁的时候把当前的线程ID当做value,并在删除之前验证key对应的value是不是自己线程的ID。

采用Lua+setNX脚本实现redis分布式锁(2.6版本以后)

  • Lua简介
    • 从 Redis 2.6.0 版本开始,通过内置的 Lua 解释器,可以使用 EVAL 命令对 Lua 脚本进行求值。
    • Redis 使用单个 Lua 解释器去运行所有脚本,并且, Redis 也保证脚本会以原子性(atomic)的方式执行:当某个脚本正在运行的时候,不会有其他脚本或 Redis 命令被执行。这和使用 MULTI / EXEC 包围的事务很类似。在其他别的客户端看来,脚本的效果(effect)要么是不可见的(not visible),要么就是已完成的(already completed)。
  • Lua脚本配置流程
    • 1、在resource目录下面新增一个后缀名为.lua结尾的文件
    • 2、编写lua脚本
    • 3、传入lua脚本的key和arg
    • 4、调用redisTemplate.execute方法执行脚本
/**
    * 获取lua结果
    * @param key
    * @param value
    * @return
    */
   public Boolean luaExpress(String key,String value) {
       DefaultRedisScript<Boolean> lockScript = new DefaultRedisScript<Boolean>();
       lockScript.setScriptSource(
               new ResourceScriptSource(new ClassPathResource("add.lua")));
       lockScript.setResultType(Boolean.class);
       // 封装参数
       List<Object> keyList = new ArrayList<Object>();
       keyList.add(key);
       keyList.add(value);
       Boolean result = (Boolean) redisTemplate.execute(lockScript, keyList);
       return result;
   }

add.lua

local lockKey = KEYS[1]
local lockValue = KEYS[2]

-- setnx info
local result_1 = redis.call('SETNX', lockKey, lockValue)
if result_1 == true
then
local result_2= redis.call('SETEX', lockKey,3600, lockValue)
return result_1
else
return result_1
end

redisson实现分布式锁

引起maven配置

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

代码实现:

@SpringBootApplication
public class RedissonApplication {
   public static void main(String[] args) {
   	SpringApplication.run(RedissonApplication.class, args);
   }
   @Bean
   Redisson redissonSentinel() {
   	//支持单机,主从,哨兵,集群等模式
   	//此为哨兵模式
   	Config config = new Config();
   	config.useSentinelServers()
   			.setMasterName("mymaster")
   			.addSentinelAddress("redis://192.168.1.1:6379")
   			.setPassword("123456");
   	return (Redisson)Redisson.create(config);
   }
}

使用:

        String lockKey = "test";//分布式锁的key
        RLock lock = redisson.getLock(lockKey);
   	lock.lock(60, TimeUnit.SECONDS); //设置60秒自动释放锁  (默认是30秒自动过期)
   	...业务代码..
   	lock.unlock(); //释放锁

采用spring redisTemplate 实现分布式锁

spring-data-redis的版本尽量高版本,2.0以下的connection.set是没有返回值的。

    @Component
    public class RedisLock {
        @Resource
        private RedisTemplate<String, Object> redisTemplate;
        //加锁
        public Boolean setNX(final String key, final String requestId, final long expirationTime, final TimeUnit timeUnit) {
            return redisTemplate.execute((RedisCallback<Boolean>) connection -> connection.set(key.getBytes(), (value == null ? "" : value).getBytes(),
                    Expiration.from(expirationTime, timeUnit),
                    RedisStringCommands.SetOption.ifAbsent()));
        }
        //释放锁
        public  Boolean releaseLock(String key, String requestId) {
            return (Boolean) redisTemplate.execute((RedisCallback<Boolean>) connection -> {
                String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
                Boolean result = connection.eval(script.getBytes(), ReturnType.BOOLEAN, 1, key.getBytes(), requestId.getBytes());
                return result;
            });
        }
    }