利用redis+AOP简单处理MQ幂等问题
思路:
1、利用redis内部的串行执行特性,使用getandset()处理分布式+并发问题;
2、注解提供入参选择,通过数据抽取后计算MD5值,实现业务性值的幂等;
代码区:
1、注解
1 /** 2 * 功能描述:MQ简单幂等性处理 3 * 作者:唐泽齐 4 */ 5 @Documented 6 @Target({ 7 ElementType.METHOD 8 }) 9 @Retention(RetentionPolicy.RUNTIME) 10 public @interface MqPitfall { 11 12 // 过期时长 默认30天 单位/秒(s) 13 long timeOut() default 30*24*60*60l; 14 15 // 幂等效验 参数 必须是能从onMessage()方法的入参中取出的属性 16 String[] args() default {}; 17 }
2、AOP
1 /** 2 * 功能描述:MQ信息过滤 3 * 作者:唐泽齐 4 */ 5 @Aspect 6 @Component 7 public class MqPitfallInterceptor { 8 9 static final String mqPitfallKey = "MqPitfall:"; 10 static final Logger logger = LoggerFactory.getLogger(com.lechuang.common.redis.intercaptor.MqPitfallInterceptor.class); 11 12 @Resource 13 RedisService redisService; 14 15 @Around("@annotation(MqPitfall)") 16 public void around(ProceedingJoinPoint point) throws Throwable { 17 MqPitfall mqPitfall = ((MethodSignature) point.getSignature()).getMethod().getAnnotation(MqPitfall.class); 18 String className = ((MethodSignature) point.getSignature()).getMethod().getDeclaringClass().getName(); 19 Map<String,Object> map = new HashMap<>(); 20 try { 21 for(Object arg: point.getArgs()) { 22 JSONObject json = null; 23 if(arg instanceof String) { 24 json = JSON.parseObject(arg.toString()); 25 } else { 26 json = JSON.parseObject(JSON.toJSONString(arg)); 27 } 28 for(String key:mqPitfall.args()) { 29 map.put(key,json.get(key)); 30 } 31 } 32 if(map.isEmpty()) { 33 for(Object arg: point.getArgs()) { 34 JSONObject json = null; 35 if(arg instanceof String) { 36 json = JSON.parseObject(arg.toString()); 37 } else { 38 json = JSON.parseObject(JSON.toJSONString(arg)); 39 } 40 for(String key: json.keySet()) { 41 map.put(key,json.get(key)); 42 } 43 } 44 } 45 } catch (Exception e) { 46 map.put("Args",Arrays.deepToString(point.getArgs())); 47 } 48 map.put("Aspect",className); 49 String thisMd5 = MD5.create().digestHex(map.toString()); 50 String key = mqPitfallKey + thisMd5; 51 52 //简单的占位锁机制 53 Object value = redisService.getAndSet(key, -1l); 54 if(ObjectUtils.isEmpty(value)) { 55 redisService.set(key,1,mqPitfall.timeOut()); 56 point.proceed(); 57 } else { 58 logger.warn("MQ信息重复消费 摘要["+thisMd5+"] ==》" + Arrays.deepToString(point.getArgs())); 59 } 60 } 61 }
3、使用
1 /** 2 * @Method 引入切面注解 3 */ 4 @Configuration 5 @Import({MqPitfallInterceptor.class}) 6 public class WebAppConfig implements WebMvcConfigurer { 7 8 }
1 /** 2 * 作者:唐泽齐 3 */ 4 @Slf4j 5 @Service 6 @RequiredArgsConstructor 7 @RocketMQMessageListener(consumerGroup = GuildTopic.GUILD_ANCHOR_ATTEST+"_guildAnchorAttestListener", consumeMode = ConsumeMode.ORDERLY, topic = GuildTopic.GUILD_ANCHOR_ATTEST) 8 public class GuildAnchorAttestListener implements RocketMQListener { 9 10 private final GuildAnchorAttestService guildAnchorAttestService; 11 12 @Override 13 @MqPitfall(args = {"userId","guildId"}) 14 public void onMessage(Object message) { 15 log.info("xxxxxx 开始 ==》" + message); 16 long millis = System.currentTimeMillis(); 17 try { 18 GuildTopicEnum guildTopicEnum = GuildTopic.find(GuildTopic.GUILD_ANCHOR_ATTEST); 19 if(!guildTopicEnum.valid(message)) { 20 log.error("xxxxxx 异常 ==> 信息效验不合格 : "+message); 21 return; 22 } 23 GuildAnchorAttest attest = guildTopicEnum.getData().toJavaObject(GuildAnchorAttest.class); 24 guildAnchorAttestService.save(attest); 25 log.info("xxxxxx 成功 ==》" + message); 26 } catch (Exception e) { 27 log.error("xxxxxx 失败 ==》 "+ message,e); 28 } finally { 29 log.info("xxxxxx 耗时 "+(System.currentTimeMillis()-millis)+"ms ==》" + message); 30 } 31 32 } 33 }