Java令牌幂等案例如何实现

wen java案例 26

本文目录导读:

Java令牌幂等案例如何实现

  1. 什么是令牌幂等
  2. 核心实现思路
  3. 完整实现代码
  4. 使用Lua脚本优化
  5. 分布式环境下的实现
  6. 最佳实践建议
  7. 完整项目结构

我来详细说明Java中令牌幂等的实现方案。

什么是令牌幂等

令牌幂等是通过为每个请求分配唯一令牌来确保同一请求只被执行一次的机制,主要解决接口重复提交问题。

核心实现思路

sequenceDiagram
    participant Client
    participant Server
    participant Redis
    Client->>Server: 1. 请求令牌
    Server->>Redis: 2. 生成并存储令牌
    Redis-->>Server: 3. 返回令牌
    Server-->>Client: 4. 返回令牌
    Client->>Server: 5. 携带令牌执行业务
    Server->>Redis: 6. 删除令牌
    Redis-->>Server: 7. 返回删除结果
    Note over Server: 8. 判断是否成功删除
    alt 删除成功
        Server-->>Client: 9. 执行业务并返回结果
    else 删除失败
        Server-->>Client: 10. 返回重复请求错误
    end

完整实现代码

1 引入依赖

<!-- Redis依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- AOP依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2 定义注解

import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface IdempotentToken {
    /**
     * 令牌过期时间
     */
    long expireTime() default 60;
    /**
     * 时间单位
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;
    /**
     * 是否开启幂等校验
     */
    boolean required() default true;
}

3 令牌服务层

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Service
public class TokenService {
    @Autowired
    private StringRedisTemplate redisTemplate;
    private static final String TOKEN_PREFIX = "idempotent:token:";
    /**
     * 生成令牌
     */
    public String createToken() {
        String token = UUID.randomUUID().toString().replace("-", "");
        // 存储到Redis,设置过期时间
        redisTemplate.opsForValue().set(
            TOKEN_PREFIX + token, 
            token, 
            60, 
            TimeUnit.SECONDS
        );
        return token;
    }
    /**
     * 校验并删除令牌(幂等校验核心)
     */
    public boolean checkToken(String token) {
        if (token == null || token.isEmpty()) {
            return false;
        }
        // 使用Lua脚本保证原子性操作
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                       "return redis.call('del', KEYS[1]) " +
                       "else " +
                       "return 0 " +
                       "end";
        Long result = redisTemplate.execute(
            (connection) -> connection.eval(
                script.getBytes(),
                ReturnType.INTEGER,
                1,
                (TOKEN_PREFIX + token).getBytes(),
                token.getBytes()
            ),
            true
        );
        return result != null && result == 1L;
    }
}

4 AOP拦截器

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.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
@Aspect
@Component
public class IdempotentTokenAspect {
    @Autowired
    private TokenService tokenService;
    @Around("@annotation(com.example.idempotent.annotation.IdempotentToken)")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        IdempotentToken annotation = method.getAnnotation(IdempotentToken.class);
        if (!annotation.required()) {
            return joinPoint.proceed();
        }
        // 获取请求中的令牌
        ServletRequestAttributes attributes = 
            (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        String token = request.getHeader("idempotent-token");
        if (token == null) {
            token = request.getParameter("idempotent_token");
        }
        // 校验令牌
        boolean isValid = tokenService.checkToken(token);
        if (!isValid) {
            throw new RuntimeException("重复请求或令牌已过期");
        }
        // 执行原方法
        return joinPoint.proceed();
    }
}

5 控制器示例

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/order")
public class OrderController {
    @Autowired
    private TokenService tokenService;
    @GetMapping("/token")
    public Result<String> getToken() {
        String token = tokenService.createToken();
        return Result.success(token);
    }
    @PostMapping("/create")
    @IdempotentToken
    public Result<String> createOrder(@RequestBody OrderDTO orderDTO) {
        // 执行业务逻辑
        String orderId = orderService.create(orderDTO);
        return Result.success(orderId);
    }
}

6 前端调用示例

// 1. 先获取令牌
async function getToken() {
    const response = await fetch('/api/order/token');
    const data = await response.json();
    return data.data;
}
// 2. 携带令牌提交请求
async function submitOrder(orderData) {
    const token = await getToken();
    const response = await fetch('/api/order/create', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'idempotent-token': token  // 在Header中携带令牌
        },
        body: JSON.stringify(orderData)
    });
    return response.json();
}

使用Lua脚本优化

1 Lua脚本实现

@Service
public class TokenService {
    // 使用Spring Data Redis提供的Lua脚本支持
    private static final DefaultRedisScript<Long> CHECK_AND_DELETE_SCRIPT;
    static {
        CHECK_AND_DELETE_SCRIPT = new DefaultRedisScript<>();
        CHECK_AND_DELETE_SCRIPT.setScriptText(
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "    return redis.call('del', KEYS[1]) " +
            "else " +
            "    return 0 " +
            "end"
        );
        CHECK_AND_DELETE_SCRIPT.setResultType(Long.class);
    }
    public boolean checkTokenWithScript(String token) {
        if (token == null || token.isEmpty()) {
            return false;
        }
        String key = TOKEN_PREFIX + token;
        Long result = redisTemplate.execute(
            CHECK_AND_DELETE_SCRIPT,
            Collections.singletonList(key),
            token
        );
        return result != null && result == 1L;
    }
}

分布式环境下的实现

1 使用Redisson分布式锁

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class DistributedTokenService {
    @Autowired
    private RedissonClient redissonClient;
    private static final String LOCK_PREFIX = "idempotent:lock:";
    public boolean executeWithLock(String businessKey, Runnable task) {
        String lockKey = LOCK_PREFIX + businessKey;
        RLock lock = redissonClient.getLock(lockKey);
        try {
            // 尝试获取锁,最多等待5秒,锁定10秒
            boolean isLocked = lock.tryLock(5, 10, TimeUnit.SECONDS);
            if (!isLocked) {
                return false;
            }
            // 执行业务
            task.run();
            return true;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        } finally {
            lock.unlock();
        }
    }
}

最佳实践建议

1 配置优化

spring:
  redis:
    host: localhost
    port: 6379
    timeout: 5000ms
    lettuce:
      pool:
        max-active: 8
        max-wait: -1ms
        max-idle: 8
        min-idle: 0
# 自定义配置
idempotent:
  token:
    expire-time: 60  # 令牌过期时间(秒)
    prefix: "idempotent:token:"  # Redis键前缀

2 异常处理

@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(RuntimeException.class)
    @ResponseBody
    public Result<?> handleException(RuntimeException e) {
        if (e.getMessage().contains("重复请求")) {
            return Result.error(400, "请勿重复提交");
        }
        return Result.error(500, "系统异常");
    }
}

3 使用建议

  1. 令牌生成时机:在进入业务页面时预生成
  2. 过期时间设置:根据业务复杂度设置,一般为1-5分钟
  3. 结合其他防重机制:如数据库唯一索引、分布式锁等
  4. 清理策略:定期清理过期令牌

完整项目结构

src/main/java/com/example/idempotent/
├── annotation/
│   └── IdempotentToken.java
├── aspect/
│   └── IdempotentTokenAspect.java
├── service/
│   └── TokenService.java
├── controller/
│   └── TokenController.java
├── model/
│   └── Result.java
└── config/
    ├── RedisConfig.java
    └── IdempotentConfig.java

这个方案通过Redis的原子性操作保证了幂等性,适用于单机和分布式环境,核心思想是"生成令牌-校验令牌-删除令牌"的三步流程。

抱歉,评论功能暂时关闭!