AOP在PowerJob中的使用,缓存锁保证并发安全,知识细节全总结
这是一篇简简单单的文章,需要你简简单单看一眼就好,如果有不明白的地方,欢迎留言讨论。

在之前的文章中出现过一次AOP的使用,就是在运行任务之前,需要判断一下,触发该任务执行的server,是不是数据库中对应任务所在app的直接server,使用的是注解@DesignateServer,本篇文章是从另一个注解,再一次顺一遍AOP的使用,而且本篇文章的注解,再一次用到了可重入锁ReentrantLock,这个也是之前的文章中说的内容,可以再熟悉一遍,本篇文章的入口就是注解——UseCacheLock。
从名字来看,该注解是一个使用缓存时的一个锁,该类位于tech.powerjob.server.core.lock包下,用来修饰方法,在运行时执行的,源码如下:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCacheLock {String type();String key();int concurrencyLevel();
}
type:从使用的代码处得出,目前只有两种一种是processJobInstance,另一种是processWfInstance
key:主要是任务id或者任务实例id,还有工作流id。其中任务id或者任务实例id的选取,是通过一个表达式来判断得出的。
concurrencyLevel:缓存要用到的字段,允许同时并发执行的写操作数。
UseCacheLock 的使用场景
该注解在powerjob中一共使用了8次,其中2次出现在任务的派发,6次出现在工作流的操作中,这一次就选在任务的派发,来讲一下该注解的使用场景。
@UseCacheLock(type = "processJobInstance", key = "#jobInfo.getMaxInstanceNum() > 0 || T(tech.powerjob.common.enums.TimeExpressionType).FREQUENT_TYPES.contains(#jobInfo.getTimeExpressionType()) ? #jobInfo.getId() : #instanceId", concurrencyLevel = 1024)
public void dispatch(JobInfoDO jobInfo, Long instanceId) {... ...
}
方法内部的代码不重要,主要是来看方法上面的注解,里面的三个关键字分别是
processJobInstance
#jobInfo.getMaxInstanceNum() > 0 || T(tech.powerjob.common.enums.TimeExpressionType).FREQUENT_TYPES.contains(#jobInfo.getTimeExpressionType()) ? #jobInfo.getId() : #instanceId
1024
第一个和最后一个没什么好说的,主要说一说中间那一条长长的表达式,该表达式通过解读,就是判断最大同时运行任务数是否大于0,以及任务的时间表达式类型是不是FIX_RATE或者FIX_DELAY。这一表达式可以说,除非人为的将MaxInstanceNum设置为0,否则该条数据默认值就是1,也就是说这个表达式,不负责任的说,99.99%都是真,也就是说都会使用JobId作为key值。
按照代码来看,就是当任务在派发的时候,会使用到该注解,为的是防止该方法同时运行派发同一个任务,如果是同时派发两个不同的任务,就不会有影响,毕竟在派发的过程中涉及到了对任务实例的数据修改,如果两个同时进行,确实会产生问题。
UseCacheLock 的AOP处理
处理该注解的类个该类在同一个包,处理的源代码如下所示:
@Around(value = "@annotation(useCacheLock)")public Object execute(ProceedingJoinPoint point, UseCacheLock useCacheLock) throws Throwable {Cache<String, ReentrantLock> lockCache = lockContainer.computeIfAbsent(useCacheLock.type(), ignore -> {int concurrencyLevel = useCacheLock.concurrencyLevel();log.info("[UseSegmentLockAspect] create Lock Cache for [{}] with concurrencyLevel: {}", useCacheLock.type(), concurrencyLevel);return CacheBuilder.newBuilder().initialCapacity(300000).maximumSize(500000).concurrencyLevel(concurrencyLevel).expireAfterWrite(30, TimeUnit.MINUTES).build();});final Method method = AOPUtils.parseMethod(point);Long key = AOPUtils.parseSpEl(method, point.getArgs(), useCacheLock.key(), Long.class, 1L);final ReentrantLock reentrantLock = lockCache.get(String.valueOf(key), ReentrantLock::new);long start = System.currentTimeMillis();reentrantLock.lockInterruptibly();try {long timeCost = System.currentTimeMillis() - start;if (timeCost > SLOW_THRESHOLD) {final SlowLockEvent slowLockEvent = new SlowLockEvent().setType(SlowLockEvent.Type.LOCAL).setLockType(useCacheLock.type()).setLockKey(String.valueOf(key)).setCallerService(method.getDeclaringClass().getSimpleName()).setCallerMethod(method.getName()).setCost(timeCost);monitorService.monitor(slowLockEvent);log.warn("[UseSegmentLockAspect] wait lock for method({}#{}) cost {} ms! key = '{}', args = {}, ", method.getDeclaringClass().getSimpleName(), method.getName(), timeCost,key,JSON.toJSONString(point.getArgs()));}return point.proceed();} finally {reentrantLock.unlock();}
}
代码看着挺长的,但是内容其实没有多少,可以一步一步拆开来看。
缓存的创建

第一步通过type来获取缓存,从文章开头我们知道,这个type就两个类型,processJobInstance就是用来派发任务的,processWfInstance就是用来操作工作流任务的,该代码里面就是processJobInstance,如果缓存存在,直接拿来用,如果不存在,则创建缓存,来看一眼创建缓存的代码:
CacheBuilder.newBuilder().initialCapacity(300000).maximumSize(500000).concurrencyLevel(concurrencyLevel).expireAfterWrite(30, TimeUnit.MINUTES).build();
这个代码的大意就是创建一个有如下属性的缓存,缓存有效时间是30分钟(expireAfterWrite(30, TimeUnit.MINUTES)),这就像鱼有7秒记忆一样,这个缓存只能记录30分钟,过期失效。缓存的最大条目数是50万(maximumSize(500000))。指定用于缓存的hash table最低总规模是300000,允许同时并发操作数是concurrencyLevel,也就是传进来的1024.
key值的获取
第二步就是获取key值,该值主要是为了获取可重入锁用的,获取该值的源代码如下所示:
final Method method = AOPUtils.parseMethod(point);
Long key = AOPUtils.parseSpEl(method, point.getArgs(), useCacheLock.key(), Long.class, 1L);
从这个代码可以看到,用到了AOPUtil这个工具类的两个方法,第一个方法是解析出当前的方法,第二个是获取key值,这个AOPUtil在tech.powerjob.server.common.utils包下。解析方法的源码如下,备注解释各代码的目的:
public static Method parseMethod(ProceedingJoinPoint joinPoint) {//获取接入点的签名,此处必须是方法的签名,否则会报异常Signature pointSignature = joinPoint.getSignature();if (!(pointSignature instanceof MethodSignature)) {throw new IllegalArgumentException("this annotation should be used on a method!");}//强转成方法的签名MethodSignature signature = (MethodSignature) pointSignature;//获取方法Method method = signature.getMethod();//如果方法所处的类是一个interfaceif (method.getDeclaringClass().isInterface()) {try {//通过IoC容器获取目标对象,然后再获取对象的方法method = joinPoint.getTarget().getClass().getDeclaredMethod(pointSignature.getName(), method.getParameterTypes());} catch (SecurityException | NoSuchMethodException e) {ExceptionUtils.rethrow(e);}}return method;
}
获取到了方法之后,就是获取key值,源代码如下,备注解释各代码的目的:
public static <T> T parseSpEl(Method method, Object[] arguments, String spEl, Class<T> clazz, T defaultResult) {//获取到方法的参数值类型String[] params = discoverer.getParameterNames(method);assert params != null;//创建数据上下文EvaluationContext context = new StandardEvaluationContext();for (int len = 0; len < params.length; len++) {//将param[len] = arguments[len]context.setVariable(params[len], arguments[len]);}try {//执行表达式,也就是前面#jobInfo.getMaxInstanceNum() > 0 || T(tech.powerjob.common.enums.TimeExpressionType).FREQUENT_TYPES.contains(#jobInfo.getTimeExpressionType()) ? #jobInfo.getId() : #instanceIdExpression expression = parser.parseExpression(spEl);//返回表达式执行的结果,以clazz设置的类型返回return expression.getValue(context, clazz);} catch (Exception e) {log.error("[AOPUtils] parse SpEL failed for method[{}], please concat @tjq to fix the bug!", method.getName(), e);return defaultResult;}
}
经过以上两步,key值就获取完毕了
加锁
加锁的源代码如下所示,就是如果缓存里面保存了锁,就直接拿到,如果没有,就new一个出来,然后就启动锁,
那两条时间主要是记录加锁的时间,如果时间过长就要记录一条日志,记录加锁慢时的任务信息。
final ReentrantLock reentrantLock = lockCache.get(String.valueOf(key), ReentrantLock::new);
long start = System.currentTimeMillis();
reentrantLock.lockInterruptibly();
try {long timeCost = System.currentTimeMillis() - start;... ...
}
... ...
加锁结束之后,就可以执行注解修饰的方法了,执行就是下面这一行:
point.proceed();
执行结束之后,将锁打开就OK了。
总结

本篇文章涉及的知识主要是AOP的使用,可重入锁的使用,IoC容器相关,Spring的表达式的使用,缓存Cache的创建,每一个知识点都够我喝一壶了,所以大家如果想要了解这些知识的细节,请自行搜索去查想要了解的内容,如果你懒得查,也可以问我,当然我也懒,回不回答就看我心情了,哼,我外号就叫不高兴,所以大家看着办吧。
相关文章:
AOP在PowerJob中的使用,缓存锁保证并发安全,知识细节全总结
这是一篇简简单单的文章,需要你简简单单看一眼就好,如果有不明白的地方,欢迎留言讨论。 在之前的文章中出现过一次AOP的使用,就是在运行任务之前,需要判断一下,触发该任务执行的server,是不是数…...
对账平台设计
背景 随着公司业务的蓬勃发展,交易履约清结算业务的复杂性也在不断的增高,资金以及各种数据的一致性和准确性也变得越发重要。 以交易链路为例,存在着如下一些潜在的不一致场景: 订单支付成功了,但是订单状态却还是“…...
JavaEE进阶第五课:SpringBoot的创建和使用
上篇文章介绍了Bean 作用域和生命周期,这篇文章我们将会介绍SpringBoot的创建和使用 目录1.为什么要学习StringBoot1.1什么是SpringBoot1.2SpringBoot的优点2.如何用Idea创建SpringBoot项目3.项目目录介绍和运行3.1输入Helloworld结尾1.为什么要学习StringBoot 在前…...
我带过的一名C++实习生——Z同学
刚开始带Z同学,吃饭聊天时,我顺便了解了下他的擅长:linux平台下C、C网络编程。 接下来的实习,主要分为两个阶段:小组公共培训和项目实训。 小组公共培训为期2周,主要学习和了解公司文化制度,讲师…...
面试题13. 机器人的运动范围
面试题13. 机器人的运动范围 难度:middle\color{orange}{middle}middle 题目描述 地上有一个 mmm 行 nnn 列的方格,从坐标 [0,0][0,0][0,0] 到坐标 [m−1,n−1][m-1,n-1][m−1,n−1] 。一个机器人从坐标 [0,0][0, 0][0,0] 的格子开始移动,它…...
LeetCode189_189. 轮转数组
LeetCode189_189. 轮转数组 一、描述 给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。 示例 1: 输入: nums [1,2,3,4,5,6,7], k 3 输出: [5,6,7,1,2,3,4] 解释: 向右轮转 1 步: [7,1,2,3,4,5,6] 向右轮转 2 步: [6,7,1,…...
java Files和Paths的使用详解 附有使用demo
前言 Java Files和Paths是Java 7中引入的新API,用于处理文件和目录。Files类提供了许多有用的静态方法来操作文件和目录,而Path类则表示文件系统中的路径。 创建文件和目录 在Java中创建文件和目录非常简单。我们可以使用Files类的createFile()方法和…...
如何使用ApacheTomcatScanner扫描Apache Tomcat服务器漏洞
关于ApacheTomcatScanner ApacheTomcatScanner是一个功能强大的Python脚本,该脚本主要针对Apache Tomcat服务器安全而设计,可以帮助广大研究人员轻松扫描和检测Apache Tomcat服务器中的安全漏洞。 功能介绍 1、支持使用多线程Worker搜索Apache Tomcat服…...
js中的定时器 setTimeout()和setInterval()
JavaScript 定时器,有时也称为“计时器”,用来在经过指定的时间后执行某些任务,类似于我们生活中的闹钟。 在 JavaScript 中,我们可以利用定时器来延迟执行某些代码,或者以固定的时间间隔重复执行某些代码。例如&…...
【吃透Js】深入学习浅拷贝和深拷贝
一、JavaScript数据类型原始类型对象类型二、原始类型和对象类型的区别1.原始类型2.引用类型3.复制4.比较5.值传递三、浅拷贝概念实现方法四、深拷贝概念五、浅拷贝、深拷贝和赋值的区别浅拷贝和赋值六、小结想要真正搞明白深浅拷贝,你必须要熟练掌握赋值、对象在内…...
AUTOSAR为啥要开发新的社区商业模式?
总目录链接>> AutoSAR入门和实战系列总目录 文章目录1 自适应平台架构中的集群更新1.1 ara::diag 服务(诊断)更新1.2 信号到服务映射和自动驾驶接口让我们讨论一下信号到服务映射服务:Automated Driving Interface:2 车载应用商店概念本文介绍Re…...
数据结构和算法面试常见题必考以及前端面试题
1.数据结构和算法 1.1 反转单向链表 public class Node {public int value;public Node next; }public Node reverseList(Node head) {Node pre null;Node next null;while (head ! null) {next head.next;head.next pre;pre head;head head.next}return pre; }1.2 在顺…...
一文解决Python所有报错
前言 Python是一种强大的编程语言,但是它也有一些报错,这些报错可能会让你感到困惑。本文将介绍如何解决Python中的常见报错。 首先,让我们来看看Python中最常见的报错:SyntaxError。这种报错表明你的代码中有语法错误,…...
LeetCode 1237. Find Positive Integer Solution for a Given Equation【双指针,二分,交互】
本文属于「征服LeetCode」系列文章之一,这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁,本系列将至少持续到刷完所有无锁题之日为止;由于LeetCode还在不断地创建新题,本系列的终止日期可能是永远。在这一系列刷题文章…...
【C语言】结构体进阶
一、结构体 1. 结构体的声明 (1) 结构的基础知识 结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。(2)结构的声明 struct tag {member-list; }variable-list;例如描述一个学生&#x…...
全志T3+FPGA国产核心板——Pango Design Suite的FPGA程序加载固化
本文主要基于紫光同创Pango Design Suite(PDS)开发软件,演示FPGA程序的加载、固化,以及程序编译等方法。适用的开发环境为Windows 7/10 64bit。 测试板卡为全志T3+Logos FPGA核心板,它是一款基于全志科技T3四核ARM Cortex-A7处理器 + 紫光同创Logos PGL25G/PGL50G FPGA设计…...
深度学习之 imgaug (图像增强)学习笔记
深度学习之 imgaug (图像增强)前言1\. 安装和卸载2\. 示例2.1 基本使用2.2 包含常用的变换示例3 Augmenters常用函数3.1 iaa.Sequential()3.2 iaa.someOf()3.3 iaa.OneOf()3.4 iaa.Sometimes()3.5 iaa.WithColorspace()3.6 iaa.WithChannels()3.7 iaa.No…...
mysql字符串等值查询中条件字段值末尾有空格也能查到数据问题
一、事故还原 我们仍然使用学生信息表,但是我们只需要保留两个字段即可: CREATE TABLE student_info (id int(11) NOT NULL AUTO_INCREMENT COMMENT 学号,name varchar(20) CHARACTER SET utf8 DEFAULT NULL COMMENT 姓名, PRIMARY KEY (id) ) ENGINEIn…...
一个关于事件溯源Event Sourcing的小荔枝,Golang实现
最后更新于2023年3月1日 10:23:13 参考的这个文章:https://martinfowler.com/eaaDev/EventSourcing.html 用C sharp实现的,我改写成Golang了 最简单的例子 func main() {eProc : NewEventProcessor()//refact : Cargo{Name: "Refactoring"}…...
Vue3 组合式函数,实现minxins
截至目前,组合式函数应该是在VUE 3应用程序中组织业务逻辑最佳的方法。它让我们可以把一些小块的通用逻辑进行抽离、复用,使我们的代码更易于编写、阅读和维护。 一. 什么是“组合式函数”? 根据官方文档说明,在 Vue 应用的概念中…...
tkinter表格神器tkintertable实战:5分钟搞定可拖拽编辑的数据表格(附完整代码)
tkinter表格神器tkintertable实战:5分钟搞定可拖拽编辑的数据表格(附完整代码) 在Python GUI开发中,表格控件一直是刚需但实现起来又颇为棘手的组件。传统tkinter自带的Treeview虽然能勉强实现表格功能,但在交互体验上…...
从51job爬虫案例出发,聊聊如何用Selenium优雅地绕过前端反爬机制
从51job爬虫案例解析Selenium反反爬高阶策略 当招聘网站的前端技术不断升级,传统爬虫手段逐渐失效时,如何让自动化工具的行为更像真实用户?这个问题困扰着许多中高级开发者。以51job为例,其动态加载、URL不变的设计让常规爬虫束手…...
Word自动编号的隐藏玩法:用题注和交叉引用,打造能“自我修复”的智能文档
Word文档工程化:构建自动编号与交叉引用的智能系统 在技术文档撰写过程中,最令人头疼的莫过于图表编号的维护。当你在200页的文档中插入新图表时,手动编号意味着要逐个修改后续所有编号和引用——这种痛苦只有经历过的人才懂。但很少有人意识…...
深度残差收缩网络(pytorch)框架+时序信号转格拉姆角场二维图; 将时序信号转换为二维图
深度残差收缩网络(pytorch)框架时序信号转格拉姆角场二维图; 将时序信号转换为二维图,使用深度残差收缩网络进行特征提取;训练后保存训练文件便于二次使用。 代码清晰,模型、训练、数据读取分类明显&#x…...
3KW无线充电系统设计:开环控制与闭环控制的MATLAB Simulink仿真模型,采用双边L...
3KW无线充电系统设计(MATLAB simulink仿真模型) 控制方式:开环控制闭环控制 拓扑结构:双边LCC拓扑结构 输入电压:750V 输出电压:400V 传输功率:3KW 最近在折腾一个3KW无线充电系统的仿真项目&am…...
CasRel模型LaTeX学术论文辅助工具:自动提取相关工作和贡献
CasRel模型LaTeX学术论文辅助工具:自动提取相关工作和贡献 每次打开一篇新的学术论文,尤其是那些动辄几十页的综述或顶会文章,你是不是也有点头大?密密麻麻的文字里,最关键的信息——“别人做了什么”、“他们有什么不…...
收藏!2026非科班/转行小白必看:3步切入AI大模型,月薪30w+实战路径
2026年的职场赛道,AI大模型依旧是绝对的“黄金风口”。 最新行业报告显示,AI相关岗位需求逆势增长37%,薪资领跑全行业,大厂校招起薪普遍突破25k。但一个残酷的现实是: 太多非科班、半路转行的程序员,还在门…...
模拟地和数字地到底怎么接?从ADC设计误区讲起,用磁珠还是直接铺铜?
数模混合电路设计中的地平面处理:从ADC噪声抑制到系统级EMC优化 1. 数模混合电路的接地困局:当磁珠成为噪声放大器 在24位ADC采样电路中,工程师老张遇到了一个诡异现象:当输入信号低于1mV时,采集数据会出现周期性毛刺。…...
保姆级教程:手把手教你将若依(RuoYi)项目从Java 8迁移到Java 17(含Spring Boot 3升级)
保姆级教程:手把手教你将若依(RuoYi)项目从Java 8迁移到Java 17(含Spring Boot 3升级) 最近几年Java生态发生了翻天覆地的变化,从Java 8到Java 17不仅仅是版本号的跳跃,更是一次技术栈的全面革新。作为国内广泛使用的…...
DeepSeek-R1-Distill-Qwen-7B创意写作展示:从诗歌到短篇小说
嗯,用户需要一篇关于DeepSeek-R1-Distill-Qwen-7B在创意写作方面效果展示的技术博客。根据标题和场景判断,这属于效果展示类文章,重点是通过实际案例展示模型在文学创作上的能力。 需要突出模型的创意写作效果,包括诗歌、微型小说…...
