简单注解实现集群同步锁(spring+redis+注解)
发布时间 - 2026-01-10 22:39:55 点击率:次互联网面试的时候,是不是面试官常问一个问题如何保证集群环境下数据操作并发问题,常用的synchronized肯定是无法满足了,或许你可以借助for update对数据加锁。本文的最终解决方式你只要在方法上加一个@P4jSyn注解就能保证集群环境下同synchronized的效果,且锁的key可以任意指定。本注解还支持了锁的超时机制。

本文需要对Redis、spring和spring-data-redis有一定的了解。当然你可以借助本文的思路对通过注解对方法返回数据进行缓存,类似com.google.code.simple-spring-memcached的@ReadThroughSingleCache。
第一步: 介绍两个自定义注解P4jSyn、P4jSynKey
P4jSyn:必选项,标记在方法上,表示需要对该方法加集群同步锁;
P4jSynKey:可选项,加在方法参数上,表示以方法某个参数作为锁的key,用来保证更多的坑,P4jSynKey并不是强制要添加的,当没有P4jSynKey标记的情况下只会以P4jSyn的synKey作为锁key。
package com.yaoguoyin.redis.lock;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* <b>同步锁:</b><br/>
* 主要作用是在服务器集群环境下保证方法的synchronize;<br/>
* 标记在方法上,使该方法的执行具有互斥性,并不保证并发执行方法的先后顺序;<br/>
* 如果原有“A任务”获取锁后任务执行时间超过最大允许持锁时间,且锁被“B任务”获取到,在“B任务”成功货物锁会并不会终止“A任务”的执行;<br/>
* <br/>
* <b>注意:</b><br/>
* 使用过程中需要注意keepMills、toWait、sleepMills、maxSleepMills等参数的场景使用;<br/>
* 需要安装redis,并使用spring和spring-data-redis等,借助redis NX等方法实现。
*
* @see com.yaoguoyin.redis.lock.P4jSynKey
* @see com.yaoguoyin.redis.lock.RedisLockAspect
*
* @author partner4java
*
*/
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface P4jSyn {
/**
* 锁的key<br/>
* 如果想增加坑的个数添加非固定锁,可以在参数上添加@P4jSynKey注解,但是本参数是必写选项<br/>
* redis key的拼写规则为 "RedisSyn+" + synKey + @P4jSynKey<br/>
*
*/
String synKey();
/**
* 持锁时间,超时时间,持锁超过此时间自动丢弃锁<br/>
* 单位毫秒,默认20秒<br/>
* 如果为0表示永远不释放锁,在设置为0的情况下toWait为true是没有意义的<br/>
* 但是没有比较强的业务要求下,不建议设置为0
*/
long keepMills() default 20 * 1000;
/**
* 当获取锁失败,是继续等待还是放弃<br/>
* 默认为继续等待
*/
boolean toWait() default true;
/**
* 没有获取到锁的情况下且toWait()为继续等待,睡眠指定毫秒数继续获取锁,也就是轮训获取锁的时间<br/>
* 默认为10毫秒
*
* @return
*/
long sleepMills() default 10;
/**
* 锁获取超时时间:<br/>
* 没有获取到锁的情况下且toWait()为true继续等待,最大等待时间,如果超时抛出
* {@link java.util.concurrent.TimeoutException.TimeoutException}
* ,可捕获此异常做相应业务处理;<br/>
* 单位毫秒,默认一分钟,如果设置为0即为没有超时时间,一直获取下去;
*
* @return
*/
long maxSleepMills() default 60 * 1000;
}
package com.yaoguoyin.redis.lock;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* <b>同步锁 key</b><br/>
* 加在方法的参数上,指定的参数会作为锁的key的一部分
*
* @author partner4java
*
*/
@Target({ ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface P4jSynKey {
/**
* key的拼接顺序
*
* @return
*/
int index() default 0;
}
这里就不再对两个注解进行使用上的解释了,因为注释已经说明的很详细了。
使用示例:
package com.yaoguoyin.redis.lock;
import org.springframework.stereotype.Component;
@Component
public class SysTest {
private static int i = 0;
@P4jSyn(synKey = "12345")
public void add(@P4jSynKey(index = 1) String key, @P4jSynKey(index = 0) int key1) {
i++;
System.out.println("i=-===========" + i);
}
}
第二步:切面编程
在不影响原有代码的前提下,保证执行同步,目前最直接的方式就是使用切面编程
package com.yaoguoyin.redis.lock;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.BoundValueOperations;
import org.springframework.data.redis.core.RedisTemplate;
/**
* 锁的切面编程<br/>
* 针对添加@RedisLock 注解的方法进行加锁
*
* @see com.yaoguoyin.redis.lock.P4jSyn
*
* @author partner4java
*
*/
@Aspect
public class RedisLockAspect {
@Autowired
@Qualifier("redisTemplate")
private RedisTemplate<String, Long> redisTemplate;
@Around("execution(* com.yaoguoyin..*(..)) && @annotation(com.yaoguoyin.redis.lock.P4jSyn)")
public Object lock(ProceedingJoinPoint pjp) throws Throwable {
P4jSyn lockInfo = getLockInfo(pjp);
if (lockInfo == null) {
throw new IllegalArgumentException("配置参数错误");
}
String synKey = getSynKey(pjp, lockInfo.synKey());
if (synKey == null || "".equals(synKey)) {
throw new IllegalArgumentException("配置参数synKey错误");
}
boolean lock = false;
Object obj = null;
try {
// 超时时间
long maxSleepMills = System.currentTimeMillis() + lockInfo.maxSleepMills();
while (!lock) {
long keepMills = System.currentTimeMillis() + lockInfo.keepMills();
lock = setIfAbsent(synKey, keepMills);
// 得到锁,没有人加过相同的锁
if (lock) {
obj = pjp.proceed();
}
// 锁设置了没有超时时间
else if (lockInfo.keepMills() <= 0) {
// 继续等待获取锁
if (lockInfo.toWait()) {
// 如果超过最大等待时间抛出异常
if (lockInfo.maxSleepMills() > 0 && System.currentTimeMillis() > maxSleepMills) {
throw new TimeoutException("获取锁资源等待超时");
}
TimeUnit.MILLISECONDS.sleep(lockInfo.sleepMills());
} else {
break;
}
}
// 已过期,并且getAndSet后旧的时间戳依然是过期的,可以认为获取到了锁
else if (System.currentTimeMillis() > getLock(synKey) && (System.currentTimeMillis() > getSet(synKey, keepMills))) {
lock = true;
obj = pjp.proceed();
}
// 没有得到任何锁
else {
// 继续等待获取锁
if (lockInfo.toWait()) {
// 如果超过最大等待时间抛出异常
if (lockInfo.maxSleepMills() > 0 && System.currentTimeMillis() > maxSleepMills) {
throw new TimeoutException("获取锁资源等待超时");
}
TimeUnit.MILLISECONDS.sleep(lockInfo.sleepMills());
}
// 放弃等待
else {
break;
}
}
}
} catch (Exception e) {
e.printStackTrace();
throw e;
} finally {
// 如果获取到了锁,释放锁
if (lock) {
releaseLock(synKey);
}
}
return obj;
}
/**
* 获取包括方法参数上的key<br/>
* redis key的拼写规则为 "RedisSyn+" + synKey + @P4jSynKey
*
*/
private String getSynKey(ProceedingJoinPoint pjp, String synKey) {
try {
synKey = "RedisSyn+" + synKey;
Object[] args = pjp.getArgs();
if (args != null && args.length > 0) {
MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
Annotation[][] paramAnnotationArrays = methodSignature.getMethod().getParameterAnnotations();
SortedMap<Integer, String> keys = new TreeMap<Integer, String>();
for (int ix = 0; ix < paramAnnotationArrays.length; ix++) {
P4jSynKey p4jSynKey = getAnnotation(P4jSynKey.class, paramAnnotationArrays[ix]);
if (p4jSynKey != null) {
Object arg = args[ix];
if (arg != null) {
keys.put(p4jSynKey.index(), arg.toString());
}
}
}
if (keys != null && keys.size() > 0) {
for (String key : keys.values()) {
synKey = synKey + key;
}
}
}
return synKey;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@SuppressWarnings("unchecked")
private static <T extends Annotation> T getAnnotation(final Class<T> annotationClass, final Annotation[] annotations) {
if (annotations != null && annotations.length > 0) {
for (final Annotation annotation : annotations) {
if (annotationClass.equals(annotation.annotationType())) {
return (T) annotation;
}
}
}
return null;
}
/**
* 获取RedisLock注解信息
*/
private P4jSyn getLockInfo(ProceedingJoinPoint pjp) {
try {
MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
Method method = methodSignature.getMethod();
P4jSyn lockInfo = method.getAnnotation(P4jSyn.class);
return lockInfo;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public BoundValueOperations<String, Long> getOperations(String key) {
return redisTemplate.boundValueOps(key);
}
/**
* Set {@code value} for {@code key}, only if {@code key} does not exist.
* <p>
* See http://redis.io/commands/setnx
*
* @param key
* must not be {@literal null}.
* @param value
* must not be {@literal null}.
* @return
*/
public boolean setIfAbsent(String key, Long value) {
return getOperations(key).setIfAbsent(value);
}
public long getLock(String key) {
Long time = getOperations(key).get();
if (time == null) {
return 0;
}
return time;
}
public long getSet(String key, Long value) {
Long time = getOperations(key).getAndSet(value);
if (time == null) {
return 0;
}
return time;
}
public void releaseLock(String key) {
redisTemplate.delete(key);
}
}
RedisLockAspect会对添加注解的方法进行特殊处理,具体可看lock方法。
大致思路就是:
1、首选借助redis本身支持对应的setIfAbsent方法,该方法的特点是如果redis中已有该数据不保存返回false,不存该数据保存返回true;
2、如果setIfAbsent返回true标识拿到同步锁,可进行操作,操作后并释放锁;
3、如果没有通过setIfAbsent拿到数据,判断是否对锁设置了超时机制,没有设置判断是否需要继续等待;
4、判断是否锁已经过期,需要对(System.currentTimeMillis() > getLock(synKey) && (System.currentTimeMillis() > getSet(synKey, keepMills)))进行细细的揣摩一下,getSet可能会改变了其他人拥有锁的超时时间,但是几乎可以忽略;
5、没有得到任何锁,判断继续等待还是退出。
第三步:spring的基本配置
#*****************jedis连接参数设置*********************# #redis服务器ip # redis.hostName=127.0.0.1 #redis服务器端口号# redis.port=6379 #redis服务器外部访问密码 redis.password=XXXXXXXXXX #************************jedis池参数设置*******************# #jedis的最大分配对象# jedis.pool.maxActive=1000 jedis.pool.minIdle=100 #jedis最大保存idel状态对象数 # jedis.pool.maxIdle=1000 #jedis池没有对象返回时,最大等待时间 # jedis.pool.maxWait=5000 #jedis调用borrowObject方法时,是否进行有效检查# jedis.pool.testOnBorrow=true #jedis调用returnObject方法时,是否进行有效检查 # jedis.pool.testOnReturn=true
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context" xmlns:jee="http://www.springframework.org/schema/jee"xmlns:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop"xmlns:redis="http://www.springframework.org/schema/redis" xmlns:cache="http://www.springframework.org/schema/cache" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.1.xsd http://www.springframework.org/schema/redis http://www.springframework.org/schema/redis/spring-redis.xsd
http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd">
<!-- 开启注解 -->
<aop:aspectj-autoproxy />
<bean class="com.yaoguoyin.redis.lock.RedisLockAspect" />
<!-- 扫描注解包范围 -->
<context:component-scan base-package="com.yaoguoyin" />
<!-- 引入redis配置 -->
<context:property-placeholder location="classpath:config.properties" />
<!-- 连接池 -->
<bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
<property name="minIdle" value="${jedis.pool.minIdle}" />
<property name="maxIdle" value="${jedis.pool.maxIdle}" />
<property name="maxWaitMillis" value="${jedis.pool.maxWait}" />
</bean>
<!-- p:password="${redis.pass}" -->
<bean id="redisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" p:host-name="${redis.hostName}" p:port="${redis.port}"
p:password="${redis.password}" p:pool-config-ref="poolConfig" />
<!-- 类似于jdbcTemplate -->
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate" p:connection-factory-ref="redisConnectionFactory" />
</beans>
redis的安装本文就不再说明。
测试
package com.yaoguoyin.redis;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:META-INF/spring/redis.xml" })
public class BaseTest extends AbstractJUnit4SpringContextTests {
}
package com.yaoguoyin.redis.lock;
import java.util.concurrent.TimeUnit;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import com.yaoguoyin.redis.BaseTest;
public class RedisTest extends BaseTest {
@Autowired
private SysTest sysTest;
@Test
public void testHello() throws InterruptedException {
for (int i = 0; i < 100; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
sysTest.add("xxxxx", 111111);
}
}).start();
}
TimeUnit.SECONDS.sleep(20);
}
@Test
public void testHello2() throws InterruptedException{
sysTest.add("xxxxx", 111111);
TimeUnit.SECONDS.sleep(10);
}
}
你可以对
void com.yaoguoyin.redis.lock.SysTest.add(@P4jSynKey(index=1) String key, @P4jSynKey(index=0) int key1)
去除注解@P4jSyn进行测试对比。
ps:本demo的执行性能取决于redis和Java交互距离;成千山万单锁并发建议不要使用这种形式,直接通过redis等解决,本demo只解决小并发不想耦合代码的形式。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,同时也希望多多支持!
# spring
# redis
# 注解
# 集群
# Redis的主从同步解析
# SpringBoot集成redis实现分布式锁的示例代码
# 基于redis setIfAbsent的使用说明
# Redis实现分布式Session管理的机制详解
# kubernetes环境部署单节点redis数据库的方法
# Redis 实现同步锁案例
# 情况下
# 设置为
# 抛出
# 你可以
# 判断是否
# 加在
# 加锁
# 参数设置
# 默认为
# 互联网
# 是在
# 就能
# 就不
# 已有
# 执行时间
# 有一定
# 要在
# 如果没有
# 会对
# 你可
相关栏目:
【
网站优化151355 】
【
网络推广146373 】
【
网络技术251813 】
【
AI营销90571 】
相关推荐:
Laravel怎么配置S3云存储驱动_Laravel集成阿里云OSS或AWS S3存储桶【教程】
1688铺货到淘宝怎么操作 1688一键铺货到自己店铺详细步骤
手机钓鱼网站怎么制作视频,怎样拦截钓鱼网站。怎么办?
Laravel如何使用Blade模板引擎?(完整语法和示例)
如何在不使用负向后查找的情况下匹配特定条件前的换行符
开心动漫网站制作软件下载,十分开心动画为何停播?
北京网站制作公司哪家好一点,北京租房网站有哪些?
Laravel怎么自定义错误页面_Laravel修改404和500页面模板
Win11摄像头无法使用怎么办_Win11相机隐私权限开启教程【详解】
详解Android图表 MPAndroidChart折线图
如何快速搭建个人网站并优化SEO?
HTML5空格和nbsp有啥关系_nbsp的作用及使用场景【说明】
如何为不同团队 ID 动态生成多个“认领值班”按钮
Laravel怎么使用Intervention Image库处理图片上传和缩放
打造顶配客厅影院,这份100寸电视推荐名单请查收
猪八戒网站制作视频,开发一个猪八戒网站,大约需要多少?或者自己请程序员,需要什么程序员,多少程序员能完成?
uc浏览器二维码扫描入口_uc浏览器扫码功能使用地址
SQL查询语句优化的实用方法总结
如何用AI一键生成爆款短视频文案?小红书AI文案写作指令【教程】
详解vue.js组件化开发实践
Laravel如何处理JSON字段的查询和更新_Laravel JSON列操作与查询技巧
Laravel Livewire是什么_使用Laravel Livewire构建动态前端界面
如何在万网自助建站平台快速创建网站?
如何在阿里云虚拟主机上快速搭建个人网站?
在线制作视频的网站有哪些,电脑如何制作视频短片?
零服务器AI建站解决方案:快速部署与云端平台低成本实践
Windows Hello人脸识别突然无法使用
如何在IIS中新建站点并配置端口与物理路径?
宙斯浏览器怎么屏蔽图片浏览 节省手机流量使用设置方法
晋江文学城电脑版官网 晋江文学城网页版直接进入
车管所网站制作流程,交警当场开简易程序处罚决定书,在交警网站查询不到怎么办?
Laravel如何实现API资源集合?(Resource Collection教程)
Google浏览器为什么这么卡 Google浏览器提速优化设置步骤【方法】
重庆市网站制作公司,重庆招聘网站哪个好?
如何快速搭建安全的FTP站点?
高性能网站服务器部署指南:稳定运行与安全配置优化方案
今日头条AI怎样推荐抢票工具_今日头条AI抢票工具推荐算法与筛选【技巧】
如何快速生成凡客建站的专业级图册?
python中快速进行多个字符替换的方法小结
如何在万网利用已有域名快速建站?
香港服务器租用费用高吗?如何避免常见误区?
iOS UIView常见属性方法小结
如何在IIS7上新建站点并设置安全权限?
Python结构化数据采集_字段抽取解析【教程】
美食网站链接制作教程视频,哪个教做美食的网站比较专业点?
Laravel中间件如何使用_Laravel自定义中间件实现权限控制
如何自定义建站之星模板颜色并下载新样式?
使用spring连接及操作mongodb3.0实例
Laravel怎么生成URL_Laravel路由命名与URL生成函数详解
如何使用 Go 正则表达式精准提取括号内首个纯字母标识符(忽略数字与嵌套)

