【Redis】加锁自动续期-RedisTemplate实现

RedisTemplate实现自动续期

导入依赖

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

配置文件

spring.redis.host=127.0.0.1
spring.redis.port=6379
#spring.redis.password=
spring.redis.database=1
spring.redis.pool.max-active=8
spring.redis.pool.max-wait=-1
spring.redis.pool.max-idle=500
spring.redis.pool.min-idle=0
spring.redis.timeout=500

工具类

注:RedisTemplate是springboot内置的Redis客户端,无需再次通过@bean注入。导入redis依赖和配置文件中添加redis配置后,可以直接使用

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Component
public class RedisDistributedLock {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    // Lua 脚本用于释放锁
    private static final String UNLOCK_SCRIPT =
            "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end";

    // Lua 脚本用于续期锁
    private static final String RENEW_LOCK_SCRIPT =
            "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('PEXPIRE', KEYS[1], ARGV[2]) else return 0 end";

    // 锁的前缀,用于区分不同的锁
    private static final String LOCK_PREFIX = "lock:";

    // 锁的默认过期时间(毫秒)
    private static final long DEFAULT_EXPIRE_TIME_MS = 5000;

    // 续期锁的时间间隔(毫秒)
    private static final long RENEW_INTERVAL_MS = 2000;

    /**
    尝试获取锁
    @param lockKey   锁的键名
    @param expireMs  锁的过期时间(毫秒)
     @return   如果成功获取锁,返回 true;否则返回 false
     **/
    public boolean tryLock(String lockKey, long expireMs) {
        String lockValue = UUID.randomUUID().toString();  // 生成唯一的锁值
        Boolean result = redisTemplate.opsForValue().setIfAbsent(LOCK_PREFIX + lockKey, lockValue, expireMs, TimeUnit.MILLISECONDS);     // setIfAbsent实现上锁
        return result != null && result;
    }

    /**
    尝试获取锁并自动续期
    @param lockKey   锁的键名
    @param expireMs  锁的过期时间(毫秒)
    @return   如果成功获取锁,返回锁的唯一标识符;否则返回 null
     **/
    public String tryLockWithRenewal(String lockKey, long expireMs) {
        String lockValue = UUID.randomUUID().toString();  // 生成唯一的锁值
        Boolean result = redisTemplate.opsForValue().setIfAbsent(LOCK_PREFIX + lockKey, lockValue, expireMs, TimeUnit.MILLISECONDS);        // setIfAbsent实现上锁
        if (result != null && result) {
            // 启动续期线程
            startRenewalThread(lockKey, lockValue, expireMs);
            return lockValue;
        }
        return null;
    }

    /**
    释放锁
    @param lockKey   锁的键名
    @param lockValue 锁的值(用于验证是否是持有锁的客户端)
     @return  如果成功释放锁,返回 true;否则返回 false
    **/
    public boolean unlock(String lockKey, String lockValue) {
        DefaultRedisScript<Long> script = new DefaultRedisScript<>(UNLOCK_SCRIPT, Long.class);
        // 执行lua脚本,参数解释下:
        // 第一个参数script为lua脚本
        // 第二个参数为key的集合,会依次替换lua脚本中的KEYS[]数组的数据,默认1开始
        // 第三个参数为参数集合,会依次替换lua脚本中的ARGVS[]数组的数据,默认1开始
        Long result = redisTemplate.execute(script, Collections.singletonList(LOCK_PREFIX + lockKey), lockValue);
        return result != null && result == 1L;
    }

    /**
    自动续期锁
    @param lockKey   锁的键名
    @param lockValue 锁的值(用于验证是否是持有锁的客户端)
    @param expireMs  锁的过期时间(毫秒)
     @return  如果成功续期,返回 true;否则返回 false
     **/
    public boolean renewLock(String lockKey, String lockValue, long expireMs) {
        DefaultRedisScript<Long> script = new DefaultRedisScript<>(RENEW_LOCK_SCRIPT, Long.class);
        Long result = redisTemplate.execute(script, Collections.singletonList(LOCK_PREFIX + lockKey), lockValue, String.valueOf(expireMs));
        return result != null && result == 1L;
    }

    /**
    启动续期线程
    @param lockKey   锁的键名
    @param lockValue 锁的值
    @param expireMs  锁的过期时间(毫秒)
     **/
    private void startRenewalThread(final String lockKey, final String lockValue, final long expireMs) {
        Thread renewalThread = new Thread(() -> {
            try {
                while (true) {
                    // 每隔一段时间续期一次,这里配置为2秒,需要确保一点啊,就是间隔时间小于过期时间,不然过期了还怎么续期呢?
                    Thread.sleep(RENEW_INTERVAL_MS);
                    if (!renewLock(lockKey, lockValue, expireMs)) {  // 续锁操作
                        // 如果续期失败,直接结束守护线程,停止锁续期行为。
                        // 这里说明下,删除锁和续锁都需要验证lockValue,这个上锁时通过uuid创建的,其他线程肯定获取的都不一致,这样确保续锁行为只能是自己的守护线程才可以操作;如果续锁失败了,则说明是主线程完成任务删除了key锁,所以这里守护线程也可以结束了
                        break;
                    }
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
        renewalThread.setDaemon(true);  // 设置为守护线程
        renewalThread.start();
    }
}

上述代码中setIfAbsent仅单次获取锁,失败会立刻返回,如果需要持续一段时间去抢锁,可以采用适当的循环机制,
持续抢锁示例如下:

   public boolean tryLockWithTimeout(String lockKey, String lockValue) throws InterruptedException {
        long startTime = System.currentTimeMillis();
        while (System.currentTimeMillis() - startTime < MAX_RETRY_TIME) {
            // 尝试设置键值对,如果键不存在则设置成功
            Boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, LOCK_EXPIRE_TIME, TimeUnit.SECONDS);
            if (success != null && success) {
                // 获取锁成功
                return true;
            }
            // 等待一段时间后重试
            Thread.sleep(RETRY_INTERVAL);
        }
        // 超时仍未获取到锁
        return false;
    }

分布式锁操作业务

注意:一般建议使用try finally结构,在finally中进行解锁,防止死锁

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class LockService {

    @Autowired
    private RedisDistributedLock redisDistributedLock;

    public void executeWithLock(String lockKey) throws InterruptedException {
        // 尝试获取锁并自动续期
        String lockValue = redisDistributedLock.tryLockWithRenewal(lockKey, 5000);
        if (lockValue != null) {
            try {
                System.out.println(Thread.currentThread().getName() + " 获取到锁,开始执行任务...");
                // 模拟任务执行
                Thread.sleep(8000);  // 任务执行时间超过锁的过期时间''
                System.out.println(Thread.currentThread().getName() + " 任务执行完成");
            } finally {
                // 释放锁
                boolean unlockSuccess = redisDistributedLock.unlock(lockKey, lockValue);
                if (unlockSuccess) {
                    System.out.println(Thread.currentThread().getName() + " 成功释放锁");
                } else {
                    System.out.println(Thread.currentThread().getName() + " 释放锁失败,锁可能已被其他客户端删除");
                }
            }
        } else {
            System.out.println(Thread.currentThread().getName() + " 未能获取到锁");
            // 如果未抢到锁的线程想要继续抢锁执行任务的话,可以在这里加逻辑去循环抢锁...(正常抢不到锁直接提示并返回就可以了)
        }
    }
}

模拟并发执行任务

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@Component
public class LockTest implements CommandLineRunner {

    @Autowired
    private LockService lockService;

    @Override
    public void run(String... args) throws Exception {
        // 创建线程池
        ExecutorService executorService = Executors.newFixedThreadPool(5);

        // 启动多个线程尝试获取锁
        for (int i = 0; i < 5; i++) {
            executorService.submit(() -> {
                try {
                    lockService.executeWithLock("my_lock");
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }

        // 关闭线程池
        executorService.shutdown();
    }
}

验证结果

img

posted @ 2025-04-23 08:50  明小子@  阅读(33)  评论(0)    收藏  举报