Redis、Redission实现分布式锁

2023-11-06

Redis实现

使用spring-data-redis提供的接口实现redis分布式锁

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

一、利用SETNX设置KEY、VALUE并设置超时时间实现分布式锁

redis2.x版本中提供了不同参数的setIfAbsent方法,看下spring-data-redis源码

public Boolean setIfAbsent(K key, V value) {
  byte[] rawKey = rawKey(key);
  byte[] rawValue = rawValue(value);
  return execute(connection -> connection.setNX(rawKey, rawValue));
}


public Boolean setIfAbsent(K key, V value, long timeout, TimeUnit unit) {
	byte[] rawKey = rawKey(key);
	byte[] rawValue = rawValue(value);
	Expiration expiration = Expiration.from(timeout, unit);
   return execute(connection -> connection.set(rawKey, rawValue, expiration, SetOption.ifAbsent()));
}

手写lock

 public boolean redisLock(String key,String lock,long timeout){
        boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, lock);
        if(flag){
            /**
             * 拿到锁设置锁key的超时时间
             */
           stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);
       }
    return flag;
}

public static Boolean lock(String key,String lock,long timeout){
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, lock, timeout,TimeUnit.SECONDS);
        if (flag) {
            log.info("获取锁成功");
        }
    return flag;
}

第一种方式:redisLock()方法是非原子性的,当执行完setIfAbsent方法后,服务器宕机或者重启,这时候key并没有设置过期时间,会导致死锁。

第二种方式:lock()方法是原子性的,调用setIfAbsent时同步设置key的过期时间

注意:这两种方式不可以用于多线程或者事务中,调用setIfAbsent返回的是null

二、SETNX设置kv(v为系统当前时间+超时时间)和GETSET(设置一个值并返回旧值)实现分布式锁

 public static Boolean redisDistributedLock(String key, long timeout) {
        //锁的值为当前系统时间+过期时间
        long lockValue = System.currentTimeMillis() + timeout;
        log.info("锁的初始值:{}",lockValue);
        if (stringRedisTemplate.opsForValue().setIfAbsent(key, String.valueOf(lockValue))) {
            log.info("获取锁成功");
            return true;
        }else {
            log.info("我没有获取到锁");
        }
        //其他人没有获取到锁
        //获取当前锁的值
        String currentLockValue  = stringRedisTemplate.opsForValue().get(key);
        log.info("获取当前锁的值:{}",currentLockValue);
        //判断锁的值小于当前系统时间说明锁已经过期
        if(StringUtils.isNotEmpty(currentLockValue) && Long.parseLong(currentLockValue) < System.currentTimeMillis()){
            //getAndSet方法,返回旧的值并设置新值,线程安全所以只会有一个线程重新设置锁的新值
            String oldValue  = stringRedisTemplate.opsForValue().getAndSet(key, String.valueOf(lockValue));
            log.info("获取oldValue的值:{}",oldValue);
            //比较getAndSet方法获取最近的值和开始的值,如果不相等就证明已经被其他线程获取了
            if(StringUtils.isNotEmpty(oldValue) && oldValue.equals(currentLockValue)){
                return true;
            }
        }
        return false;
}

这种方式没有设置key的过期时间,而是将value值设置为当前系统时间+过期时间。此方式要求服务器时间一致。

Redisson锁

1、redisson依赖

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

2、配置类

@Configuration
public class RedissonConfig {
    
    @Bean
    public Redisson redisson() {
        // 单机模式
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(0);
        config.useSingleServer().setPassword("123456");
        return (Redisson) Redisson.create(config);
    }

}
 

3、redisson自动续期(看门狗)

public void task3() {
        //获取锁,没有获取到锁的会阻塞
        //redisson设置一个key的默认过期时间为30s
        //redisson会自动续期

        //设置lockey
        String lockKey = "taskLock";
        RLock lock = redisson.getLock(lockKey);
        //上锁
        /**
         * 处理业务执行时间大于锁的时间,自动续期
         * 不设置过期时间,默认锁的时间为30s,每1/3的时间就自动续期,业务处理完需要手动释放锁
         */
        lock.lock();
        //lock.lock(10,TimeUnit.SECONDS);  这种是10秒后锁自动过期,不会有自动续期的机制
        //boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS); 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
        try {
            //模拟业务执行
            Thread.sleep(40000);
            log.info("模拟业务执行了40s");
        } catch (Exception e){
            e.printStackTrace();
        }finally {
            //释放锁
            lock.unlock();
        }

}

lock.lock(); 是阻塞式等待的,默认加锁时间是30s;如果业务超长,运行期间会自动续期到30s。不用担心业务时间长,锁自动过期被删掉;加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认会在30s内自动过期,不会产生死锁问题;

也可以自己指定解锁时间lock.lock(10,TimeUnit.SECONDS),10秒钟自动解锁,自己指定解锁时间redis不会自动续期;

看门狗原理

源码分析

private void renewExpiration() {
        RedissonBaseLock.ExpirationEntry ee = (RedissonBaseLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName());
        if (ee != null) {
            Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
                public void run(Timeout timeout) throws Exception {
                    RedissonBaseLock.ExpirationEntry ent = (RedissonBaseLock.ExpirationEntry)RedissonBaseLock.EXPIRATION_RENEWAL_MAP.get(RedissonBaseLock.this.getEntryName());
                    if (ent != null) {
                        Long threadId = ent.getFirstThreadId();
                        if (threadId != null) {
                            RFuture<Boolean> future = RedissonBaseLock.this.renewExpirationAsync(threadId);
                            future.onComplete((res, e) -> {
                                if (e != null) {
                                    RedissonBaseLock.log.error("Can't update lock " + RedissonBaseLock.this.getRawName() + " expiration", e);
                                    RedissonBaseLock.EXPIRATION_RENEWAL_MAP.remove(RedissonBaseLock.this.getEntryName());
                                } else {
                                    if (res) {
                                        RedissonBaseLock.this.renewExpiration();
                                    }

                                }
                            });
                        }
                    }
                }
            }, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);
            ee.setTimeout(task);
        }
    }

1、如果lock锁指定了过期时间,到期后锁自动释放,不会自动续期

2、当我们没有设置过期时间,默认的过期时间是30s,即 lockWatchdogTimeout = 30000ms。

自动续期时间: internalLockLeaseTime / 3 ,如果设置lockWatchdogTimeout = 60000ms,那么自动续期时间为60/3,即20秒自动续期。

解释一下:源码中 this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();

如果没有设置看门狗超时时间,默认30s,那么每隔10秒自动续期。

如果设置了看门狗超时时间,就是lockWatchdogTimeout/3秒自动续期

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Redis、Redission实现分布式锁 的相关文章

随机推荐