同步操作将从 LawlietPersonal/component-lock 强制同步,此操作会覆盖自 Fork 仓库以来所做的任何修改,且无法恢复!!!
确定后同步将在后台操作,完成时将刷新页面,请耐心等待。
这是一个springboot插件,可直接集成到任何springboot工程进行使用
它封装了锁的使用,尤其是并发场景下的分布式互斥锁,可以通过注解的方式对业务无侵入的施加锁
在加锁的同时,还可以指定被锁定业务逻辑的时间长度,如果时间到而业务逻辑尚未执行完毕,可通过已提供的策略进行自动处理(直接抛出异常、忽略等等)
目前该插件实现了针对juc(原生的java.concurrent.lock)、redis(基于redisson,同时,支持单节点redis,sentinel模式redis,cluster集群模式的redis)、zookeeper的锁机制,juc只适用于单机服务,redis和zookeeper可用于施加分布式锁
<dependency> <groupId>com.gitee.lawlietpersonal</groupId> <artifactId>component-lock</artifactId> <version>1.4.0</version> </dependency>
compile("com.gitee.lawlietpersonal:component-lock:1.4.0")
其中版本号可选用对应tag的版本号,你可以选择release版本(已发布到maven中央仓库的稳定版),也可以使用SNAPSHOT版本
在resources目录下
默认的application.yml application.properties ...
或者是你自定义能够识别的xxx.yml xxx.properties ...
等配置文件中指定如下属性
# 根节点配置 lock: enable: true #是否开启,若不开启,则springboot启动时不会扫描并加载相关的配置 type: juc # 必须。锁的类型,目前支持 juc redis zookeeper # 锁对象的缓存容量,超过该容量,将触发lfu算法,保证容量始终处于该阈值以内,防止扩容带来的吞吐量降低 # 默认是128 但需要根据你们业务的实际情况,合理的设置容量,太小的话会频繁触发lfu算法,影响被加锁的业务执行效率,太大的话会占用过多的内存资源 lockCacheCapacity: 1024 # 为了保证缓存的锁对象容量在 lock.lockCacheCapacity * 0.75 范围内不会扩容,所指定的清理算法,目前支持lfu(默认),lru strategyForCapacity: lru # 这是用来控制主线程fork出子线程总体数量的参数 # 因为每调用业务接口之前,在lock之后,主线程会fork出一个子线程去执行业务逻辑,而主线程处于阻塞状态 # 这样做的目的是便于控制处于锁状态下业务逻辑的执行时长等 # 所以需要控制进程中总体的线程数 # 一般来说,每一个锁对应一个key,需要参考应用中key的总数,然后结合当前服务器和应用资源合理设置 # 若不配置,该值默认是 128 forkTaskQueueSize: 256 zookeeper: # 当type = zookeeper时必须,zookeeper锁的配置 # 必须,zookeeper集群或单机的服务地址 connectionUrl: 192.168.240.129:2182,192.168.216.130:2182,192.168.216.131:2182 # 非必须,zookeeper中锁的节点名称,当为空时,该值默认为 businessLock # zookeeper中会在根节点下默认创建一个用于该工程的锁的节点目录,名为 /lock-${applicationName}-${rootPath} rootPath: lawlietLock redis: # 当type = redis时必须,redis锁的配置 注意:single、sentinel、cluster配置三者只能配置一种 # # 非必须 密码 # password: 123456 single: # 单节点时的地址 host: 127.0.0.1 # 单节点时的端口 port: 6379 # # 哨兵部署的模式配置 # sentinel: # # 非必须 master节点名称,默认redisMasterNode # masterName: xxx # # 必须 哨兵地址 # addresses: # - 192.168.1.104:6371 # - 192.168.1.104:6372 # # 集群的部署模式 # cluster: # # 非必须 集群状态扫描间隔时间,单位是毫秒,默认2000ms # scanInterval: 2000 # # 必须 集群中各个主从节点的地址 # addresses: # - 192.168.2.104:6371 # - 192.168.2.104:6372
- 当你的lock.type设置为redis时,redis服务的部署方式 -> single、sentinel、cluster 三选一,有且仅有一种
- 如果没有密码,则不要配置lock.redis.password
该注解使用仅限于方法,key是你针对这项业务逻辑自定义的key,用于区分业务
而strategyOfExpired目前有2种策略(详见常量类注释)
此外,对于stopOnExpired 属性,当设置了超时时间,可以配置超时后是否继续执行业务代码,默认继续执行
/** * 申请加锁的注解,注解于方法上,则整个方法会根据配置锁的类型进行加锁 * @author xiangjz * @version 1.0 * @date 2020/9/3 11:03 */ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) @Inherited public @interface LockAcquire { /** * 自定义的业务lock.key * @return */ String key() default ""; /** * 锁的过期的时间 * 默认永不过期 * 单位 ms * @return */ long expire() default -1L; /** * 当锁超时仍没有被释放释放时的回调类型 * @return */ int strategyOfExpired() default LockExpireStrategyTypeConstant.LOCK_EXPIRE_STRATEGY_IGNORE; /** * 当expire > 0,当超时时,是否立即停止尚未完成的业务逻辑 * @return */ boolean stopOnExpired() default false; /** * 当前进程中,fork的子线程之前尝试申请资源的耗时(ms) * 若<0,则视为永久等待可用资源 * @return */ long expireOnForkingFull() default -1L; }
该注解作用于方法中的参数,key和keys用于制定某一参数中的字段值,order是最终生成锁key时的拼接顺序
需要注意的是,一个方法可以有N个参数,你可以根据参数(基本类型:String, Integer,int)的值,或者参数(Object, Map)中字段的值去根据你指定的顺序生成不同的锁key,达到灵活配置分布式锁的目的
引入ognl技术,支持无限层级的动态lock key的配置,例如:
@LockAcquire(key = "lockTest2-", expireOnForkingFull = 100)
public String lock2(@LockCustomParam(key = "key1.key11") Map<String, Object> params,
@LockCustomParam(key = "name", keys = {"child.child.child.name"}) LockTestEntity entity) {
System.out.println("开始获取锁2---");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("执行完毕2");
return "2";
}
值得注意的是:目前仍不支持Collection类型的参数
package com.qingzhu.component.lock.annotation.business; import java.lang.annotation.*; /** * 申请加锁的注解,注解于方法参数上,需要在该方法上有 LockAcquire 注解 * @author xiangjz * @version 1.0 * @date 2020/9/3 11:03 */ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER}) @Inherited public @interface LockCustomParam { /** * 根据参数字段值而动态拼接的key * 同一个注解中,优先级高于keys * @return */ String key() default ""; /** * key() 拼接成lock.key的顺序 * 当有多个LockCustomParam注解时,order升序拼接 * @return */ int order() default 0; /** * 注解于一个参数,但需要指定参数中多个字段时 * 按照数组顺序,进行lock.key的拼接 * 同一个注解中,优先级低于key * @return */ String[] keys() default ""; }
目前,本插件只支持针对方法级别加锁,也就是说,如果你要针对某一段业务逻辑加锁,你必须把它抽离出来,放到一个spring bean的方法中
package com.qingzhu.biz.labor.lock.service; import com.alibaba.fastjson.JSONObject; import com.qingzhu.component.lock.annotation.business.LockAcquire; import com.qingzhu.component.lock.annotation.business.LockCustomParam; import org.springframework.stereotype.Component; import java.util.Map; @Service public class LockTestService { @LockAcquire(key = "testBusiness", expire = 10, strategyOfExpired = 0) public String doBusiness(@LockCustomParam(key = "fdfasdf") Integer a, @LockCustomParam long b, @LockCustomParam(keys = {"", "key1", "key2"}) Map<String, Object> map, @LockCustomParam(key = "name", keys = "age") BusinessEntity entity) { System.out.println(a); System.out.println(b); for (int i = 0; i < 100000; i++) { log.info(JSONObject.toJSONString(entity) + i); } System.out.println(JSONObject.toJSONString(map)); System.out.println(JSONObject.toJSONString(entity)); return "success"; } @LockAcquire(key = "testBusiness", expire = 2000, strategyOfExpired = 0, stopOnExpired = true, expireOnForkingFull = 100) public String doBusiness2(@LockCustomParam(key = "fdfasdf") Integer a, @LockCustomParam long b, @LockCustomParam(keys = {"", "key1", "key2"}) Map<String, Object> map, @LockCustomParam(key = "name", keys = "age") BusinessEntity entity) throws InterruptedException { System.out.println(a); System.out.println(b); Thread.sleep(3000); System.out.println(JSONObject.toJSONString(map)); System.out.println(JSONObject.toJSONString(entity)); return "success"; } }
如果你觉得麻烦,不想将已有的业务代码单独提炼到一个类中,你可以像如下例子一样,直接通过注入 LockExecutor 类的方式,通过lock和unlock的方式,包裹你的业务代码,完成分布式锁
注意:这种方式不支持加业务的超时时间
同时,你需要自定义你的lockKey,自行拼接字符串
@Service @Slf4j public class LockTestService { private final LockExecutor lockExecutor; public LockTestService(LockExecutor lockExecutor) { this.lockExecutor = lockExecutor; } public String doBusiness3(BusinessEntity entity) { LockEntity lock = null; try { lock = lockExecutor.lock("testBusiness-" + entity.getName()); // 你的业务代码开始 for (int i = 0; i < 1000000; i++) { System.out.println(JSONObject.toJSONString(entity) + i); } // 你的业务代码结束 } finally { if(lock != null) { lock.unlock(); } } return "success"; } }
由于使用了spring的aop来实现加锁,所以你不可以在其它使用到动态代理的类的方法中,直接将该方法写到本类,并直接调用,这将使得后者的aop不生效,原因是动态代理在调用本类中的其它方法时,直接使用this.xxx(),故无法进行第二次代理
该属性需要在配置文件中,使用lock.lockCacheCapacity配置锁缓存的容量(默认是128)
插件内使用ConcurrentHashMap实现对锁对象的缓存,而capacity是该容器的容量,会在一开始初始化完成,并且不会扩容,当你产生的锁对象即将超过capacity * 0.75f时,会触发lfu算法,将最近最少频次使用的锁对象清除(清除至capacity的一半),使得该缓存始终保持不扩容的状态,保证整个系统的吞吐量和稳定性
该属性需要在配置文件中,使用lock.forkTaskQueueSize配置锁缓存的容量(默认是128)
这是用来控制主线程fork出子线程总体数量的参数
因为每调用业务接口之前,在lock之后,主线程会fork出一个子线程去执行业务逻辑,而主线程处于阻塞状态。这样做的目的是便于控制处于锁状态下业务逻辑的执行时长等通用逻辑
但是如果每次调用业务逻辑之前都去fork一个子线程,会造成应用内线程随着并发数增加(尤其是根据参数内的key值动态生成锁的业务)而无限增加线程,可能造成系统吞吐降低甚至瘫痪
因此在每次lock之后,需要申请一下fork thread资源,如果没有申请到,则意味着fork出来的子线程数量达到阈值,此时会怎样,下面4.3.2介绍
这个属性是配合lock.forkTaskQueueSize用的,如果不设置,默认是-1
意思就是,每次lock之后,需要申请fork thread资源,在申请资源的同时,需要设置一个expireOnForkingFull(超时时间)
如果expireOnForkingFull是 -1,则永久阻塞等待,直到被signal唤醒
如果expireOnForkingFull >= 0,则在申请资源的时候,只在expireOnForkingFull时间内进行申请,超过时间还没有申请到资源,则会直接抛出LockExpiredException异常,可以在外层业务代码中捕获处理
因为spring的aop最终原理是通过拦截器链层层调用,所以如果你自定义了很多aop注解,或者直接使用spring自带的注解(比如:@Transactional 事务注解),是完全没有问题的。但如果你的其它自定义注解是通过自己用动态代理的方式实现的,就会有问题,原因如上 4.1 所述
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。