Scene: an online shopping mall to do commodity limited seconds kill.

1 Lock in a single-node environment

Store the quantity of goods in Redis. Every user needs to check the quantity of goods in Redis (instead of mysql database) before buying goods. Regardless of transactions), if the quantity of goods is greater than 0, it proves that goods are in stock. And then we’re doing the inventory deduction and the following operations. Because of multithreaded concurrency issues, we have to use synchronized code blocks inside the get() method. This ensures atomicity of inventory query and deinventory operations.

package springbootdemo.demo.controller; /* * @auther * @mail [email protected] * @date 2020-01-13 11:19 * @notify * @version 1.0 */ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class RedisLock { @Autowired private RedisTemplate<String, String> redisTemplate; @GetMapping(value = "buy") public String get() { synchronized (this) { String phone = redisTemplate.opsForValue().get("phone"); Integer count = Integer.valueOf(phone); if (count > 0) { redisTemplate.opsForValue().set("phone", String.valueOf(count - 1)); System.out.println(" grab "+ count +" item "); }return ""; }}}Copy the code

View Code

2 Redis lock is used in distributed mode.

However, as traffic increases, the number of concurrent transactions increases. The company had to make a copy of the original system and put it on a new server. Then use Nginx for load balancing. The Apache JMeter tool is used to simulate a high-concurrency environment. \

Obviously, the current thread lock doesn’t work. So we need a new lock that has no coupling between the two systems.

Use the Redies API to set a key if it does not exist. This key is the lock we use today. If the lock fails to be set, it indicates that the current thread has acquired the lock and returns. We ended up throwing exceptions instead of releasing locks in order to reduce inventory and other business. Put the lock release operation in the finally code block. It looks perfect.

package springbootdemo.demo.controller; /* * @auther * @mail [email protected] * @date 2020-01-13 11:19 * @notify * @version 1.0 */ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class RedisLock { @Autowired private RedisTemplate<String, String> redisTemplate; @GetMapping(value = "buy") public String get() { Boolean phoneLock = redisTemplate.opsForValue().setIfAbsent("phoneLock", ""); if (! phoneLock) { return ""; } try{ String phone = redisTemplate.opsForValue().get("phone"); Integer count = Integer.valueOf(phone); if (count > 0) { redisTemplate.opsForValue().set("phone", String.valueOf(count - 1)); System.out.println(" grab "+ count +" item "); } }finally { redisTemplate.delete("phoneLock"); } return ""; }}Copy the code

View Code

3 One service is down, and the lock cannot be released

If an exception is thrown in finally and a service is closed in finally, the lock will still be released without affecting other threads to acquire the lock. If an exception is thrown in finally, or a service is closed in finally, the lock will not be acquired by any other service. Eventually, the product doesn’t sell.

package springbootdemo.demo.controller; /* * @auther * @mail [email protected] * @date 2020-01-13 11:19 * @notify * @version 1.0 */ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class RedisLock { @Autowired private RedisTemplate<String, String> redisTemplate; @GetMapping(value = "buy") public String get() { int i = 0; Boolean phoneLock = redisTemplate.opsForValue().setIfAbsent("phoneLock", ""); if (! phoneLock) { return ""; } try { String phone = redisTemplate.opsForValue().get("phone"); Integer count = Integer.valueOf(phone); if (count > 0) { i = count; redisTemplate.opsForValue().set("phone", String.valueOf(count - 1)); System.out.println(" grab "+ count +" item "); } } finally { if (i == 20) { System.exit(0); } redisTemplate.delete("phoneLock"); } return ""; }}Copy the code

View Code

Add an expiration date to each lock

The problem is that if something goes wrong, the lock can’t be released. Here we are introducing Redis API to set the expiration time of key. In this way, if the thread holding the lock has not had time to release the lock in any case, the lock will be automatically released when the Redis key expires. But there are problems with that

If the lock is released after the key expires, but the current thread is not finished executing. The other thread will take the lock and continue to buy the item, while the slower thread will release the lock once it’s finished. Lock failure!

package springbootdemo.demo.controller; /* * @auther * @mail [email protected] * @date 2020-01-13 11:19 * @notify * @version 1.0 */ import javafx.concurrent.Task; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.TimeUnit; @RestController public class RedisLock { @Autowired private RedisTemplate<String, String> redisTemplate; @GetMapping(value = "buy") public String get() { Boolean phoneLock = redisTemplate.opsForValue().setIfAbsent("phoneLock", "", 3, TimeUnit.SECONDS); if (! phoneLock) { return ""; } try { String phone = redisTemplate.opsForValue().get("phone"); Integer count = Integer.valueOf(phone); if (count > 0) { try { Thread.sleep(99999999999L); } catch (Exception e) { } redisTemplate.opsForValue().set("phone", String.valueOf(count - 1)); System.out.println(" grab "+ count +" item "); } } finally { redisTemplate.delete("phoneLock"); } return ""; }}Copy the code

View Code

5 Extend the lock expiration time to resolve lock failure

The problem arises when a thread’s key has expired, but the thread’s task has not completed, and the transaction has not been completed. But the lock was gone. Now we have to extend the lock time. If the item is in stock, create a thread to keep the key alive.

Prevent key expiration. Then, after the transaction, stop the timer and release the lock.

package springbootdemo.demo.controller; /* * @auther * @mail [email protected] * @date 2020-01-13 11:19 * @notify * @version 1.0 */ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.TimeUnit; @RestController public class RedisLock { @Autowired private RedisTemplate<String, String> redisTemplate; @GetMapping(value = "buy") public String get() { Boolean phoneLock = redisTemplate.opsForValue().setIfAbsent("phoneLock", "", 3, TimeUnit.SECONDS); if (! phoneLock) { return ""; } Timer timer = null; try { String phone = redisTemplate.opsForValue().get("phone"); Integer count = Integer.valueOf(phone); if (count > 0) { timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { redisTemplate.opsForValue().set("phoneLock", "", 3, TimeUnit.SECONDS); }}, 0, 1); redisTemplate.opsForValue().set("phone", String.valueOf(count - 1)); System.out.println(" grab "+ count +" item "); } } finally { if (timer ! = null) { timer.cancel(); } redisTemplate.delete("phoneLock"); } return ""; }}Copy the code

View Code

Simplify code with Redisson

In Step 5, our code is already well developed and does not have high concurrency issues. However, the code is too redundant. In order to use the Redis lock, we need to set a key of fixed length, and then delete the key when the purchase is completed. However, in order to prevent the key from expiring early, we have to create a new thread to perform the scheduled task. Now we can use the Redissson framework to simplify the code. The getLock() method replaces Redis’ setIfAbsent(), and lock() sets the expiration time. Eventually we release the lock after the transaction. The Redisson framework does this for us, which uses polling to see if the key is out of date, \

Automatically reset the Redis key expiration time if the transaction is not completed

package springbootdemo.demo.controller; /* * @auther * @mail [email protected] * @date 2020-01-13 11:19 * @notify * @version 1.0 */ import org.redisson.Redisson; import org.redisson.api.RLock; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.TimeUnit; @RestController public class RedissonLock { @Autowired private RedisTemplate<String, String> redisTemplate; @Autowired private Redisson redisson; @GetMapping(value = "buy2") public String get() { RLock phoneLock = redisson.getLock("phoneLock"); phoneLock.lock(3, TimeUnit.SECONDS); try { String phone = redisTemplate.opsForValue().get("phone"); Integer count = Integer.valueOf(phone); if (count > 0) { redisTemplate.opsForValue().set("phone", String.valueOf(count - 1)); System.out.println(" grab "+ count +" item "); } } finally { phoneLock.unlock(); } return ""; }}Copy the code

View Code