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块来确保即使发生异常,锁也能被正确释放。

学海无涯苦作舟!!!

相关推荐
koping_wu19 小时前
【RabbitMQ】架构原理、消息丢失、重复消费、顺序消费、事务消息
分布式·架构·rabbitmq
nongcunqq20 小时前
abap 操作 excel
java·数据库·excel
rain bye bye20 小时前
calibre LVS 跑不起来 就将setup 的LVS Option connect下的 connect all nets by name 打开。
服务器·数据库·lvs
喵桑..20 小时前
kafka源码阅读
分布式·kafka
阿里云大数据AI技术1 天前
云栖实录|MaxCompute全新升级:AI时代的原生数据仓库
大数据·数据库·云原生
酷ku的森1 天前
RabbitMQ的概述
分布式·rabbitmq
不剪发的Tony老师1 天前
Valentina Studio:一款跨平台的数据库管理工具
数据库·sql
weixin_307779131 天前
在 Microsoft Azure 上部署 ClickHouse 数据仓库:托管服务与自行部署的全面指南
开发语言·数据库·数据仓库·云计算·azure
六元七角八分1 天前
pom.xml
xml·数据库
虚行1 天前
Mysql 数据同步中间件 对比
数据库·mysql·中间件