该帖子介绍如何设计利用AOP设计幂等注解,且可设置两种持久化模式
1、普通模式:基于redis的幂等注解,持久化程度较低
2、增强模式:基于数据库(MySQL)的幂等注解,持久化程度高
如果只需要具有redis持久化幂等的功能就可以,参考Spring Boot中使用AOP设计一个基于redis的幂等注解,简单易懂教程-CSDN博客
由于对于一些非查询操作,有时候需要保证该操作是幂等的,该帖子设计幂等注解的原理是使用AOP和反射机制获取方法的类、方法和参数,然后拼接形成一个幂等键,当下一次有重复操作过来的时候,判断该幂等键是否存放,如果存在则为”重复操作“,不继续执行;如果不存在,则为”第一次操作“,可以执行。
javaer可以在自己的项目中,加入这个点,增加项目的亮点。
1、配置依赖、配置redis、创建MySQL表
1.1、在pom文件中加入依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>
1.2、配置redis地址
如何安装redis、获取redis的ip地址,以及redis可视化工具RDM的使用,参考以下博客的1、2点Spring Boot项目中加入布隆过滤器————实战-CSDN博客
如果不想使用docker容器安装redis,可以自己下载安装redis。
spring:redis:host: 192.168.57.111 #替换为自己redis所在服务器的ipport: 6378 #替换为自己redis的端口password: # 如果无密码则留空
1.3、使用RDM连接redis
如何连接,参考以下博客的1、2点Spring Boot项目中加入布隆过滤器————实战-CSDN博客
1.4、创建mysql持久化键的表
在springboot连接的mysql数据库上,执行以下语句
-- 创建用于存储幂等键的表
CREATE TABLE idempotent_keys (id BIGINT AUTO_INCREMENT PRIMARY KEY, -- 主键,自增,唯一标识每条记录idempotent_key VARCHAR(255) NOT NULL UNIQUE, -- 幂等键,唯一约束,用于防止重复操作created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -- 键的创建时间,默认为当前时间
) COMMENT='存储幂等键的表,用于实现幂等性操作';
创建成功
下一步是写逻辑代码
2、 主要逻辑代码
2.1、创建目录和文件
创建类似的目录结构,util与service同一级即可,并如下创建四个文件
2.2、Idempotent.java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** 幂等性注解* 支持Redis持久和数据库持久模式*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {Mode mode() default Mode.REDIS; // 持久模式:默认Redisenum Mode {REDIS, DATABASE}
}
默认为redis持久模式,可设置为数据库持久模式
2.3、IdempotentAspect.java
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;@Aspect
@Component
public class IdempotentAspect {private final RedisUtil redisUtil;private final IdempotentDatabaseUtil databaseUtil;public IdempotentAspect(RedisUtil redisUtil, IdempotentDatabaseUtil databaseUtil) {this.redisUtil = redisUtil;this.databaseUtil = databaseUtil;}/*** 定义Pointcut,用于拦截service包中的所有方法*///@Pointcut("execution(* com.xxx.service..*(..)) && @annotation(idempotent)")//可以对下面这一行注释掉,然后使用上面这一行代码,但包的路径需要换@Pointcut("@annotation(idempotent)")public void idempotentMethods(Idempotent idempotent) {}/*** 定义环绕通知,处理幂等性逻辑*/@Around("idempotentMethods(idempotent)")public Object handleIdempotent(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable {// 生成幂等键String key = generateKey(joinPoint);if (key == null || key.isEmpty()) {throw new IllegalArgumentException("无法生成幂等键");}boolean success;if (idempotent.mode() == Idempotent.Mode.REDIS) {success = redisUtil.setIfAbsent(key, "1", 10, TimeUnit.MINUTES);} else {success = databaseUtil.saveKeyIfAbsent(key);}if (!success) {throw new IllegalStateException("重复操作");//这里可使用自己定义的结果返回类包裹信息,就可以不抛出错误}try {return joinPoint.proceed();} finally {// 可选:操作完成后清理key,视业务需求决定是否需要}}/*** 动态生成幂等键*/private String generateKey(ProceedingJoinPoint joinPoint) {// 获取类名String className = joinPoint.getTarget().getClass().getSimpleName();// 获取方法名String methodName = joinPoint.getSignature().getName();// 获取参数Object[] args = joinPoint.getArgs();String argsString = Arrays.toString(args);// 原始键内容String rawKey = String.format("%s:%s:%s", className, methodName, argsString);// 对键进行MD5编码return "IDEMPOTENT:"+md5(rawKey);}private String md5(String input) {try {MessageDigest md = MessageDigest.getInstance("MD5");byte[] hashBytes = md.digest(input.getBytes());StringBuilder hexString = new StringBuilder();for (byte b : hashBytes) {String hex = Integer.toHexString(0xff & b);if (hex.length() == 1) hexString.append('0');hexString.append(hex);}return hexString.toString();} catch (NoSuchAlgorithmException e) {throw new RuntimeException("MD5算法不可用", e);}}
}
在该代码里,使用的是这个,虽然可以用但不严谨,因为更严谨一点,我们只运行幂等注解被我们的几题的service类里的方法使用,因为如果用在其他类的方法上的话,会造成同一个操作出现两个不同的幂等键,造成混乱。
@Pointcut("@annotation(idempotent)")
所以建议这一行注释掉,然后使用下面面这一行代码,但包的路径需要换
@Pointcut("execution(* com.xxx.service..*(..)) && @annotation(idempotent)")
2.4、IdempotentDatabaseUtil.java
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;@Component
public class IdempotentDatabaseUtil {private final JdbcTemplate jdbcTemplate;public IdempotentDatabaseUtil(JdbcTemplate jdbcTemplate) {this.jdbcTemplate = jdbcTemplate;}/*** 尝试保存幂等键** @param key 幂等键* @return 如果键不存在并保存成功,则返回true;否则返回false*/public boolean saveKeyIfAbsent(String key) {String sql = "INSERT INTO idempotent_keys (idempotent_key) VALUES (?)";try {jdbcTemplate.update(sql, key);return true; // 插入成功} catch (Exception e) {return false; // 键已存在}}/*** 删除幂等键** @param key 幂等键*/public void deleteKey(String key) {String sql = "DELETE FROM idempotent_keys WHERE idempotent_key = ?";jdbcTemplate.update(sql, key);}/*** 检查是否存在幂等键** @param key 幂等键* @return 存在则返回true,否则返回false*/public boolean exists(String key) {String sql = "SELECT COUNT(1) FROM idempotent_keys WHERE idempotent_key = ?";Integer count = jdbcTemplate.queryForObject(sql, new Object[]{key}, Integer.class);return count != null && count > 0;}/*** 清理过期幂等键(可选,用于定期清理)** @param durationInMinutes 清理指定分钟数之前的键*/public void cleanOldKeys(int durationInMinutes) {String sql = "DELETE FROM idempotent_keys WHERE created_at < NOW() - INTERVAL ? MINUTE";jdbcTemplate.update(sql, durationInMinutes);}
}
2.5、RedisUtil.java
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;import java.util.concurrent.TimeUnit;@Component
public class RedisUtil {private final StringRedisTemplate redisTemplate;public RedisUtil(StringRedisTemplate redisTemplate) {this.redisTemplate = redisTemplate;}public boolean setIfAbsent(String key, String value, long timeout, TimeUnit unit) {Boolean result = redisTemplate.opsForValue().setIfAbsent(key, value, timeout, unit);return result != null && result;}public void delete(String key) {redisTemplate.delete(key);}
}
3、在业务代码上测试
3.1、redis持久化模式
@Idempotent(mode = Idempotent.Mode.REDIS)
把上面这一行代码扣在方法头上就能用了,如下
@Override
@Idempotent(mode = Idempotent.Mode.REDIS)
public <T> ReturnStatus<T> createTask(TaskRequest TaskRequest) {//业务代码
}
启动项目,使用postman调用createTask接口
结果显示,成功!
查看RDM中redis的数据
redis幂等键存在,同一个接口同样的参数再调用一次postman
因为已经存在幂等键了,调用失败,再查看idea控制台打印的日志,有”重复操作“的信息,符合实际,测试成功!
3.2、数据库持久化模式
@Idempotent(mode = Idempotent.Mode.DATABASE)
把上面这一行代码扣在方法头上就能用了,如下
@Override
@Idempotent(mode = Idempotent.Mode.DATABASE)
public <T> ReturnStatus<T> createTask(TaskRequest TaskRequest) {//业务代码
}
启动项目,使用postman调用createTask接口
成功执行,查看一下数据库是否有该数据,yes,存在
查看打印出的日志(需要在application.yml中配置,这一步无关紧要),显示了在idempotent_keys这张表插入数据的sql语句
同一个接口同样的参数再调用一次postman
查看idea控制台打印的日志
由于幂等键存在,所以调用失败,符合实际,测试成功!