java代码实现接口幂等性问题

问题描述:接口幂等性问题

解决办法:接口幂等问题传统方式是基于redis解决,由于项目中没有用到redis所以用ConcurrentHashMap+锁的方式模拟redis实现接口幂等问题的处理

上代码:

第一版:直接在业务层添加代码,代码入侵性比较高

        //每次请求的唯一key
        String key = record.getSuperviseId().toString() + record.getSendCourtId().toString() + record.getReceiveCourtId().toString();
        try {
            //对保全局map进行操作时进行加锁操作 注:这里的锁采用com.google.common.collect.Interners包下的字符串锁 
            // this会锁整个类对象范围太大
            synchronized (lock.intern("superviseChatRecord" + key)) {
                //在map里面获取key  如果能获取到则证明在间隔时间内已经执行过一次
                Long time = KEY_MAP.get(key);
                if (time != null && System.currentTimeMillis() - time < 1000) {
                    throw  new BusinessException("不能重复发送消息!");
                }
                //如果获取不到则证明是第一次或者超过间隔时间保存的key 则运行继续保存或执行后续的操作
                //将唯一键保存入map
                KEY_MAP.put(key, System.currentTimeMillis());
                //执行保存逻辑
                return  this.save(record);
            }
        } finally {
            //最后如果key对应的时间值大于间隔时间则移除key
            //移除原因 1.防止map过大内存溢出  2.下次请求大于间隔时间则认为是两次请求不是连点造成的幂等性问题
            if (KEY_MAP.get(key)-System.currentTimeMillis()>1000){
                KEY_MAP.remove(key);
            }
        }

第二版:用自定义注解+AOP的方式解决

package com.iexecloud.shzxzh.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @Description : 幂等 自定义注解,基于post请求,请求入参为json
 * @Author : ;lirui
 * @Date : 2022/9/22 14:06
 * @Version : 1.0
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {

    /**
     * 需要排除的字段,逗号分割
     */
    String exclude() default "";

    /**
     * 指定字段幂等,逗号分割
     */
    String include() default "";
}
package com.iexecloud.shzxzh.aspect;

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.iexecloud.shzxzh.annotation.Idempotent;

import com.iexecloud.shzxzh.exception.BusinessException;
import com.iexecloud.shzxzh.util.ReqDedupHelper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @Description : 基于注解的幂等功能
 * @Author : ;lirui
 * @Date : 2022/9/22 14:06
 * @Version : 1.0
 */
@Aspect
@Component
@Slf4j
@RequiredArgsConstructor
public class IdempotentAspect {

    private static final ConcurrentHashMap<String, Long> concurrentHashMap = new ConcurrentHashMap<>();

    @Around("@annotation(com.iexecloud.shzxzh.annotation.Idempotent)")
    public Object log(ProceedingJoinPoint joinPoint) throws Throwable {



        RequestAttributes ra = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes sra = (ServletRequestAttributes) ra;
        if (sra == null) {
            return joinPoint.proceed();
        }
        Object[] args = joinPoint.getArgs();
        HttpServletRequest request = sra.getRequest();
        String methodType = request.getMethod();
        if (!"POST".equalsIgnoreCase(methodType) || args == null || args.length == 0) {
            return joinPoint.proceed();
        }

        //只对POST请求做幂等校验
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        String methodName = method.getName();
        Object response;
        Idempotent idempotent = method.getAnnotation(Idempotent.class);
        String include = idempotent.include();
        String exclude = idempotent.exclude();
        List<String> includeList = StrUtil.split(include, ",");
        List<String> excludeList = StrUtil.split(exclude, ",");
        String dedupMD5 = methodName + new ReqDedupHelper().dedupParamMD5(JSONUtil.toJsonStr(args[0]), includeList, excludeList);
        synchronized (this) {
            Long time = concurrentHashMap.get(dedupMD5);
            if (time != null && System.currentTimeMillis() - time < 2000) {
                throw new BusinessException("请勿重复提交");
            }
            concurrentHashMap.put(dedupMD5, System.currentTimeMillis());
        }
        try {
            response = joinPoint.proceed();
        } finally {
            concurrentHashMap.remove(dedupMD5);
        }
        return response;
    }
}

最后在controller层添加相关注解:

    /**
     * 发送消息
     * @param
     * @return
     */
    //这里是指定唯一key包含的字段
    @Idempotent(include = "superviseId,sendCourtId,receiveCourtId,content")
    @PostMapping("sendMessage")
    public R sendMessage(@RequestBody  SuperviseChatRecord record) {
        Boolean flag = superviseChatRecordService.sendMessage(record);
        if (flag){
            return R.ok("发送消息成功");
        }
        return R.failed("发送消息失败");
    }

以上就是为解决消息幂等性所想到的解决方案,如有问题请及时提出,当然实现接口幂等性的解决方案还有很多种,包括用数据库实现,或者redis实现等等。

后续在自己的demo里添加了redis然后用redis实现的代码:

        String key = record.getSuperviseId().toString() + record.getSendCourtId().toString() + record.getReceiveCourtId().toString();
        synchronized (this){
            //获取redis里的key
            String time = redis.opsForValue().get(key);
            //如果key已经存在或者key对应的时间小于间隔时间则抛异常
            if (StringUtils.isNotBlank(time)&&System.currentTimeMillis()-Long.valueOf(time)<1000){
                throw new BusinessException("切勿重复提交");
            }
            //如果redis里面没有值则添加key并设置过期时间
            redis.opsForValue().set(key, String.valueOf(System.currentTimeMillis()),1, TimeUnit.SECONDS);
        }
        //执行业务逻辑

### Java实现接口幂等性的方法 为了确保接口幂等性,在 Java 应用程序中通常会采用多种策略和技术来防止重复提交请求或执行操作。下面介绍几种常见的技术手段并提供相应的代码示例。 #### 1. 使用唯一标识符验证幂等性 当客户端发起请求时,服务器端可以通过校验唯一标识符(如 UUID 或订单号)来判断该请求是否已经被处理过。如果已经存在,则不再重新处理而是返回之前的结果。 ```java public class IdempotentService { private final Map<String, String> processedRequests = new ConcurrentHashMap<>(); public synchronized Response handleRequest(String uniqueId, Request request) { if (processedRequests.containsKey(uniqueId)) { return new Response(processedRequests.get(uniqueId)); } else { // 处理业务逻辑... String result = processBusinessLogic(request); // 将结果缓存起来以便后续查询 processedRequests.putIfAbsent(uniqueId, result); return new Response(result); } } } ``` 此段代码展示了如何利用 `ConcurrentHashMap` 来存储已处理过的请求及其对应的响应数据[^3]。 #### 2. 利用数据库事务与乐观锁控制并发访问 对于涉及数据库更新的操作来说,可以借助于版本字段配合 SQL 的 `UPDATE ... WHERE version = ?` 语句以及设置合适的隔离级别来达到防重的效果。这种方式被称为“乐观锁定”。 ```sql -- 假设有一个表 t_order(id INT PRIMARY KEY, status VARCHAR(20), version INT) UPDATE t_order SET amount=amount+?,version=version+1 WHERE id=? AND version=? ``` 在应用程序层面: ```java @Transactional(isolation = Isolation.READ_COMMITTED) public void updateOrderAmount(Long orderId, BigDecimal increment) throws Exception{ Order order = orderRepository.findById(orderId).orElseThrow(() -> new RuntimeException("Order not found")); int rowsAffected = jdbcTemplate.update( "UPDATE t_order SET amount=?,version=version+1 WHERE id=? AND version=?", order.getAmount().add(increment), orderId, order.getVersion() ); if(rowsAffected == 0){ throw new OptimisticLockingFailureException(); } } ``` 上述例子中,一旦检测到记录被其他事务修改则抛出异常终止当前流程,从而避免了脏读等问题的发生[^4]。 #### 3. 分布式环境下的全局唯一 ID 和分布式锁 在一个分布式的微服务体系结构里,可能还会遇到跨多个实例间的竞争条件。此时除了要保证本地服务内的等外还需要考虑整个集群范围的一致性问题。一种解决方案就是引入像 Redis 这样的中间件作为协调者生成全局唯一的序列号或者是基于它构建简单的分布式互斥锁机制。 ```java // 获取Redis连接对象 Jedis jedis = pool.getResource(); try { // 设置key的有效期为5秒,并尝试加锁 Long lockResult = jedis.setnx(lockKey, Thread.currentThread().getName()); Boolean expireSuccess = jedis.expire(lockKey, 5); if ((lockResult != null && lockResult.equals(1L))) { try { // 执行具体业务逻辑... } finally { // 解除锁定 jedis.del(lockKey); } } else { log.warn("{} failed to acquire the distributed lock",Thread.currentThread().getName()); } } catch(Exception e){ ... }finally{ jedis.close(); } ``` 这段代码片段实现了通过 Redis 实现简单版的分布式锁功能,有效解决了高并发场景下资源争抢的问题[^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值