当前位置:网站首页>How to implement a distributed lock with redis

How to implement a distributed lock with redis

2022-06-23 11:14:00 InfoQ

Scene simulation

General e-commerce website will encounter, such as group buying 、 seckill 、 Special offers and things like that , One of the common characteristics of such activities is the surge of visits 、 Thousands or even tens of thousands of people rush to buy a commodity . However , As an event commodity , Inventory must be very limited , How to control inventory to avoid overbought , To prevent unnecessary losses is a headache for many e-commerce website programmers , This is also the most basic problem .

In the design of seckill system ,
Oversold
It's a classic 、 Common questions , There is an upper limit on the quantity of any commodity , How to avoid the number of people who successfully place an order to buy goods does not exceed the upper limit of the number of goods , This is the difficulty of every rush buying activity .

For a large number of concurrent requests , We can go through  Redis  To resist , That is to say, for inventory direct request  Redis  cache , Do not request the database directly , If in  Redis  There is  50  Inventory , as follows :

null
But whether it's a cache or a database , Without any treatment , There will be overbought problems , A common way to deal with this is through... In the code
JVM  Lock
The way , as follows :

server1

@RestController
public class SkillController {

 @Autowired
 private RedisTemplate redisTemplate;

 //  Seckill interface
 @RequestMapping("/deduct_stock")
 public String deductStock() {

 //  Lock
 synchronized (this) {
 int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock").toString());
 if (stock > 0) {
 //  stock  -1
 int realStock = stock - 1;
 //  Deducting the inventory
 redisTemplate.opsForValue().set("stock", realStock + "");
 System.out.println(" Deduction succeeded , Surplus stock :" + realStock);
 } else {
 System.out.println(" Deduction failed , Insufficient inventory ");
 }
 }

 return "8080";
 }
}

Of course , In the case of single machine, there is really no problem , But now most systems are distributed systems , Even if it is  ERP  The system will also be deployed  2  This machine prevents
A single point of failure
, So in general, a request is as follows :

null
server2

server2  and  server1  The code is basically the same , It's just on  2  individual  JVM  example .

@RestController
public class SkillController {

 @Autowired
 private RedisTemplate redisTemplate;

 //  Seckill interface
 @RequestMapping("/deduct_stock")
 public String deductStock() {

 //  Lock
 synchronized (this) {
 int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock").toString());
 if (stock > 0) {
 //  stock  -1
 int realStock = stock - 1;
 //  Deducting the inventory
 redisTemplate.opsForValue().set("stock", realStock + "");
 System.out.println(" Deduction succeeded , Surplus stock :" + realStock);
 } else {
 System.out.println(" Deduction failed , Insufficient inventory ");
 }
 }

 return "8090";
 }
}

Nginx

Generally speaking , Front end by  nginx  The request is forwarded and passed  upstream  Load balancing , The key configurations are as follows :

null
Jmeter

We pass through here  Jmeter  To conduct concurrent pressure measurement , Useless reference  
Jmeter  Use
, Then there is a download link :
Jmeter  download
 ( Extraction code :2hyo).

Concurrent request
:1 s  Inside  200  A request ( Simulate high concurrency ), loop  5  Time , altogether  1000  Total requests .

null
Request address
: Second kill inventory reduction interface .

null

JVM  lock

Understand the configuration above , Then start  2  An example , Ports are  8080,8090, as follows :

null
If you don't know how to start  2  See below for an example :

null
Be careful : To modify the boot port .

Use  JVM  There is a problem with locks, which are the way code blocks are synchronized , The results of the above test are as follows :

null
Not only  2  Two services have the same inventory at the same time , Even the same service has the same value , Obviously, in a highly concurrent distributed scenario ,JVM  Level lock is not feasible .

Redis SETNX

SETNX
Format :setnx key value
take  key  The value of the set  value , If and only if  key  non-existent .
If a given  key  Already exist , be  SETNX  Don't do anything .
SETNX  yes 『SET if Not eXists』( If it doesn't exist , be  SET) Abbreviation .

1、 Implement the simplest distributed lock

@RestController
public class SkillController {

 @Autowired
 private RedisTemplate redisTemplate;

 @RequestMapping("/deduct_stock")
 public String deductStock() {

 //  goods  ID, In the specific application, the request should be passed in
 String lockKey = "lock:product_01";
 // SETNX  Lock
 Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, "product");
 
 //  If  false  It means that the lock exists , Go straight back to
 if (!result) {
 //  Simulate the return service
 return " The system is busy ";
 }
 int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock").toString());
 if (stock > 0) {
 //  stock  -1
 int realStock = stock - 1;
 //  Deducting the inventory   Simulate more business operations
 redisTemplate.opsForValue().set("stock", realStock + "");
 System.out.println(" Deduction succeeded , Surplus stock :" + realStock);
 } else {
 System.out.println(" Deduction failed , Insufficient inventory ");
 }

 //  The lock needs to be released after locking
 redisTemplate.delete(lockKey);
 
 return "8080";
 }
}

2、 The problem is

①  Business code exception --- Deadlock

In the actual scene , A second kill process involves many business operations , If a business operation throws an exception before releasing the lock , So that the lock is not released , Then there will be
Deadlock
problem . At this point  key  Always exist in  redis  in , Other threads execute  SETNX  Fail forever .

In other words, we should ensure that the release lock is executed , So put the above business code in  
try catch
  perhaps  
try finally
  in :

null
② Redis  Downtime

But in fact, the above code may not completely solve the problem , If  Redis  Down or restarted , It will also lead to  finally  Code execution in failed , The result is the same as above .

So usually we need to give this  key  Set expiration time , namely :

Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, "product");

redisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);

But there is an atomic problem with the above writing , So we can't write separately , You have to synthesize a command :

Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, "product", 10, TimeUnit.SECONDS);
③  Possible problems with high concurrency

Generally speaking , When the amount of concurrency is small , The above wording has met the requirements , But for tens of thousands of concurrent , It may cause the response of the interface to slow down , such as :

A request  A  Performing a complete operation requires  15s , The timeout we set above is  10s , So now request  A  It's not finished , But due to setting the expiration time  key  It's deleted , Then there's another request  B, It can be locked successfully , And in  8s  Internal execution complete , and  B  In the process of execution  A  The same is being done , If at this time  A  May precede  B  To carry out  finally  The code in removes the lock , but  A  The deleted lock is not its , It is  B  Lock added , Empathy , When requested  C  After locking, I was asked  B  It's released , in other words , This distributed lock is directly invalid ( Even though it's very unlikely ), There will also be overbought problems .

The root of this problem is :
The lock you added was released by someone else
.

So we can be sure of  value  Value uniqueness , Such as  UUID, as follows :

null
But this approach also has atomic problems , In the picture above  ②  Code at , The result will also lead to
The lock you put on was released by someone else
.

Redisson

Answer the above question , We can go through  Redisson  To solve , It's very easy to use , and  JDK  Medium  Lock  Use similar , as follows :

@RestController
public class RedissonController {

 @Autowired
 private Redisson redisson;

 @Autowired
 private RedisTemplate redisTemplate;

 @RequestMapping("/deduct_stock1")
 public String deductStock() {

 //  goods  ID, In the specific application, the request should be passed in
 String lockKey = "lock:product_01";

 //  Get the lock
 RLock lock = redisson.getLock(lockKey);
 //  Lock
 lock.lock();

 try {
 int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock").toString());
 if (stock > 0) {
 //  stock  -1
 int realStock = stock - 1;
 //  Deducting the inventory
 redisTemplate.opsForValue().set("stock", realStock + "");
 System.out.println(" Deduction succeeded , Surplus stock :" + realStock);
 } else {
 System.out.println(" Deduction failed , Insufficient inventory ");
 }
 } finally {
 //  Release the lock
 lock.unlock();
 }

 return "8080";
 }
}

It's normal to test again :

null
Redisson  The principle is as follows :

null
Redisson  The locking mechanism of the lock is shown in the figure above , Thread to get lock , If the acquisition is successful, execute the save data to  redis  database . If the fetch fails , Through  while  Loop trying to get lock ( Customizable waiting time , Failed to return after timeout ), After success , Save data to  redis  database .

Redisson  The distributed lock provided supports automatic renewal of locks ( Lock for life ), in other words , If the thread is still not finished , that  Redisson  Will automatically give  redis  Target in  key  Extend the timeout period , This is in  Redisson  Referred to as  Watch Dog( watchdog ) Mechanism .

that  
redisson  How to achieve atomicity

Certainly  
lua
. Whether it's a lock operation , Or the watchdog mechanism is through  
lua
To guarantee its atomicity .

The locking call link is as follows :

RedissonLock.lock()--->lockInterruptibly()--->tryAcquire()--->tryLockInnerAsync()
The key code is  
tryLockInnerAsync()
  in :

<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
 internalLockLeaseTime = unit.toMillis(leaseTime);
 return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
 //  If the lock doesn't exist , Through hset Set its value , And set expiration time
 &quot;if (redis.call('exists', KEYS[1]) == 0) then &quot; +
 &quot;redis.call('hset', KEYS[1], ARGV[2], 1); &quot; +
 &quot;redis.call('pexpire', KEYS[1], ARGV[1]); &quot; +
 &quot;return nil; &quot; +
 &quot;end; &quot; +
 //  If the lock already exists , It is the current thread , Through hincrby Incrementing the value 1, That is, the re-entry of the lock
 &quot;if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then &quot; +
 &quot;redis.call('hincrby', KEYS[1], ARGV[2], 1); &quot; +
 &quot;redis.call('pexpire', KEYS[1], ARGV[1]); &quot; +
 &quot;return nil; &quot; +
 &quot;end; &quot; +
 //  If the lock already exists , Not the current thread , The expiration time is returned  ttl
 &quot;return redis.call('pttl', KEYS[1]);&quot;,
 Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}

Then the automatic renewal of locks is as follows :

RedissonLock.lock()--->lockInterruptibly()--->tryAcquire()--->scheduleExpirationRenewal()
scheduleExpirationRenewal()
  Method will start a child thread to perform the operation of automatic delay , Of course, it is also the implementation of  
lua
Code , as follows , Intercept the key part :

// getName() Is the name of the current lock  
RFuture<Boolean> future = commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
 //  Judge this lock  getName()  Whether in redis in , If there is one, go ahead  pexpire  delay   Default 30s
 &quot;if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then &quot; +
 &quot;redis.call('pexpire', KEYS[1], ARGV[1]); &quot; +
 &quot;return 1; &quot; +
 &quot;end; &quot; +
 &quot;return 0;&quot;,
 Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));

He will judge the lock  
getName()
  Whether in  redis  in , If there is one, go ahead  
pexpire
  delay , Default
lockWatchdogTimeout=30s
, And every interval
lockWatchdogTimeout/3=10s
Time , To perform the delay operation .

Source code :https://gitee.com/javatv/redis.git

Reference resources :
redisson  The watchdog mechanism in
原网站

版权声明
本文为[InfoQ]所创,转载请带上原文链接,感谢
https://yzsam.com/2022/174/202206231100460557.html