您的位置:

Java幂等

一、什么是幂等

幂等指的是相同的请求执行一次和执行多次的效果是一致的,不会因为多次请求产生副作用。

对于一些数据更新操作,例如修改用户信息、下单等操作,如果发生网络故障或者客户端异常,造成了多次请求,如果系统没有幂等措施,可能导致数据多次更新,最终产生异常数据。

幂等可以有效解决重复执行的问题。

二、为什么需要幂等

网络通信中常常出现不稳定的情况,例如网络中断、超时等问题,而相同操作请求多次则会产生意料之外的结果。在银行支付、物流发货、物资调拨等场景中,重复操作可能会导致金钱流、库存等数据的不一致,因此在这些场景中需要通过幂等来保证数据的一致性、正确性和安全性。

三、java中的实现方法

1、Token检测

Token是服务器返回的一段随机生成的字符串,客户端在每次请求时都必须带上这个Token,服务器在接收到Token后将Token与之前存储的Token比较,如果相同,则代表请求已经被处理,是重复请求,直接返回结果即可。

代码示例:

public class IdempotentTokenInterceptor extends HandlerInterceptorAdapter {
    private TokenProvider tokenProvider;

    public IdempotentTokenInterceptor(TokenProvider tokenProvider) {
        this.tokenProvider = tokenProvider;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = request.getHeader("X-api-token");

        if(token == null) {
            throw new BusinessException("Missing X-api-token header!");
        }

        if(!tokenProvider.validateToken(token)) {
            throw new BusinessException("Invalid X-api-token!");
        }

        return true;
    }
}

public class TokenProvider {
    public String generateToken() {
        String token = UUID.randomUUID().toString();
        //save to cache or database
        return token;
    }

    public boolean validateToken(String token) {
        //从缓存中获取,并删除Token
        return true;
    }
}

public class BusinessController {
    private TokenProvider tokenProvider;

    public BusinessController(TokenProvider tokenProvider) {
        this.tokenProvider = tokenProvider;
    }

    @PostMapping("/api")
    public ApiResponse businessApi() {
        String token = tokenProvider.generateToken();

        //save token to cache or database

        //process business logic

        return ApiResponse.ok();
    }
}

2、数据库唯一约束

在数据库表中设置一个唯一约束,例如唯一索引,保证相同的请求只处理一次,后续的请求会抛出数据库异常。

代码示例:

public class BusinessController {
    @Autowired
    private BusinessService businessService;

    @PostMapping("/api")
    public ApiResponse businessApi(@RequestBody BusinessRequest request) {
        businessService.processData(request);
        return ApiResponse.ok();
    }
}

@Service
public class BusinessService {
    @Autowired
    private BusinessDao businessDao;

    public void processData(BusinessRequest request) {
        //检查是否已经处理
        boolean processed = businessDao.checkIfProcessed(request);

        if(processed) {
            throw new BusinessException("Request has been processed!");
        }

        //处理请求
        businessDao.process(request);
    }
}

@Repository
public class BusinessDao {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    public boolean checkIfProcessed(BusinessRequest request) {
        String sql = "SELECT COUNT(*) FROM business_table WHERE request_id=?";
        int count = jdbcTemplate.queryForObject(sql, new Object[]{request.getRequestId()}, Integer.class);
        return count > 0;
    }

    public void process(BusinessRequest request) {
        String sql = "INSERT INTO business_table (request_id, data) VALUES (?,?)";
        jdbcTemplate.update(sql, request.getRequestId(), request.getData());
    }
}

3、拦截器

拦截器是一种在请求被处理之前或之后,拦截并处理请求的机制。通过在拦截器中实现幂等处理,可以保证相同请求只处理一次。

代码示例:

public class IdempotentInterceptor extends HandlerInterceptorAdapter {
    @Autowired
    private IdempotentKeyGenerator idempotentKeyGenerator;

    @Autowired
    private CacheManager cacheManager;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //获取幂等Key
        String idempotentKey = idempotentKeyGenerator.generate(request);

        if(StringUtils.isNotBlank(idempotentKey)) {
            Cache cache = cacheManager.getCache("idempotent");
            ValueWrapper valueWrapper = cache.get(idempotentKey);

            if(valueWrapper != null && valueWrapper.get() != null) {
                throw new BusinessException("Request has been processed!");
            }

            cache.put(idempotentKey, idempotentKey);
        }

        return true;
    }
}

public interface IdempotentKeyGenerator {
    String generate(HttpServletRequest request);
}

@Component
public class DefaultIdempotentKeyGenerator implements IdempotentKeyGenerator {
    @Override
    public String generate(HttpServletRequest request) {
        return request.getHeader("X-idempotent-key");
    }
}

@Configuration
public class CacheConfig {
    @Bean
    public CacheManager cacheManager() {
        SimpleCacheManager cacheManager = new SimpleCacheManager();

        Cache idempotentCache = new ConcurrentMapCache("idempotent", false);

        cacheManager.setCaches(Arrays.asList(idempotentCache));

        return cacheManager;
    }
}

@Controller
public class BusinessController {
    @Autowired
    private BusinessService businessService;

    @PostMapping("/api")
    public ApiResponse businessApi(@RequestBody BusinessRequest request) {
        businessService.processData(request);
        return ApiResponse.ok();
    }
}

@Service
public class BusinessService {
    public void processData(BusinessRequest request) {
        //处理请求
    }
}

四、总结

幂等可以帮助我们解决重复操作的问题,确保请求的正确性和唯一性,提高系统的稳定性和安全性。在Java中,有多种实现幂等的方式,例如Token检测、数据库唯一约束、拦截器等,我们可以根据实际需求和场景选择合适的方式。