“This is the ninth day of my participation in the First Challenge 2022. For details: First Challenge 2022”

Distributed lock scenario

Second kill, grab coupons, interface idempotent check, etc

Single machine single thread

Along with the development of the Internet, Lao wang saw the development of the Internet dividends, so after his resignation, just started a business service platform, pants is developed for a period of time, the project released online, in 618 will arrive soon, send some coupons do the thinking of large presses, active users, by the way, make a fortune! Then Wang ermazi, the brother of old Cousin Wang’s daughter-in-law, wrote a program to snatch coupons. The code is as follows:

@RequestMapping("/get_coupon") public String deductStock() { int coupon = Integer.parseInt(stringRedisTemplate.opsForValue().get("coupon_100")); // jedis.get("stock") if (coupon > 0) { int currCoupons = coupon - 1; stringRedisTemplate.opsForValue().set("coupon_100", currCoupons + ""); // jedis.set(key,value) system.out.println (" Success, remaining inventory :" + currCoupons); } else {system.out.println (" deduction failed, stock insufficient "); } return "success"; }Copy the code

2. If the number of coupons is greater than 0, then -1, and put the reduced amount into Redis again. Wang Erma son thought so not to achieve a grab coupons function, their local test is no problem, and then submitted to the test.

The test student ran a test with Jmeter, and found that hundreds of coupons of 1000 were oversold. The test report was submitted immediately and fed back to Wang Ermazi.

Single machine multithreading concurrency

Two pitted see the test result, close scrutiny of the code, and found that if more concurrent, can really oversold int coupon = Integer. The parseInt (stringRedisTemplate. OpsForValue () get (” coupon_100 “)); If N threads come in at the same time, the number of coupons they get is 1000, and the code after that is -1, it doesn’t make any sense. Thread A: 1000-1=999, and put it in Redis. Thread B: 1000-1=999, and put it in Redis. A lock is added to this code, so that the concurrency is more immediate, and also a thread by thread to execute the inventory-1 operation, so there is no problem, so the code is updated as follows:

@RequestMapping("/get_coupon") public String deductStock() { synchronized (this){ int coupon = Integer.parseInt(stringRedisTemplate.opsForValue().get("coupon_100")); // jedis.get("stock") if (coupon > 0) { int currCoupons = coupon - 1; stringRedisTemplate.opsForValue().set("coupon_100", currCoupons + ""); // jedis.set(key,value) system.out.println (" Success, remaining inventory :" + currCoupons); } else {system.out.println (" deduction failed, stock insufficient "); } } return "success"; }Copy the code

Then Wang er Ma zi resubmitted the test. The test student got the test and ran with Jmeter and said, Hey! Really nothing, coupons will not be oversold, I thought wang Er ma Zi this guy to solve the bug efficiency is good, so the test students released the code to the pre-release environment, the pre-release environment is like this:

Results found coupons, and was robbed over, test students then hit the bug back to wang Two pockmarked son.

A distributed lock

Redis setNx application

If it is a polymorphic server, the client thread is distributed to Tomcat through nginx, and synchronization cannot be used to lock tomcat, so we have to think of a distributed lock, so after Wang Ermazi modification, The code is updated to look like this:

@RequestMapping("/get_coupon") public String deductStock() { String couponKey="coupon_100"; Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(couponKey, "lock"); if(! Lock){return "queue "; } / / - business process - the begin int coupon = Integer. The parseInt (stringRedisTemplate. OpsForValue () get (" coupon_100 ")); // jedis.get("stock") if (coupon > 0) { int currCoupons = coupon - 1; stringRedisTemplate.opsForValue().set("coupon_100", currCoupons + ""); // jedis.set(key,value) system.out.println (" Success, remaining inventory :" + currCoupons); } else {system.out.println (" deduction failed, stock insufficient "); } / / -- business processing - end stringRedisTemplate. Delete (couponKey); return "success"; }Copy the code

Code parsing: through the use of redis setnx method, the first thread in the first setnx value, so that the subsequent thread setnx can not be successfully executed, can only wait for the first thread delete key, the following thread can continue to execute setnx, a simple distributed lock.

Ensure that the Redis delete key succeeds by finally

But after the first two are tested twice back bugs, wang decided to classmate, close scrutiny the code again, repeatedly legacy, the result was found a problem, one thousand abnormal, in this part business processing code that stringRedisTemplate. Delete (couponKey); No thread can come in after that, so the code is updated as follows:

@RequestMapping("/get_coupon") public String deductStock() { String couponKey="coupon_100"; try { Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(couponKey, "lock"); if(! Lock){return "queue "; } / / - business process - the begin int coupon = Integer. The parseInt (stringRedisTemplate. OpsForValue () get (" coupon_100 ")); // jedis.get("stock") if (coupon > 0) { int currCoupons = coupon - 1; stringRedisTemplate.opsForValue().set("coupon_100", currCoupons + ""); // jedis.set(key,value) system.out.println (" Success, remaining inventory :" + currCoupons); } else {system.out.println (" deduction failed, stock insufficient "); } / / - business process - the finally end} {stringRedisTemplate. Delete (couponKey); } return "success"; }Copy the code

The abnormal, so even if the execution of a business code through the finally also can perform stringRedisTemplate. Delete (couponKey); In this way, you don’t have to worry about an exception in the program that can’t execute the DELETE key.

The use of redis key is valid (stringRedisTemplate. The use of the expire)

Wang students still don’t rest assured, and the code, and found one thousand program in the process of execution, in the business code, server goes down, the stringRedisTemplate. Delete (couponKey); I will never be able to execute it. When I think of it, I am afraid that I did not submit the test, or I will be despised in the heart of the test. So the code is modified as follows:

@RequestMapping("/get_coupon") public String deductStock() { String couponKey="coupon_100"; try { Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(couponKey, "lock"); / / thread lock to 10 SECONDS, 10 SECONDS automatically delete key stringRedisTemplate. Expire (couponKey, 10, TimeUnit. SECONDS); if(! Lock){return "queue "; } / / - business process - the begin int coupon = Integer. The parseInt (stringRedisTemplate. OpsForValue () get (" coupon_100 ")); // jedis.get("stock") if (coupon > 0) { int currCoupons = coupon - 1; stringRedisTemplate.opsForValue().set("coupon_100", currCoupons + ""); // jedis.set(key,value) system.out.println (" Success, remaining inventory :" + currCoupons); } else {system.out.println (" deduction failed, stock insufficient "); } / / - business process - the finally end} {stringRedisTemplate. Delete (couponKey); } return "success"; }Copy the code

Add the following code to delete the key even if it is down in the process of processing business.

/ / thread lock to 10 SECONDS, 10 SECONDS automatically delete key stringRedisTemplate. Expire (couponKey, 10, TimeUnit. SECONDS);Copy the code

However, if the following code is executed after the crash, then the above code will not execute, still have to figure out a way to deal with it

Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(couponKey, "lock");
Copy the code

Redis set key Also sets the validity time

The code is modified again as follows:

@RequestMapping("/get_coupon") public String deductStock() { String couponKey="coupon_100"; try { Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(couponKey, "lock",10,TimeUnit.SECONDS); if(! Lock){return "queue "; } / / - business process - the begin int coupon = Integer. The parseInt (stringRedisTemplate. OpsForValue () get (" coupon_100 ")); // jedis.get("stock") if (coupon > 0) { int currCoupons = coupon - 1; stringRedisTemplate.opsForValue().set("coupon_100", currCoupons + ""); // jedis.set(key,value) system.out.println (" Success, remaining inventory :" + currCoupons); } else {system.out.println (" deduction failed, stock insufficient "); } / / - business process - the finally end} {stringRedisTemplate. Delete (couponKey); } return "success"; }Copy the code

When redis set the key, set the validity period

Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(couponKey, "lock",10,TimeUnit.SECONDS);
Copy the code

Xiao Wang thought that the code should be no problem? But to be prudent, I had to think about it a little more carefully, and I did find a problem, as shown below

Explanation:

When the first thread in and lock 10 s, but the thread 1 business code execution time of more than 10 s, thread 1 lock will be null and void automatically, lock, thread 2 at this time you can come in and perform business code, when the thread 2 business code execution, thread 1 business code execution is completed, just delete the lock, thread 2 locks deleted as a result, At this point, thread 3 can come in and lock, so that all thread locks are not deleted by themselves, but are deleted by the previous thread, and the data lock is meaningless.

Wang thought, how to let the back of the thread can not delete the front of the thread said?

Sets a unique value for the lock

So After careful consideration, Xiao Wang reformed the code as follows

@RequestMapping("/get_coupon") public String deductStock() { String couponKey="coupon_100"; String lockId= UUID.randomUUID().toString(); try { Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(couponKey, lockId,10,TimeUnit.SECONDS); if(! Lock){return "queue "; } / / - business process - the begin int coupon = Integer. The parseInt (stringRedisTemplate. OpsForValue () get (" coupon_100 ")); // jedis.get("stock") if (coupon > 0) { int currCoupons = coupon - 1; stringRedisTemplate.opsForValue().set("coupon_100", currCoupons + ""); // jedis.set(key,value) system.out.println (" Success, remaining inventory :" + currCoupons); } else {system.out.println (" deduction failed, stock insufficient "); } / / - business process - the finally end} {the if (lockId. Equals (stringRedisTemplate. OpsForValue () get (couponKey))) { stringRedisTemplate.delete(couponKey); } } return "success"; }Copy the code

Code resolution: Give each thread a unique ID(UUID). Select * from thread ID where ID = 0; select * from thread ID;

if(lockId.equals(stringRedisTemplate.opsForValue().get(couponKey))){
    stringRedisTemplate.delete(couponKey);
}
Copy the code

In this way, the thread can only delete its own lock, but the execution time of business code is uncontrollable, that is, the lock time cannot be accurate. As long as the lock times out, the business code may be executed by multiple threads at the same time, and the lock is meaningless.

Lock the lives

Imagine: if the business code is normal and still executing, we add a function that can automatically prolong the lock time. After the business code is executed, the lock is deleted, and then the automatic delay lock function stops running, so that the lock timeout problem can be solved. The diagram below:

How does releasing locks preserve atomicity

If the lock is extended every 10 seconds, it will stop running. If the lock is automatically deleted after 30 seconds, the program will continue to run. What if an exception occurs and the lock extension continues every 10 seconds? Another question

finally { if(lockId.equals(stringRedisTemplate.opsForValue().get(couponKey))){ stringRedisTemplate.delete(couponKey); }}Copy the code

Redisson tools

The above mentioned lock delay and delete the atomicity of the lock, it has helped to do the solution, the following step by step to shorthand use, and source analysis. Redisson website: redisson.org/

Springboot integration redisson

Pom. XML is introduced into redisson

<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.9.1</version>Copy the code

Springboot initializes redisson

Add the following code to the SpringBoot boot file

@SpringBootApplication public class MyredisApplication { public static void main(String[] args) { SpringApplication.run(MyredisApplication.class, args); } @Bean public Redisson redisson(){ Config config=new Config(); / / redis use single redisson config. UseSingleServer () setAddress (" redis: / / 192.168.253.131:6379 "). The setDatabase (0); / * / / redis use cluster config. UseClusterServers () addNodeAddress (" redis: / / 192.168.253.131:8001)" AddNodeAddress (" redis: / / 192.168.253.131:8002 "). The addNodeAddress (" redis: / / 192.168.253.132:8003)" AddNodeAddress (" redis: / / 192.168.253.132:8004 "). The addNodeAddress (" redis: / / 192.168.253.133:8005)" AddNodeAddress (" redis: / / 192.168.253.133:8006 "); */ return (Redisson) Redisson.create(config); }}Copy the code

For my test use, redisson provides connection schemes for different types of Redis deployment using stand-alone connections

Specific use of Redisson

public class CouponController { @Autowired private StringRedisTemplate stringRedisTemplate; @Autowired private Redisson redisson; @RequestMapping("/get_coupon") public String deductStock() { String couponKey="coupon_100"; RLock redissonLock=redisson.getLock(couponKey); Redissonlock. lock(); redisson.lock (); / / - business process - the begin int coupon = Integer. The parseInt (stringRedisTemplate. OpsForValue () get (" coupon_100 ")); // jedis.get("stock") if (coupon > 0) { int currCoupons = coupon - 1; stringRedisTemplate.opsForValue().set("coupon_100", currCoupons + ""); // jedis.set(key,value) system.out.println (" Success, remaining inventory :" + currCoupons); } else {system.out.println (" deduction failed, stock insufficient "); } / / - business process - the finally end} {/ / unlock redissonLock. Unlock (); } return "success"; }}Copy the code

The above code is mainly added:

1. Inject redisson into controller

@Autowired
private Redisson redisson;
Copy the code

2. Get the Redisson lock

RLock redissonLock=redisson.getLock(couponKey);Copy the code

3, lock, the bottom code at the same time to achieve lock and watchdog lock life function

RLock redissonLock=redisson.getLock(couponKey);Copy the code

Release the lock

/ / unlock redissonLock. Unlock ();Copy the code

This simple introduction can realize the application of high concurrency distributed lock. Because the space is too long, the next article will introduce the basic implementation code principle of Redisson. If you are interested, you can read one article.