Distributed lock application of seckill project

1. Create a Redisson module

Step 1: Create zmall-redisson module based on Spring Initialzr method

Step 2: Add related dependencies in the zmall-redisson module

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!--redis-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!--commons-pool2-->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

<!--redisson-->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.17.0</version>
</dependency>

Step 3: Configure application.yml

server:
  port: 8081
spring:
  redis:
    host: 127.0.0.1
    password: 123456
    database: 1
    port: 6379

2. Simulate high-concurrency scenarios to place orders in seconds

2.1 Scenario Simulation

@RestController
public class RedissonController {
    
    

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @RequestMapping("/updateStock")
    public String updateStock() {
    
    
		int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get("stock");
        if (stock > 0) {
    
    
           	int realStock = stock - 1;
            stringRedisTemplate.opsForValue().set("stock", realStock + "");//jedis.set("stock",realStock+"");
            System.out.println("扣减成功,剩余库存:" + realStock);
        } else {
    
    
            System.out.println("扣减失败,库存不足");
        }
        return "end";
    }
}

2.2 Case demonstration

  • Example 1: Single-threaded case

Directly open the browser and enter: http://localhost:8081/updateStock to check the inventory deduction in redis.
insert image description here
insert image description here

  • Example 2: Multi-threaded situation

Step 1: Configure multi-boot service

insert image description here

Step 2: Configure nginx to achieve load balancing

upstream tomcats{
    
    
	server 127.0.0.1:8081 weight=1;
	server 127.0.0.1:8082 weight=2;
}

server
{
    
    
	listen 80;
	server_name localhost;
	
	location / {
    
    
		proxy_pass http://tomcats/;
	}
}

insert image description here
Step 3: Configure jmeter to implement pressure testing
Create test cases and send 4 groups of threads in a loop, 200 in each group;

The result of checking the inventory in redis is 0; checking the multi-service console information shows that the deduction fails and the inventory is insufficient.

  • Result analysis

1) In the case of a single thread, call the updatestock method to deduct the inventory, and the order is placed normally (nothing to say)
2) In the case of multi-threading, call the updatestock method to deduct the inventory normally, but the order is abnormal (oversold)

Reason analysis: under high concurrency conditions, multiple threads call the updateStock method at the same time. According to the normal thinking, thread 1, thread 2, and thread 3 should respectively realize the stock reduction by one (in the case of 100 stocks, there should be 97 left now), and at the same time Three flash orders are generated; then in the case of concurrency, it will not be executed according to the script design at all, but thread 1, thread 2, and thread 3 will deduct the inventory at the same time, resulting in 99 remaining inventory, but 3 orders are generated, indicating Oversold.

3. JVM-level locks and redis-level distributed locks

3.1 JVM-level locks

@RestController
public class RedissonController {
    
    

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @RequestMapping("/updateStock")
    public String updateStock() {
    
    
    	//jvm级锁,单机锁
        synchronized (this){
    
    
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get("stock");
            if (stock > 0) {
    
    
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", realStock + "");//jedis.set("stock",realStock+"");
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
    
    
                System.out.println("扣减失败,库存不足");
            }
        }
        return "end";
    }
}

insert image description here

JVM-level synchronization lock, stand-alone lock. In the above synchronous code block, only one thread can carry out the flash sale order inventory deduction at the same time in the stand-alone environment, and the subsequent thread can enter after the completion. However, in a distributed environment, there will still be oversold situations.

Please add a picture description

3.2 redis-level distributed lock

3.2.1 What is setnx

Format: setnx key value

Set the value of the key to value if and only if the key does not exist; if the given key does not exist, then setnx does nothing.
setnx is set if not existsshorthand for (set if absent).

setnx "zking" "xiaoliu"      第一次设置有效
setnx "zking" "xiaoliu666"   第二次设置无效

The first time using setnx to set zking directly succeeds, but the second time using setnx to set zking fails, which also means that the lock fails.

Use of setnx for redis-level distributed locks

@RestController
public class RedissonController {
    
    

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @RequestMapping("/updateStock")
    public String updateStock() {
    
    
    	//使用redis级分布式锁setnx加锁
    	String lockKey="lockKey";
    	Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"zking");
    	if(!flag)
    		return "error_code";
        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get("stock");
        if (stock > 0) {
    
    
            int realStock = stock - 1;
            stringRedisTemplate.opsForValue().set("stock", realStock + "");//jedis.set("stock",realStock+"");
            System.out.println("扣减成功,剩余库存:" + realStock);
        } else {
    
    
            System.out.println("扣减失败,库存不足");
        }
        //解锁
        stringRedisTemplate.delete(lockKey);
    }
    return "end";
}

Manometry
insert image description here
insert image description here

3.2.2 Scenario Analysis

Based on the code of the above redis distributed lock setnx, scene analysis is realized.

  • Problem 1: An exception occurred during the execution of the inventory deduction business, resulting in the failure to delete the lock normally, resulting in a deadlock.

Solution : Solve by try/catch/finally code block.

//使用redis级分布式锁setnx加锁
String lockKey="lockKey";
try{
    
    
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"zking");
    if(!flag)
    	return "error_code";
    int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get("stock");
    if (stock > 0) {
    
    
    	int realStock = stock - 1;
    	stringRedisTemplate.opsForValue().set("stock", realStock + "");//jedis.set("stock",realStock+"");
    	System.out.println("扣减成功,剩余库存:" + realStock);
    } else {
    
    
    	System.out.println("扣减失败,库存不足");
    }
}finally{
    
    
	//解锁
	stringRedisTemplate.delete(lockKey);
}

insert image description here
insert image description here

  • Question 2: If the Redis service is down when executing the inventory deduction business, the finally block based on the above question 1 will be meaningless, or deadlock.

Solution : Set the expiration time when locking to ensure atomicity

//使用redis级分布式锁setnx加锁
String lockKey="lockKey";
try{
    
    
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"zking",10,TimeUnit.SECONDS);
    if(!flag)
    	return "error_code";
    int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get("stock");
    if (stock > 0) {
    
    
    	int realStock = stock - 1;
    	stringRedisTemplate.opsForValue().set("stock", realStock + "");//jedis.set("stock",realStock+"");
    	System.out.println("扣减成功,剩余库存:" + realStock);
    } else {
    
    
    	System.out.println("扣减失败,库存不足");
    }
}finally{
    
    
	//解锁
	stringRedisTemplate.delete(lockKey);
}
- 问题3:高并发场景下,线程执行先后顺序无法把控(自己加的锁被其他线程释放掉了,o(╥﹏╥)o)**

**场景分析**

> 线程1:业务执行时间15s,加锁时间10s,那么导致业务未执行完成锁被提前释放;
> 线程2:业务执行时间8s,加锁时间10s;
> 线程3:业务执行时间5s,加锁时间10s,那么导致线程2的任务还没有执行完成就是线程3将所删除掉了;
>
> 以此类推,只要是高并发场景一直存在,那么锁一直处于失效状态(永久失效)

**解决办法**:可以在加锁的时候设置一个线程ID,只有是相同的线程ID才能进行解锁操作。
//使用redis级分布式锁setnx加锁
String lockKey="lockKey";
String clientId= UUID.randomUUID().toString();
try{
    
    
  	
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,clientId,10,TimeUnit.SECONDS);
    if(!flag)
    	return "error_code";
    int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get("stock");
    if (stock > 0) {
    
    
    	int realStock = stock - 1;
    	stringRedisTemplate.opsForValue().set("stock", realStock + "");//jedis.set("stock",realStock+"");
    	System.out.println("扣减成功,剩余库存:" + realStock);
    } else {
    
    
    	System.out.println("扣减失败,库存不足");
    }
}finally{
    
    
	//只有是相同的线程ID时才进行解锁操作
    if(stringRedisTemplate.opsForValue().get(lockKey).equals(clientId)) {
    
    
        //业务代码执行完毕删除redis锁(解锁)
        stringRedisTemplate.delete(lockKey);
    }
}

Question 4. Is it most reasonable and effective to add multiple times to the lock?

Solution : redisson, watchdog mechanism.

4. Redisson distributed lock + source code interpretation

4.1 What is Redisson

​ Redisson - is an advanced distributed coordination Redis customer service terminal, which can help users easily implement some Java objects in a distributed environment. Redisson, Jedis, and Lettuce are three different clients that operate Redis, and the APIs of Jedis and Lettuce It focuses more on CRUD (addition, deletion, modification) of the Redis database, while the Redisson API focuses on distributed development.

Features :

  • Mutual exclusion: Under the condition of distributed high concurrency, we need to ensure that only one thread can acquire the lock at the same time. This is the most basic point.

  • Prevent deadlock: Under the condition of distributed high concurrency, for example, when a thread acquires a lock, it has not had time to release the lock, and it cannot execute the command to release the lock due to system failure or other reasons, causing other threads to fail. A lock is acquired, causing a deadlock. Therefore, it is very necessary to set the effective time of the lock in distributed to ensure that after the system fails, the lock can be actively released within a certain period of time to avoid deadlock.

  • Performance: For shared resources with a large amount of access, it is necessary to consider reducing the lock waiting time to avoid blocking a large number of threads. Therefore, when designing the lock, two points need to be considered.

    • The granularity of the lock should be as small as possible. For example, if you want to reduce inventory through locks, you can set the name of the lock to be the ID of the product instead of choosing any name. In this way, the lock is only valid for the current commodity, and the granularity of the lock is small.
    • The scope of the lock should be as small as possible. For example, if only 2 lines of code can be locked to solve the problem, then don't lock 10 lines of code.
  • Reentrant: ReentrantLock is a reentrant lock, and its characteristic is that the same thread can repeatedly acquire the lock of the same resource. Reentrant locks are very conducive to the efficient use of resources

4.2 Working principle of Redisson

Please add a picture description

4.3 Getting Started Case

Create a RedissonConfig configuration class

@Configuration
public class RedissonConfig {
    
    

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private String port;

    @Value("${spring.redis.password}")
    private String password;

    @Bean
    public RedissonClient redissonClient(){
    
    
        Config config=new Config();
        String url="redis://"+host+":"+port;
        config.useSingleServer().setAddress(url).setPassword(password).setDatabase(1);
        return Redisson.create(config);
    }
}

Use redisson distributed lock to realize spike order

@RequestMapping("/updateStock")
public String updateStock() {
    
    
	String lockKey="lockKey";
	RLock clientLock = redissonClient.getLock(lockKey);
	clientLock.lock();
	try {
    
    
		int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get("stock");
		if (stock > 0) {
    
    
			int realStock = stock - 1;
			stringRedisTemplate.opsForValue().set("stock", realStock + "");//jedis.set("stock",realStock+"");
			System.out.println("扣减成功,剩余库存:" + realStock);
		} else {
    
    
			System.out.println("扣减失败,库存不足");
		}
	} finally {
    
    
		//解锁
		clientLock.unlock();
	}
	return "end";
}

Restart the jmeter pressure test, and send 4 groups of 200 requests in a row. Checking the multi-service console, the result shows that the flash sale order is placed normally, and there is no oversold situation.

Note: Regarding the problem of reading information in redis
Please add a picture description

The renderings are as follows:
insert image description here

5. The seckill project integrates redisson to realize distributed locks

Step 1: Configure pom.xml in zmall-order module

<!--redisson-->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.17.0</version>
</dependency>

Step 2: Create Redisson configuration class

@Configuration
public class RedissonConfig {
    
    

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private String port;

    @Value("${spring.redis.password}")
    private String password;

    @Bean
    public RedissonClient redissonClient(){
    
    
        Config config=new Config();
        String url="redis://"+host+":"+port;
        config.useSingleServer().setAddress(url).setPassword(password).setDatabase(1);
        return Redisson.create(config);
    }
}

Step 3: Integrate projects to implement Redisson distributed locks

@Transactional
@Override
public JsonResponseBody<?> createKillOrder(User user, Integer pid) {
    
    
    //6.根据秒杀商品ID和用户ID判断是否重复抢购
    Order order = redisService.getKillOrderByUidAndPid(user.getId(), pid);
    if(null!=order)
    	return new JsonResponseBody<>(JsonResponseStatus.ORDER_REPART);

    RLock clientLock = redissonClient.getLock("scekill:goods:" + pid);
    clientLock.lock();
    try {
    
    
        //7.Redis库存预减
        long stock = redisService.decrement(pid);
        if (stock < 0) {
    
    
        	redisService.increment(pid);
        	return new JsonResponseBody<>(JsonResponseStatus.STOCK_EMPTY);
        }
        //创建订单
        order = new Order();
        order.setUserId(user.getId());
        order.setLoginName(user.getLoginName());
        order.setPid(pid);

        //将生成的秒杀订单保存到Redis中
        redisService.setKillOrderToRedis(pid, order);
        //将生成的秒杀订单推送到RabbitMQ中的订单队列中
        rabbitTemplate.convertAndSend(RabbitmqOrderConfig.ORDER_EXCHANGE,
        RabbitmqOrderConfig.ORDER_ROUTING_KEY, order);
    }catch (Exception e){
    
    
        e.printStackTrace();
        throw new BusinessException(JsonResponseStatus.ORDER_ERROR);
    }finally {
    
    
    	clientLock.unlock();
    }
    return new JsonResponseBody<>();
}

Restart the jmeter pressure test.

Started services:

nacos
MySQL
redis
RabbitMQ
Nginx
zmall-gateway
zmall-user
zmall-product
zmall-order
zmall-RabbitMQ
zmall-cart

The final result is as follows

After optimization - add Redisson: 500 concurrency: qps is between 140-160
After optimization - without Redisson: 500 concurrency: qps is between 250
Before optimization: 500 concurrency, qps is around 50 (refer to the fifth lesson pressure test)

insert image description here

insert image description here

Note: The actual project is online, and the distributed project is multi-node, so it is necessary to add Redisson, otherwise there will be repeated order spikes between multiple microservice nodes;
insert image description here
insert image description here
insert image description here
insert image description here
carry out pressure measurement

Guess you like

Origin blog.csdn.net/qq_63531917/article/details/129022691