Redis篇-18--分布式锁2-实战篇(RedisTemplate实现,Redission实现)

上一篇我们重点介绍了Redis实现分布式锁的相关原理以及方式。本篇以Java中两个常用的Redis客户端RedisTemplate和Redission实现一下分布式锁的功能。

1、使用RedisTemplate实现

在redisTemplate中,可以使用setIfAbsent方法获取分布式锁,使用execute方法执行lua脚本去解锁和执行锁续期,完整示例如下:

第一步:导入依赖

java 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

第二步:配置文件

java 复制代码
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配置后,可以直接使用。

java 复制代码
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仅单次获取锁,失败会立刻返回,如果需要持续一段时间去抢锁,可以采用适当的循环机制,
持续抢锁示例如下:

java 复制代码
   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中进行解锁,防止死锁。

java 复制代码
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() + " 未能获取到锁");
            // 如果未抢到锁的线程想要继续抢锁执行任务的话,可以在这里加逻辑去循环抢锁...(正常抢不到锁直接提示并返回就可以了)
        }
    }
}

第五步:模拟并发执行任务

java 复制代码
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();
    }
}

第六步:验证结果

启动项目,可以在项目的启动日志中看到如下信息。

如上的日志说明:分布式锁生效了,只有线程2执行到了任务,且执行完成后正常释放锁。其他线程因为没有抢到锁而直接终止。

2、使用Redission实现

Redisson是一个功能强大的Redis Java客户端,它不仅提供了对Redis的基本操作支持,还内置了多种分布式数据结构和分布式锁的实现。

使用Redisson可以大大简化Redis分布式锁的实现,并且Redisson提供了更高级的功能,如 自动续期、可重入锁和公平锁等。

基本原理示意图:

第一步:导入依赖

java 复制代码
<dependency>
   <groupId>org.redisson</groupId>
   <artifactId>redisson</artifactId>
   <version>3.20.0</version>
</dependency>

第二步:配置文件

java 复制代码
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

第三步:RedissionClient配置类--注入容器配置

java 复制代码
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RedissonConfig {

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer()
                .setAddress("redis://localhost:6379");     // 这里测试,直接写死,实际使用需要改成配置文件注入
        return Redisson.create(config);
    }
}

第四步:编写redission工具类

注意:tryLock时指定过期时间不会启动看门狗线程进行锁续期。

java 复制代码
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;

@Service
public class DistributedLockService {

    @Autowired
    private RedissonClient redissonClient;

    private static final String LOCK_KEY = "resource_1_lock";

    /**
    尝试获取锁  (当明确指定了过去时间leaseTime时(-1不算),不会启动看门狗线程,会在指定时间后自动释放锁)
    @param lockKey 锁的键名
    @param waitTime 等待锁的最大时间(毫秒)
    @param leaseTime 锁的过期时间(毫秒)
     @return 是否成功获取锁
    **/
    public boolean tryLock(String lockKey, long waitTime, long leaseTime) {
        RLock lock = redissonClient.getLock(lockKey);
        try {
            return lock.tryLock(waitTime, leaseTime, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        }
    }

    /**
     尝试获取锁,且自动续期锁  (leaseTime为-1或不设置时,会启动看门狗线程)
     @param lockKey 锁的键名
     @param waitTime 等待锁的最大时间(毫秒)
     @param leaseTime 锁的过期时间(毫秒)
     @return 是否成功获取锁
     **/
    public boolean tryLockWithAutoRenewal(String lockKey, long waitTime, long leaseTime) {
        RLock lock = redissonClient.getLock(lockKey);
        try {
            return lock.tryLock(waitTime, -1, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        }
    }

    /**
    释放锁
    @param lockKey 锁的键名
     **/
    public void unlock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.unlock();
    }
}

第五步:使用redission执行业务

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class LockUsageExample {

    @Autowired
    private DistributedLockService lockService;

    public void executeWithLock(String lockKey) {
        long waitTime = 2000;  // 等待锁的最大时间(5秒)
        long leaseTime = 5000;  // 锁的过期时间(10秒)
        // 尝试获取锁
        if (lockService.tryLockWithAutoRenewal(lockKey, waitTime, leaseTime)) {
            try {
                // 执行业务逻辑
                System.out.println(Thread.currentThread().getName() + "成功获取锁,开始执行任务...");
                // 模拟长时间任务
                try {
                    Thread.sleep(8000);  // 任务耗时8秒
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }

            } finally {
                // 释放锁
                lockService.unlock(lockKey);
                System.out.println(Thread.currentThread().getName() +"任务完成,锁已释放...");
            }
        } else {
            System.out.println(Thread.currentThread().getName() +"无法获取锁,任务被其他客户端占用...");
        }
    }
}

第六步:模拟并发执行任务

java 复制代码
package com.zw.service.redission;

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 LockUsageExample usageExample;

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

        // 启动多个线程尝试获取锁
        for (int i = 0; i < 5; i++) {
            executorService.submit(() -> {
                usageExample.executeWithLock("kkey1");
            });
        }

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

第七步:验证结果

启动项目,可以在项目的启动日志中看到如下信息。

如上日志可以看到,只有线程5执行到了任务,且执行完成后正常释放锁。

Redission注意点:

- 自动续期机制 :Redisson的自动续期机制通过看门狗定期检查锁的状态,并在锁即将过期时自动延长锁的过期时间,确保锁不会因过期而被误释放。

看门狗线程会每隔一段时间(通常是internalLockLeaseTime / 3,其中internalLockLeaseTime 默认为30)检查锁的状态并自动续期,续期后过期时间又变成了internalLockLeaseTime,即30秒。

(实现:在lock或tryLock方法时,不指定过期时间或指定为-1时会启动看门狗)

**- 容错性:**即使启用了自动续期机制,Redisson仍然会为锁设置一个合理的过期时间(默认为 30 秒)。这意味着如果程序崩溃,线程被中断或看门狗无法继续续期锁时,锁将在过期时间到达后自动被Redis删除。其他客户端可以在此之后重新获取锁,从而避免死锁。

**- 避免死锁的最佳实践:**设置合理的过期时间、使用tryLock而不是lock、确保unlock的调用、以及监控锁的使用情况,都是避免死锁的有效手段。

**- 确保unlock的调用:**在任务完成后,务必调用unlock方法来释放锁。建议使用try-finally块来确保即使发生异常,锁也能被正确释放。

学海无涯苦作舟!!!

相关推荐
·云扬·42 分钟前
MySQL 8.0 Redo Log 归档与禁用实战指南
android·数据库·mysql
IT邦德1 小时前
Oracle 26ai DataGuard 搭建(RAC到单机)
数据库·oracle
惊讶的猫1 小时前
redis分片集群
数据库·redis·缓存·分片集群·海量数据存储·高并发写
不爱缺氧i1 小时前
完全卸载MariaDB
数据库·mariadb
期待のcode1 小时前
Redis的主从复制与集群
运维·服务器·redis
纤纡.1 小时前
Linux中SQL 从基础到进阶:五大分类详解与表结构操作(ALTER/DROP)全攻略
linux·数据库·sql
jiunian_cn2 小时前
【Redis】渐进式遍历
数据库·redis·缓存
橙露2 小时前
Spring Boot 核心原理:自动配置机制与自定义 Starter 开发
java·数据库·spring boot
冰暮流星2 小时前
sql语言之分组语句group by
java·数据库·sql
符哥20082 小时前
Ubuntu 常用指令集大全(附实操实例)
数据库·ubuntu·postgresql