Spring Boot + Redis: Simulate 100,000 people’s spike grab orders

Foreword

The content of this article mainly explains the redis distributed lock, which is almost necessary for interviews in major factories. The following uses it in combination with the scene of simulating order grabbing; this article does not involve the redis environment construction, and quickly builds a personal test Environment, it is recommended to use docker here; the content nodes of this article are as follows:

Jedis nx generate lock

  • how to remove lock
  • Simulate the action of grabbing orders (100,000 individuals start grabbing)
  • NX of jedis generates locks

For operating redis in java, a good way is to use jedis, first introduce dependencies in pom:

<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>

For the generation of distributed locks, you usually need to pay attention to the following aspects:

Lock creation strategy: Redis common keys are generally allowed to be overwritten. After user A sets a certain key, B can also succeed in setting the same key. If it is a lock scenario, it is impossible to know which user successfully set it; Here, the setnx method of jedis solves this problem for us. The simple principle is: when user A succeeds in setting first, then user B returns failure when setting, and only one user is allowed to get the lock at a certain point in time.

Lock expiration time: In a snap-up scenario, if there is no concept of expiration, when user A generates a lock, but the subsequent process is blocked and the lock cannot be released, other users will always fail to acquire the lock at this time and cannot complete the snap-up activities; of course, under normal circumstances, it will not be blocked, and the A user process will release the lock normally; the expiration time is just for more security.

Here is the code for the above setnx operation:

public boolean setnx(String key, String val) {<!-- -->
 Jedis jedis = ;
try {<!-- -->
 jedis = jedisPool. getResource();
if (jedis == ) {<!-- -->
return false;
 }
return jedis.set(key, val, "NX", "PX", 1000 * 60).
 equalsIgnoreCase("ok");
 } catch (Exception ex) {<!-- -->
 } finally {<!-- -->
if (jedis != ) {<!-- -->
 jedis. close();
 }
 }
return false;
 }

The point of attention here is the set method of jedis, and the description of its parameters is as follows:

NX: Whether there is a key, if it exists, the set will not succeed
PX: The key expiration time unit is set to milliseconds (EX: in seconds)

If setnx fails, just encapsulate and return false. Next, we call the setnx method through a get method API:

@GetMapping("/setnx/{key}/{val}")
public boolean setnx(@PathVariable String key, @PathVariable String val) {<!-- -->
returnjedisCom.setnx(key, val);
}

Visit the following test url. Normally, true is returned for the first time, and false is returned for the second time. Since the redis key already exists at the time of the second request, it cannot be successfully set


From the above figure, we can see that only one set is successful, and the key has a valid time, and the condition of the distributed lock has been reached at this time.

How to remove a lock

The above is to create a lock, which also has an effective time, but we cannot completely rely on this effective time. For example, the scenario is: the effective time is set for 1 minute. After user A obtains the lock, he does not encounter any special circumstances and generates a snap-up order normally. At this time, other users should be able to place orders normally, but since there is a lock that can be released automatically after 1 minute, other users cannot place orders normally during this 1 minute (because the lock still belongs to user A), so we need user A to operate When finished, take the initiative to unlock:

public int delnx(String key, String val) {<!-- -->
 Jedis jedis = ;
try {<!-- -->
 jedis = jedisPool. getResource();
if (jedis == ) {<!-- -->
return0;
 }

//if redis.call('get','orderkey')=='1111' then return redis.call('del','orderkey') else return 0 end
 StringBuilder sbScript = new StringBuilder();
 sbScript.append("if redis.call('get','").append(key).append("')").append("=='") .append(val).append("'").
 append(" then ").
 append(" return redis.call('del','").append(key).append("')").
 append(" else ").
 append(" return 0").
 append("end");

return Integer.valueOf(jedis.eval(sbScript.toString()).toString());
 } catch (Exception ex) {<!-- -->
 } finally {<!-- -->
if (jedis != ) {<!-- -->
 jedis. close();
 }
 }
return0;
 }

The jedis method is also used here to directly execute the lua script: judge whether it exists according to val, and del if it exists;

In fact, I personally think that after obtaining val through the get method of jedis, and then comparing whether the value is the user currently holding the lock, if it is deleted at the end, the effect is actually the same; but the script is executed directly through eval, so as to avoid one more operation Redis only shortens the interval of atomic operations. (If you have different opinions, please leave a message to discuss); also create a get method api here to test:

@GetMapping("/delnx/{key}/{val}")
public int delnx(@PathVariable String key, @PathVariable String val) {<!-- -->
returnjedisCom.delnx(key, val);
}

Note that when delnx is used, it is necessary to pass the value when the lock is created, because the value of et and the value of delnx are used to judge whether it is an operation request to hold a lock, and del is only allowed if the value is the same;

Simulate grabbing action (100,000 individuals start grabbing)

With the above rough basis for distributed locks, we simulate the scene of 100,000 people grabbing orders, which is actually just a concurrent operation request. Due to the limited environment, we can only test in this way; initialize 100,000 users, and initialize inventory, commodities as follows and other information, the following code:

//total inventory
privatelong nKuCuen = 0;
//Commodity key name
private String shangpingKey = "computer_key";
// Timeout for acquiring locks in seconds
privateint timeout = 30 * 1000;

@GetMapping("/qiangdan")
public List<String> qiangdan() {<!-- -->

//The user who grabbed the product
 List<String> shopUsers = new ArrayList<>();

//Construct many users
 List<String> users = new ArrayList<>();
 IntStream. range(0, 100000). parallel(). forEach(b -> {<!-- -->
 users.add("God Cow-" + b);
 });

//Initialize inventory
 nKuCuen = 10;

//Simulate opening grab
 users.parallelStream().forEach(b -> {<!-- -->
 String shopUser = qiang(b);
if (!StringUtils.isEmpty(shopUser)) {<!-- -->
 shopUsers. add(shopUser);
 }
 });

return shopUsers;
 }

With the above 100,000 different users, we set the product to only have 10 stocks, and then simulate the panic buying through parallel flow, as follows:

/**
 * Simulate grabbing action
 *
 * @param b
 * @return
 */
private String qiang(String b) {<!-- -->
//User starts grabbing time
long startTime = System. currentTimeMillis();

//Continue to acquire the lock within 30 seconds if it is not grabbed
while ((startTime + timeout) >= System. currentTimeMillis()) {<!-- -->
// Is the item left
if (nKuCuen <= 0) {<!-- -->
break;
 }
if (jedisCom.setnx(shangpingKey, b)) {<!-- -->
//User b gets the lock
 logger.info("User {} got the lock...", b);
try {<!-- -->
// Is the item left
if (nKuCuen <= 0) {<!-- -->
break;
 }

//Simulate the time-consuming operation of order generation for easy viewing: Shenniu-50 times to acquire lock records
try {<!-- -->
 TimeUnit. SECONDS. sleep(1);
 } catch (InterruptedException e) {<!-- -->
 e.printStackTrace();
 }

//Purchasing is successful, the product is decremented, and the user is recorded
 nKuCuen -= 1;

//Successfully grab the order and jump out
 logger.info("User {} grabs the order successfully and jumps out...remaining inventory: {}", b, nKuCuen);

return b + "Successful order grabbing, remaining inventory: " + nKuCuen;
 } finally {<!-- -->
 logger.info("User {} releases the lock...", b);
// release the lock
 jedisCom.delnx(shangpingKey, b);
 }
 } else {<!-- -->
//User b does not get the lock, and continues to request the lock within the timeout range, no processing is required
// if (b.equals("God Cow-50") || b.equals("God Cow-69")) {<!-- -->
// logger.info("User{} is waiting to acquire the lock...", b);
// }
 }
 }
return"";
 }

The logic implemented here is:

1. parallelStream(): Parallel stream simulates multi-user buying

2. (startTime + timeout) >= System.currentTimeMillis(): Determine the user who has not successfully grabbed the lock, and continue to acquire the lock within timeout seconds

3. Judging whether the inventory is sufficient before and after acquiring the lock

4. jedisCom.setnx(shangpingKey, b): The user obtains the snapping lock

5. After acquiring the lock and placing an order successfully, finally release the lock: jedisCom.delnx(shangpingKey, b)

Let’s look at the recorded log results:

Finally, the user who successfully snapped up is returned: