Impotence means that the effect of executing the same operation multiple times and once is the same, and there will be no side effects due to repeated execution. In actual applications, due to network delay, repeated clicks from users, automatic system retry, etc., the same request may be sent to the server for processing multiple times. If idempotence is not achieved, it may lead to data duplication and business exceptions.
1. Idepotency implementation based on Token token
Token token strategy is one of the most common idempotence implementation methods. Its core idea is to obtain a unique token before performing business operations, and then submit it with the request when calling the interface, verify and destroy the token to ensure that it is only used once.
Implementation steps
- The client calls to get the token interface first
- The server generates a unique token and saves it into Redis, and sets the expiration time
- The client attaches to the token parameter when calling the business interface
- The server verifies the existence of the token and deletes it to prevent reuse
Code implementation
@RestController @RequestMapping("/api") public class OrderController { @Autowired private StringRedisTemplate redisTemplate; @Autowired private OrderService orderService; // Get the token interface @GetMapping("/token") public Result<String> getToken() { // Generate a unique token String token = ().toString(); // Save Redis and set expiration time ().set("idempotent:token:" + token, "1", 10, ); return (token); } // Create an order interface @PostMapping("/order") public Result<Order> createOrder(@RequestHeader("Idempotent-Token") String token, @RequestBody OrderRequest request) { // Check whether the token exists String key = "idempotent:token:" + token; Boolean exist = (key); if (exist == null || !exist) { return ("The token does not exist or has expired"); } // Delete tokens to ensure idempotence if (((key))) { return ("Token has been used"); } // Execute business logic Order order = (request); return (order); } }
Simplified implementation through AOP
Idepotency implementation can be further simplified through custom annotations and AOP:
// Custom idempotence annotation@Target() @Retention() public @interface Idempotent { long timeout() default 10; // Expiration time, unit minutes} // AOP implementation@Aspect @Component public class IdempotentAspect { @Autowired private StringRedisTemplate redisTemplate; @Around("@annotation(idempotent)") public Object around(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable { // Get the token in the request header HttpServletRequest request = ((ServletRequestAttributes) ()).getRequest(); String token = ("Idempotent-Token"); if ((token)) { throw new BusinessException("Idempogenic Token cannot be empty"); } String key = "idempotent:token:" + token; Boolean exist = (key); if (exist == null || !exist) { throw new BusinessException("The token does not exist or has expired"); } // Delete tokens to ensure idempotence if (((key))) { throw new BusinessException("Token has been used"); } // Execute the target method return (); } } // Notes on the controller@RestController @RequestMapping("/api") public class OrderController { @Autowired private OrderService orderService; @PostMapping("/order") @Idempotent(timeout = 30) public Result<Order> createOrder(@RequestBody OrderRequest request) { Order order = (request); return (order); } }
Pros and cons analysis
advantage
- Simple to implement and easy to understand
- Invasion of business code can be achieved through AOP
- You can pre-create tokens to reduce the delay in request processing
shortcoming
- Two requests are required to complete a business operation
- Increases the complexity of the client
- Rely on external storage such as Redis
2. Idepotency implementation based on database unique constraints
Idepotency can be achieved simply and effectively using the unique constraints of the database. When trying to insert duplicate data, the database throws a unique constraint exception, which we can catch and handle appropriately.
Implementation method
- Add a unique index on a critical business table
- Capture unique constraint exceptions when inserting data
- Decide whether to return an error or return existing data based on business needs
Code implementation
@Service public class PaymentServiceImpl implements PaymentService { @Autowired private PaymentRepository paymentRepository; @Transactional @Override public PaymentResponse processPayment(PaymentRequest request) { try { // Create a payment record, including a unique business identity Payment payment = new Payment(); (()); (()); // Unique transaction ID (()); (); (new Date()); // Save payment history (payment); // Call the payment gateway API // ...Payment processing logic... // Update payment status (); (payment); return new PaymentResponse(true, "Payment Success", ()); } catch (DataIntegrityViolationException e) { //Catch unique constraint exception if (() instanceof ConstraintViolationException) { // Idepotency processing - query existing payment records Payment existingPayment = paymentRepository .findByTransactionId(()) .orElse(null); if (existingPayment != null) { if ((())) { // Payment has been processed successfully, and the successful result is returned return new PaymentResponse(true, "Payment Processed", ()); } else { // Payment is being processed, return an appropriate prompt return new PaymentResponse(false, "Payment processing", ()); } } } // Other data integrity issues ("Pay failed", e); return new PaymentResponse(false, "Pay failed", null); } } } // Payment entity@Entity @Table(name = "payments") public class Payment { @Id @GeneratedValue(strategy = ) private Long id; private String orderNo; @Column(unique = true) // Unique constraint private String transactionId; private BigDecimal amount; @Enumerated() private PaymentStatus status; private Date createTime; // Getters and setters... }
Pros and cons analysis
advantage
- Simple implementation, taking advantage of existing database features
- No additional storage components required
- Strong consistency guarantee
shortcoming
- Unique constraints that depend on databases
- May cause frequent exception handling
- Can be a performance bottleneck in high concurrency
3. Idepotency implementation based on distributed lock
Distributed locking is another effective way to achieve idempotence and is particularly suitable for high concurrency scenarios. By locking the service unique identifier, it is possible to ensure that only one request can execute business logic at the same time.
Implementation method
- Use Redis, Zookeeper, etc. to implement distributed locking
- The key of the lock is used by the requested unique identifier
- Acquire the lock before business processing, release the lock after processing is completed
Distributed lock implementation based on Redis
@Service public class InventoryServiceImpl implements InventoryService { @Autowired private StringRedisTemplate redisTemplate; @Autowired private InventoryRepository inventoryRepository; private static final String LOCK_PREFIX = "inventory:lock:"; private static final long LOCK_EXPIRE = 10000; // 10 seconds @Override public DeductResponse deductInventory(DeductRequest request) { String lockKey = LOCK_PREFIX + (); String requestId = ().toString(); try { // Try to acquire a distributed lock Boolean acquired = ().setIfAbsent(lockKey, requestId, LOCK_EXPIRE, ); if ((acquired)) { // Acquisition of lock failed, indicating that it may be a duplicate request return new DeductResponse(false, "The request is being processed, please do not repeat submission"); } // Check whether the request has been processed Optional<InventoryRecord> existingRecord = (()); if (()) { // Idepotency control - The request has been processed return new DeductResponse(true, "Inventory Deducted", ().getId()); } // Execute inventory deduction logic Inventory inventory = (()) .orElseThrow(() -> new BusinessException("The product does not exist")); if (() < ()) { throw new BusinessException("Insufficient Inventory"); } // Deduct inventory (() - ()); (inventory); // Record inventory operations InventoryRecord record = new InventoryRecord(); (()); (()); (()); (new Date()); (record); return new DeductResponse(true, "Inventory deduction succeeded", ()); } catch (BusinessException e) { return new DeductResponse(false, (), null); } catch (Exception e) { ("Inventory deduction failed", e); return new DeductResponse(false, "Inventory deduction failed", null); } finally { // Release the lock, be careful to release only your own lock if ((().get(lockKey))) { (lockKey); } } } }
Simplified implementation with Redisson
@Service public class InventoryServiceImpl implements InventoryService { @Autowired private RedissonClient redissonClient; @Autowired private InventoryRepository inventoryRepository; private static final String LOCK_PREFIX = "inventory:lock:"; @Override public DeductResponse deductInventory(DeductRequest request) { String lockKey = LOCK_PREFIX + (); RLock lock = (lockKey); try { // Try to acquire the lock, wait for 5 seconds, the lock expires for 10 seconds boolean acquired = (5, 10, ); if (!acquired) { return new DeductResponse(false, "The request is being processed, please do not repeat submission"); } // Check whether the request has been processed // ...The subsequent business logic is the same as the previous example... } catch (InterruptedException e) { ().interrupt(); return new DeductResponse(false, "Request interrupted", null); } catch (Exception e) { ("Inventory deduction failed", e); return new DeductResponse(false, "Inventory deduction failed", null); } finally { // Release the lock if (()) { (); } } } }
Pros and cons analysis
advantage
- Suitable for high concurrency scenarios
- Can be used in conjunction with other idempotence strategies
- Provide better real-time control
shortcoming
- Highly complex implementation
- Rely on external storage services
4. Idepotency implementation based on requested content summary
This scheme calculates the hash value or digest of the requested content, and generates a unique identifier as an idempotent key, ensuring that the request for the same content is processed only once.
Implementation method
- Calculate the summary value of the requested parameter (such as MD5, SHA-256, etc.)
- Store digest values in Redis or database as idempotent keys
- Check whether the digest value already exists before the request is processed.
- Existence means repeated requests and no business logic is executed.
Code implementation
@RestController @RequestMapping("/api") public class TransferController { @Autowired private TransferService transferService; @Autowired private StringRedisTemplate redisTemplate; @PostMapping("/transfer") public Result<TransferResult> transfer(@RequestBody TransferRequest request) { // Generate a request digest as an idempotent key String idempotentKey = generateIdempotentKey(request); String redisKey = "idempotent:digest:" + idempotentKey; // Try to set idempotent keys in Redis, use SetNX operation to ensure atomicity Boolean isFirstRequest = () .setIfAbsent(redisKey, "processed", 24, ); // If the key already exists, it means it is a repeated request if ((isFirstRequest)) { // Query processing results (you can also directly store processing results) TransferRecord record = (idempotentKey); if (record != null) { // Return the previous processing result return (new TransferResult( (), "Transaction processed", (), ())); } else { // Idepotential key exists but the record cannot be found, it may be being processed return ("The request is being processed, please do not repeat submission"); } } try { // Execute transfer business logic TransferResult result = (request, idempotentKey); return (result); } catch (Exception e) { // When processing fails, delete the idempotent key and allow the client to try again // Or you can keep the key but record the failed state, depending on the business requirements (redisKey); return ("The transfer process failed: " + ()); } } /** * Generate a summary of the requested content as an idempotent key */ private String generateIdempotentKey(TransferRequest request) { // Combine key fields to ensure that business operations can be uniquely identified String content = () + "|" + () + "|" + ().toString() + "|" + (); // Calculate MD5 summary try { MessageDigest md = ("MD5"); byte[] digest = ((StandardCharsets.UTF_8)); return ().formatHex(digest); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("Creating idempotent key failed", e); } } } @Service public class TransferServiceImpl implements TransferService { @Autowired private TransferRecordRepository transferRecordRepository; @Autowired private AccountRepository accountRepository; @Override @Transactional public TransferResult executeTransfer(TransferRequest request, String idempotentKey) { // Execute transfer business logic // 1. Check the account balance // 2. Deduct source account // 3. Add target account // Generate transaction ID String transactionId = ().toString(); // Save transaction records, including idempotent keys TransferRecord record = new TransferRecord(); (transactionId); (()); (()); (()); (idempotentKey); (); (new Date()); (record); return new TransferResult( transactionId, "Successful transfer", (), ); } @Override public TransferRecord findByIdempotentKey(String idempotentKey) { return (idempotentKey).orElse(null); } } // Transfer record entity@Entity @Table(name = "transfer_records", indexes = { @Index(name = "idx_idempotent_key", columnList = "idempotent_key", unique = true) }) public class TransferRecord { @Id @GeneratedValue(strategy = ) private Long id; private String transactionId; private String fromAccount; private String toAccount; private BigDecimal amount; @Column(name = "idempotent_key") private String idempotentKey; @Enumerated() private TransferStatus status; private Date createTime; // Getters and setters... }
Simplified implementation with custom annotations
// Custom idempotence annotation@Target() @Retention() public @interface Idempotent { /** * Expiration time (seconds) */ int expireSeconds() default 86400; // Default 24 hours /** * Idespicable key source, which can be extracted from request body, request parameters, etc. */ KeySource source() default KeySource.REQUEST_BODY; /** * Extract the expression of the parameter (such as SpEL expression) */ String[] expression() default {}; enum KeySource { REQUEST_BODY, // Request body PATH_VARIABLE, // Path variable REQUEST_PARAM, // Request parameters CUSTOM // Customize } } // AOP implementation@Aspect @Component public class IdempotentAspect { @Autowired private StringRedisTemplate redisTemplate; @Around("@annotation(idempotent)") public Object around(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable { // Get the request parameters Object[] args = (); // Generate idempotent keys according to the annotation configuration String idempotentKey = generateKey(joinPoint, idempotent); String redisKey = "idempotent:digest:" + idempotentKey; // Check whether the request is repeated Boolean setSuccess = () .setIfAbsent(redisKey, "processing", (), ); if ((setSuccess)) { // Get the stored processing results String value = ().get(redisKey); if ("processing".equals(value)) { throw new BusinessException("The request is being processed, please do not repeat submission"); } else if (value != null) { // Processed, return cached results return (value, ); } } try { // Execute the actual method Object result = (); //Storing processing results ().set(redisKey, (result), (), ); return result; } catch (Exception e) { // Processing failed, delete key allows retry (redisKey); throw e; } } /** * Generate idempotent keys according to the annotation configuration */ private String generateKey(ProceedingJoinPoint joinPoint, Idempotent idempotent) { // Extract the request parameters and generate a summary based on KeySource and expression // The actual implementation will be more complicated, so it is simplified here String content = ""; // Calculate MD5 summary try { MessageDigest md = ("MD5"); byte[] digest = ((StandardCharsets.UTF_8)); return ().formatHex(digest); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("Creating idempotent key failed", e); } } } // Notes on the controller@RestController @RequestMapping("/api") public class TransferController { @Autowired private TransferService transferService; @PostMapping("/transfer") @Idempotent(expireSeconds = 3600, source = KeySource.REQUEST_BODY, expression = {"fromAccount", "toAccount", "amount", "requestTime"}) public Result<TransferResult> transfer(@RequestBody TransferRequest request) { // Execute transfer business logic TransferResult result = (request); return (result); } }
Pros and cons analysis
advantage
- More general solutions
- Relatively simple to implement and easy to integrate
- Client-friendly, no additional token requests are required
shortcoming
- Hash computing has certain performance overhead
- Changes in the order of form data may result in different summary values
Summarize
Impotence design is an important guarantee for system stability and reliability. By rationally selecting and implementing idempotence strategies, data inconsistency caused by repeated requests can be effectively prevented. In actual projects, the most suitable idempotence implementation solution should be selected based on the specific business needs and system architecture.
The above is the detailed content of the implementation strategies of four interface idempotence in SpringBoot. For more information about the idempotence of SpringBoot interface, please pay attention to my other related articles!