Location>code7788 >text

Redis realizes counter design in high concurrency scenarios

Popularity:967 ℃/2025-04-14 08:19:27

Most Internet companies need to deal with counter scenarios, such as request frequency control of risk control systems, playback statistics of content platforms, inventory deductions of e-commerce systems, etc.

Traditional solutions are generally used directly(key), this is the easiest way, but this way can expose serious problems in production environments:

// Example of hidden dangers
 public long addOne(String key) {
     Long result = (key);
     // If TTL is not set, the key will permanently reside in memory.
     return result;
 }

INCR hasAutomatic initialization mechanism, that is, when Redis detects that the target key does not exist, it willAutomatically initialize it to 0, then perform incremental operation

Implementation of high availability counters

Atomic operation guarantees counting accuracy

NX+EX atomic initialization

(key, "0", "nx", "ex", time);

By RedisSET key value NX EXCommand, implement atomized "create if not exists +Set expiration time", avoid data overwriting by multiple threads due to competition for initialization (for example, after thread A is initialized, thread B uses SET to cover the value of 0)

Redis single-threaded model ensures command atomicity without additional distributed locks

Use the setnx command to set the expiration time to prevent the key from expired

INCR atomic increment

long result = (key);

First setnx command, then use INCR to perform incremental operations

Right now:

public void addOne(String key) {
    (key, "0", "nx", "ex", time);
    Long result = (key); 
	return result;
}

Dual compensation mechanism solves expired exceptions

But just using the above two commands may still lead to concurrency security problems.

For example:

When two threads execute SETNX at the same time, the initialized thread is not grabbed and directly executes INCR, resulting in the existence of key but no TTL

If there is a thread A that is executingSET key 0 NX EX 60, and thread B also executes the method addOne. At this time, thread A is executing, thread B cannot execute the set operation, and will directly continue to execute subsequent commands (such as INCR). At this time, if thread A fails to initialize the key due to network jitter and other reasons, it may cause the key to never expire. Therefore, there is a compensation mechanism to complete the setting of the redis key timeout time

Note: When the SETNX command cannot be executed (i.e., when the target key already exists), the subsequent command (such as INCR) will be directly executed without blocking and waiting.

First incremental compensation

Therefore, it can be judged byresult == 1To identify whether it is the first increment, if it is the first increment, it will be forced to renew

if (result == 1) {
    (key, time);
}

TTL abnormality detection compensation

existExtreme scenes(Redis master-slave switching, command execution exception causes TTL to be lost), key may exist for a long time due to unset or expiration time lost

if ((key) == -1) {
    (key, time);
}

Check if the TTL is-1(-1 means no expiration time), reset the expiration time as a bottom-up protection.

The code after the double compensation mechanism is as follows:

public void addOne(String key) {
     (key, "0", "nx", "ex", time);
     Long result = (key);
     //Solve concurrency problems, otherwise the counter will never be cleared
     //If the result of incr is 1, there are two results, first perform the set operation, and there is an expiration time.  The second type: directly execute the incr operation, and the redisKey at this time has no expiration time.  Therefore, compensation is required
     if (result == 1) {
          (key, time);
     }

     // Check whether there is an expiration time, and the key compensation for the expiration time is not set for the exception.
     if ((key) == -1) {
          (key, time);
     }
     return result;
 }

Exception handling and downgrade strategies

Sometimes it may be due to network jitter, short-term service unavailability, main and backup switching, etc.Temporary malfunction, resulting in the Redis operation failure, so the exception can be processed, the operations that need to be completed can be put into the queue, and then a thread loop is used to retry to ensure final consistency

public void addOne(String key) {
     Long result = 1;
     try{
         (key, "0", "nx", "ex", time);
         result = (key);
         //Solve concurrency problems, otherwise the counter will never be cleared
         //If the result of incr is 1, there are two results, first perform the set operation, and there is an expiration time.  The second type: directly execute the incr operation, and the redisKey at this time has no expiration time.  Therefore, compensation is required
         if (result == 1) {
              (key, time);
         }

         // Check whether there is an expiration time, and the key compensation for the expiration time is not set for the exception.
         if ((key) == -1) {
              (key, time);
         }
     } catch (Exception e) {
         //Drop it into the retry queue and keep trying again
    	 (key);
	 }
     return result;
 }

Architectural design diagram

graph TD A[Client Request] --> B{Key exists?} B -->|No| C[SET NX EX Initialization] B -->|Yes| D[INCR atomic increment] C --> D D --> E{result=1?} E -->|Yes| F[Compensation Settings TTL] E -->|No| G[check TTL] G -->|TTL=-1| H[secondary compensation] G -->|TTL is normal| I[Return result] H --> I F --> I

Comparison of key mechanisms

mechanism Problems solved Redis feature utilization Performance impact
SET NX EX Concurrent initialization of competition Atomic single command O(1)
INCR Inaccurate counting/oversold Atomic increment O(1)
TTL double compensation Key never expires EXPIRE command idempotence 1 extra query
Retry the exception queue Network Jitter/Redis is not available Final consistency Asynchronous processing

This solution fully taps the potential of Redis atomic commands, compensates for the uncertainty of distributed systems through compensation mechanisms, and ultimately finds a balance between simplicity and reliability.

Previous recommendations

  • "SpringBoot" EasyExcel implements the import and export of millions of data
  • "SpringBoot" The most complete SpringBoot commentary in history
  • Detailed explanation of Spring Framework IoC Core
  • A long article of ten thousand words will take you to explore all the expansion points in Spring
  • How to implement a general interface current limiting, heavy-weight and anti-shake mechanism
  • A long article of ten thousand words will take you into the underlying data structure of Redis
  • Analysis of the most comprehensive principle of volatile keyword