您的位置:首页 > 科技 > IT业 > 电脑网游_摄影网站app_地推任务网_市场营销策略

电脑网游_摄影网站app_地推任务网_市场营销策略

2024/12/23 11:33:16 来源:https://blog.csdn.net/qq_34207422/article/details/144513461  浏览:    关键词:电脑网游_摄影网站app_地推任务网_市场营销策略
电脑网游_摄影网站app_地推任务网_市场营销策略

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

1、使用RedisTemplate实现

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

第一步:导入依赖

<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 {@Autowiredprivate 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 {@Autowiredprivate 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 {@Autowiredprivate LockService lockService;@Overridepublic 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提供了更高级的功能,如 自动续期、可重入锁和公平锁等。

基本原理示意图:
在这里插入图片描述

第一步:导入依赖

<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.20.0</version>
</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

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

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

第四步:编写redission工具类

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

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 {@Autowiredprivate 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执行业务

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;@Component
public class LockUsageExample {@Autowiredprivate 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() +"无法获取锁,任务被其他客户端占用...");}}
}

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

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 {@Autowiredprivate LockUsageExample usageExample;@Overridepublic 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块来确保即使发生异常,锁也能被正确释放。

学海无涯苦作舟!!!

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com