当前位置: 首页 > news >正文

JAVA 笔记

文章目录

  • 示例1.用redisson实现应用级分布式锁
  • 示例2.可以基于业务ID加锁的本地锁
  • 示例3.开发一个具备LRU功能的map

示例1.用redisson实现应用级分布式锁

需求:有一个模块需要多实例运行,在某些业务上,只希望由其中一个实例去执行,另外一个实例不执行。那么可以考虑以下方案:

思路:redisson分布式锁是采用redis的hash结构实现,外层KEY即我们的 业务名称(可以再拼接上我们业务ID),内层HashKey使用redisson实例对象ID+线程ID,内层HashValue是重入次数。 基于这个思想,我们只需要把线程ID换成我们的实例ID即可实现分布式应用级锁,同一个应用实例里面使用的HashKey都是相同的。

Redis分布式锁 加锁使用的LUA脚本如下

if ((redis.call('exists', KEYS[1]) == 0) or (redis.call('hexists', KEYS[1], ARGV[2]) == 1)) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; return redis.call('pttl', KEYS[1]);

:KEYS[1]是外层的KEY,ARGV[1]是Key过期时间,ARGV[2]是内层的HashValue

Redis分布式锁 释放锁使用的LUA脚本如下

if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil;end; local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); if (counter > 0) then redis.call('pexpire', KEYS[1], ARGV[2]); return 0; else redis.call('del', KEYS[1]); redis.call(ARGV[4], KEYS[2], ARGV[1]); return 1; end; return nil;

:KEYS[1]是外层的KEY,ARGV[1]是内层HashKey,ARGV[2]是Key过期时间,ARGV[3]是内层的HashKey

代码示例如下:

AppInstanceLockConfig.java: 用于随机生成 实例名称 和 实例ID

@Component
@Data
public class AppInstanceLockConfig {public static String REDIS_KEY_INSTANCE_LOCK;public static Long REDIS_VALUE_INSTANCE_LOCK;public static Long RANDOM_APP_ID;@Value("${spring.application.name}")private String applicationName;@Value("${server.port}")private String serverPort;@PostConstructprivate void init(){REDIS_KEY_INSTANCE_LOCK = "Lock:AppInstanceLock:" + applicationName;// IP + 五位随机数RANDOM_APP_ID = REDIS_VALUE_INSTANCE_LOCK = Long.parseLong(NetUtil.getLocalhostStr().replaceAll("\\.", "") + RandomUtil.randomNumbers(5));}}

AppInstanceLockUtil.java:用于分布式应用级锁 加锁也解锁的核心逻辑


import lombok.extern.slf4j.Slf4j;
import org.redisson.RedissonLock;
import org.redisson.api.RFuture;
import org.redisson.api.RedissonClient;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;@Slf4j
public class AppInstanceLockUtil {private static final RedissonClient redissonClient = SpringUtils.getBean(RedissonClient.class);private static boolean isOptimisticLockSuc = false;private static boolean isOptimisticLockInitialized = false;/*** 乐观方式尝试获取实例锁,通过利用本地锁标记快速判断当前是否持有锁,后台定时任务定期(10s)刷新一次锁标记* 应用场景:适用于 在一致性不高(锁在多实例间非强互斥场景) 且 获取锁频繁 且 要求和redis网络断开后服务能离线自治 的业务场景* 缺点:多实例运行时,当出现单边网络故障时(如原先已经获取到应用锁的应用实例和redis网络断开,而其它实例和redis网络是通的)*      此时可能出现多个实例都获取到应用锁的情况,可能会造成业务重复处理。当单边网络故障恢复后,此时会变成只有一个实例获取到应用级锁,其它实例获取失败,即恢复正常。* 应用实例锁 不需要释放锁,程序挂了会自动释放。* @return 加锁结果*/public static boolean tryOptimisticAppInstanceLock(){return isOptimisticLockInitialized ? isOptimisticLockSuc : tryAppInstanceLock();}/*** 悲观方式尝试获取实例锁,每次都会操作redis尝试获取锁* 应用场景:一致性要求高的业务场景* 应用实例锁 不需要释放锁,程序挂了会自动释放。* @return 加锁结果*/public static boolean tryAppInstanceLock(){try {return isOptimisticLockSuc = doTryAppInstanceLock();}finally {isOptimisticLockInitialized = true;}}/*** 尝试获取实例锁,应用实例锁 不需要释放锁,程序挂了会自动释放。* @return 加锁结果*/private static boolean doTryAppInstanceLock(){RedissonLock redissonLock = (RedissonLock) redissonClient.getLock(AppInstanceLockConfig.REDIS_KEY_INSTANCE_LOCK);// 用 应用ID代替线程ID,实现应用级别的分布式锁。RFuture<Boolean> future = redissonLock.tryLockAsync(AppInstanceLockConfig.RANDOM_APP_ID);try {return future.toCompletableFuture().get();} catch (InterruptedException e) {future.cancel(true);e.printStackTrace();return false;} catch (ExecutionException e2) {e2.printStackTrace();return false;}}/*** 强制释放应用实例锁(正常情况下是不需要去使用这个方法的)* @return*/public static boolean forceAppInstanceUnLock(){RedissonLock redissonLock = (RedissonLock) redissonClient.getLock(AppInstanceLockConfig.REDIS_KEY_INSTANCE_LOCK);return  redissonLock.forceUnlock();}public static void registerGracefullyStopHook(){Runtime.getRuntime().addShutdownHook(new Thread(() ->{try {if(tryAppInstanceLock() && forceAppInstanceUnLock()){// 获取应用锁成功,那么删除中的应用锁log.info("应用级被当前实例锁持有,应用级锁释放成功~~");}}catch (Exception e){log.error("优雅关闭钩子业务报错:{}", e.getMessage());e.printStackTrace();}}));}static {// 定时检测锁Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> {boolean b = tryAppInstanceLock();log.info("定时任务触-->应用锁获取:" + b);}, 10, 10, TimeUnit.SECONDS);}}

用法:

1:采用本地标记去获取应用级锁(不会直接操作redis,只是判断本地锁标记,速度很快)

if(!AppInstanceLockUtil.tryOptimisticAppInstanceLock()){// 获取应用级锁成功
}else {// 获取应用级锁失败
}

2:操作redis去获取应用级锁(需要操作redis,速度较慢,但锁结果准确)

if(!AppInstanceLockUtil.tryAppInstanceLock()){// 获取应用级锁成功
}else {// 获取应用级锁失败
}

注:这里的应用级锁加锁后,无需释放。当实例关闭后,锁自然会释放。

示例2.可以基于业务ID加锁的本地锁

LocalLock .java:本地锁加锁、解锁的核心逻辑


import lombok.extern.slf4j.Slf4j;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;@Slf4j
public class LocalLock {// 获取锁失败时,重试获取锁的自旋间隔(ms)public static final  Long DEFAULT_RETRY_INTERVAL_IN_MILLIS = 5l;ThreadLocal lockKeyThreadLocal = new ThreadLocal();ThreadLocal lockValThreadLocal = new ThreadLocal();ThreadLocal retryIntervalInMillisThreadLocal = new ThreadLocal();// 自增计数器AtomicLong counter = new AtomicLong();// Key是业务ID,Val是自增计数器的值Map<String, String> map = new ConcurrentHashMap<>();public LocalLock(Long retryIntervalInMillis) {this.retryIntervalInMillisThreadLocal.set(retryIntervalInMillis);}/*** 尝试获取锁* @param bizIdObj 业务ID* @return 是否获取锁成功*/public boolean tryLock(Object bizIdObj){String bizId = String.valueOf(bizIdObj);// 锁的值由两部分组成,一个是自增id,第二个是时间戳(用于持有锁时间超时检测,避免死锁)。String lockVal = generateNewLockVal();// putIfAbsent后,会返回旧的值,当返回null表明当前设置成功。if(map.putIfAbsent(bizId, lockVal) == null){// 存储业务ID,后面释放锁需要用到lockKeyThreadLocal.set(bizId);lockValThreadLocal.set(lockVal);return true;}return false;}/*** 阻塞(自旋)方式获取锁,直到获取锁成功* @param bizIdObj 业务ID*/public void lock(Object bizIdObj) throws InterruptedException {String bizId = String.valueOf(bizIdObj);// 锁的值由两部分组成,一个是自增ID,第二个是时间戳(用于持有锁时间超时检测,避免死锁)while (true){String lockVal = generateNewLockVal();if(map.putIfAbsent(bizId, lockVal) == null){// 获取锁成功。 putIfAbsent后,会返回旧的值,当返回null表明当前设置成功。// 存储业务ID,后面释放锁需要用到lockKeyThreadLocal.set(bizId);lockValThreadLocal.set(lockVal);return;}// 获取锁失败,休眠重试TimeUnit.MILLISECONDS.sleep(getRetryIntervalInMillis());}}/*** 释放锁*/public void unLock(){String lockVal = String.valueOf(lockValThreadLocal.get());String bizId = String.valueOf(lockKeyThreadLocal.get());if(!map.remove(bizId, lockVal)){// 当前锁为空或当前线程不是锁的持有者throw new IllegalMonitorStateException();}}/*** 释放锁(不抛移除)*/public void unLockSafety(){try {unLock();}catch (Exception e){log.error("释放本地锁失败!锁ID:{}, 错误信息:{}", lockKeyThreadLocal.get(), e.getMessage());}}/*** 强制释放锁*/public void unLockForce(String bizId){map.remove(bizId);}public String generateNewLockVal(){return System.currentTimeMillis() + "_" + counter.incrementAndGet();}public Long getRetryIntervalInMillis(){return retryIntervalInMillisThreadLocal.get() == null ? DEFAULT_RETRY_INTERVAL_IN_MILLIS : Long.parseLong(retryIntervalInMillisThreadLocal.get()+"");}
}

LocalLockClient.java:用户通过该类去创建 本地锁

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;public class LocalLockClient {private static Map<String, LocalLock> map = new ConcurrentHashMap<>();public static LocalLock getLockClient(String bizName){return getLockClient(bizName, 10L);}public static LocalLock getLockClient(String bizName, Long retryIntervalInMillis){if(map.containsKey(bizName)) return map.get(bizName);if(map.size() >= 10000) throw new RuntimeException("业务锁数量超过10000,请检查用法是否合理!");LocalLock localLock = new LocalLock(retryIntervalInMillis);LocalLock oldLocalLock = map.putIfAbsent(bizName, localLock);return oldLocalLock == null ? localLock : oldLocalLock;}
}

用法

// 创建本地锁(指定锁业务名称 以及 自选重试间隔ms)
LocalLock localLock = LocalLockClient.getLockClient("TransferMoney", 5L);
// 基于具体业务ID加锁
Long userId = 111;
localLock.lock(userId);
try {// TODO 进行你的业务操作...
}finally {// 释放本地锁localLock.unLockSafety();
}

:上面本地锁是不含死锁检测功能的,也就是是如果用户获取锁之后忘记释放锁,那么可能就会导致死锁出现。你可以在示例的基础上,再加上一个定时任务,定期检查map中所有value里的时间戳,如果距离当前系统时间超过一定值,如5分钟,那么可以认为是出现死锁,此时可以把出现死锁的Key从map中进行移除。

示例3.开发一个具备LRU功能的map

需求:有时候我们想更新 某个业务ID的最新时间,但又不确定我们即将进行更新的时间是最新时间,故而我们需要先查该业务ID在数据库的时间,然后和当前即将进行更新的时间进行比较,如果当前时间比数据库时间新,那么进行更新,否则放弃更新。
  针对这种需求,如果业务ID很多,并且更新频率很频繁,那么可能导致频繁查询数据库,导致性能降低。故而可以考虑基于内存方式,将频繁判断的业务ID以及其最新更新时间 放到内存中,同时又要限制业务ID的数量,既然存在限制数量,那么就存在业务ID淘汰策略,这里的淘汰策略我们采用LRU(最近最少使用)方式,这种方式符合了操作系统的时间局部性原理。

代码示例如下:

public class LRUCache<K, V> {private final int capacity;private final LinkedHashMap<K, V> cacheMap;private final Lock lock = new ReentrantLock();public LRUCache(int capacity) {this.capacity = capacity;// 初始化LinkedHashMap,并设置accessOrder为true以启用LRU功能,即每次更新或者访问map中的key,都会让该key所在Node节点重新被放到链表最后。this.cacheMap = new LinkedHashMap<K, V>(capacity, 0.75f, true) {@Overrideprotected boolean removeEldestEntry(Map.Entry<K, V> eldest) {// 当缓存大小超过容量时,移除最老的条目return size() > LRUCache.this.capacity;}};}public V get(K key) {// 由于 LinkedHashMap 不是线程安全的map,故而这块需要加锁。你也可以采用其他方式解决lock.lock();try {return cacheMap.getOrDefault(key, null);} finally {lock.unlock();}}public void put(K key, V value) {// 由于 LinkedHashMap 不是线程安全的map,故而这块需要加锁。你也可以采用其他方式解决lock.lock();try {cacheMap.put(key, value);} finally {lock.unlock();}}// 可以根据需要添加其他方法,如remove, clear等,并确保它们也是线程安全的public static void main(String[] args) {// 设置容量为3,一旦设置数量超过3个,那么就会触发淘汰策略LRUCache<Integer, String> lruCache = new LRUCache<>(3);lruCache.put(1, "one");   // 内部Key的顺序:1lruCache.put(2, "two");   // 内部Key的顺序:1、2lruCache.put(3, "three");  // 内部Key的顺序:1、2、3System.out.println(lruCache.get(1)); // 输出: ONE  。访问Key也会触发排序更新。内部Key的顺序:2、3、1lruCache.put(4, "four"); // 此时会移除key 2,因为2是最久未使用的。  内部Key的顺序:2、3、4System.out.println(lruCache.get(2)); // 输出: null}
}

注:设置、更新、访问 都会认为是 “使用了”,故而 会将操作的key所在的节点移动到链表最后。

http://www.xdnf.cn/news/695485.html

相关文章:

  • 【超详细教程】零基础本地部署DeepSeek-Coder-v2 16B!Ollama+GPU加速,100%跑通!
  • CCLINKIE转PROFINET:让执行器“丝滑”入网!
  • 脑机新手指南(一):BCILAB 脑机接口工具箱新手入门指南
  • 从新安全法到隐患判定标准:特种设备证件管理政策全梳理
  • vben-admin 2.8.0 版本修改 axios响应处理逻辑
  • MySQL:零基础入门(狂神版)
  • PyTorch安装Cuda版本选择
  • WMS系统选型与实施避坑手册
  • HarmonyOS 5 应用开发导读:从入门到实践
  • C++STL——map与set的使用
  • “顶点着色器”和“片元着色器”是先处理完所有顶点再统一进入片元阶段,还是一个顶点处理完就去跑它的片元?
  • 上传头像upload的简易方法,转base64调接口的
  • Spring AI 系列之使用 Spring AI 开发模型上下文协议(MCP)
  • maven编译时跳过test过程
  • MYSQL备份恢复知识:第六章:恢复原理
  • pythonocc hlr实例 deepwiki 显示隐藏线
  • Linux 系统入门篇四
  • SPEAR开源程序是用于逼真演示 AI 研究的模拟器
  • 【STM32】按键控制LED 光敏传感器控制蜂鸣器
  • HTTPS加密通信详解及在Spring Boot中的实现
  • 网盘解析工具v1.3.6,增加文件夹解析下载
  • 工业级安卓触控一体机在激光机械中的应用分析
  • 异步上传石墨文件进度条前端展示记录(采用Redis中String数据结构实现)
  • 杆塔倾斜在线监测装置:电力设施安全运行的“数字守卫”
  • Shell - ​​Here Document(HereDoc)
  • 今日行情明日机会——20250528
  • NC37 合并区间【牛客网】
  • 设计模式-依赖倒转原则
  • 微服务FallbackFactory和FallbackClass
  • MCP Server的五种主流架构:从原理到实践的深度解析