SoFunction
Updated on 2025-04-17

Implementation strategies for 4 interface idempotence in SpringBoot

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!