Redis uses Lua scripts and Redisson to ensure atomicity and consistency in inventory deduction


insert image description here

foreword

Background: A classmate who had a technical exchange in the community recently said that he was asked about the deduction of commodity inventory during the interview. I roughly organized the content to make it easier for everyone to understand.In fact, it is nothing more than the atomicity of distributed locks and Redis commands

In a distributed system, ensuring the atomicity and consistency of data is a key issue. Especially in scenarios such as inventory deduction, it is crucial to ensure the atomicity of operations to avoid problems of data inconsistency and concurrency conflicts. To solve this challenge, we can leverage the power of the Redis database to achieve the atomicity and consistency of inventory deductions.

This blog will introduce two key technologies: Redis Lua script and Redisson, and their application in inventory deduction scenarios. Lua script is a scripting language embedded in the Redis server, which has the characteristics of atomic execution and high performance. Redisson is a distributed Java object and service framework based on Redis, which provides rich functions and advantages.

所以无论是对于中小型企业还是大型互联网公司,保证库存扣减的原子性和一致性都是至关重要的。本博客将帮助读者全面了解如何利用 Redis Lua脚本和 Redisson 来实现这一目标,为他们的分布式系统提供可靠的解决方案。让我们一起深入研究这些强大的工具,提升我们的分布式系统的性能和可靠性。

1. Use SpringBoot + Redis native implementation

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

@Component
public class StockService {
    
    

    @Resource
    private RedisTemplate<String, Object> redisTemplate;
	// 扣减商品库存
    public void decreaseStock(String productId, int quantity) {
    
    
        String lockKey = "lock:" + productId;
        String stockKey = "stock:" + productId;

        ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();

        Boolean acquiredLock = valueOperations.setIfAbsent(lockKey, "locked");

        try {
    
    
            if (acquiredLock != null && acquiredLock) {
    
    
                // 获取锁成功,设置锁的过期时间,防止死锁
                redisTemplate.expire(lockKey, 5, TimeUnit.SECONDS);

                Integer currentStock = (Integer) valueOperations.get(stockKey);
                if (currentStock != null && currentStock >= quantity) {
    
    
                    int newStock = currentStock - quantity;
                    valueOperations.set(stockKey, newStock);
                    System.out.println("库存扣减成功");
                } else {
    
    
                    System.out.println("库存不足,无法扣减");
                }
            } else {
    
    
                System.out.println("获取锁失败,其他线程正在操作");
            }
        } finally {
    
    
            // 释放锁
            if (acquiredLock != null && acquiredLock) {
    
    
                redisTemplate.delete(lockKey);
            }
        }
    }
}  

Let's think about it, there are several problems with the above writing method, this kind of problem

  1. Lock release problem: In the current code, the release of the lock is to determine whether to release the lock by judging whether the lock is acquired successfully or not. However, if redisTemplate.expireafter the expiration time of the set lock is executed, an exception occurs in the code and the release part of the lock is not executed, the lock will not be released in time, and other threads may not be able to acquire the lock. In order to solve this problem, consider using Lua scripts to achieve atomic acquisition of locks and setting expiration time.

  2. Lock re-entry problem: In the current code, the lock re-entry is not handled. If the same thread calls decreaseStockthe method multiple times, the acquisition of the lock will fail because the lock is already occupied by the current thread. In order to solve this problem, you can consider using ThreadLocal or maintaining a counter to record the number of lock reentries, so that you can handle it correctly when releasing the lock.

Solution
For the optimization of the above code, the following points can be considered:

  1. Use setIfAbsentthe method to set the lock, and combine the expiration time of the lock with setting the lock into an atomic operation to avoid the time overhead of operating Redis again after acquiring the lock. can be opsForValue().setIfAbsent(lockKey, "locked", 5, TimeUnit.SECONDS)achieved using . This ensures that acquiring the lock and setting the expiration time is an atomic operation, avoiding the time interval between two Redis operations.

  2. Use luascripts to release locks to ensure the atomicity of releasing locks. By using executethe method to execute luathe script, the release of the lock can be combined into a single atomic operation. Here is sample code:

String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class);
Long releasedLock = redisTemplate.execute(redisScript, Collections.singletonList(lockKey), "locked");
if (releasedLock != null && releasedLock == 1) {
    
    
    // 锁释放成功
}
  1. Using Redissonsuch reliable distributed lock frameworks, they provide richer functions and reliability, and have solved many problems related to distributed locks. These frameworks can simplify code and provide more powerful lock management functions, such as reentrant locks, fair locks, red locks, etc. You can introduce such frameworks into your project Redissonand use the distributed lock functions they provide.

2. Implemented using redisson

There may be some students who don't know much about Redisson. Let me explain some of his excellence.
Redisson is a distributed Java object and service framework based on Redis, which provides rich functions and advantages, making it more convenient and reliable to use Redis in a distributed environment.It can be said that Redisson is currently the most powerful and powerful distributed lock tool based on Redis. It is not one of them, so you can use it boldly in the project. If there is a problem, let’s talk about it. Don’t be too fettered

Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上

  1. Distributed locks: Provides the implementation of various distributed locks such as reentrant locks, fair locks, interlocks, and red locks, which can be used to solve concurrency control problems. It supports automatic renewal and asynchronous release of locks, which can prevent problems caused by lock expiration, and provides more advanced functions such as waiting for locks, timeout locks, etc.

  2. Distributed collection: Provides a series of distributed collection implementations, such as distributed lists, collections, ordered collections, queues, blocking queues, etc. These distributed collections can safely share and manipulate data among multiple nodes, providing an efficient data storage and access mechanism.

  3. Distributed objects: Redisson supports manipulating Java objects in a distributed environment. It provides functions such as distributed mapping, distributed atomic variables, and distributed counters, which can easily store, operate, and synchronize distributed objects.

  4. Optimized Redis commands: Redisson provides more efficient data access by optimizing the calling method of Redis commands. It uses a thread pool and asynchronous operations, and can execute multiple Redis commands in one network round trip
    , reducing network latency and the number of connections, and improving performance and throughput.

  5. Scalability and high availability: Redisson supports Redis cluster and sentinel mode, which can easily meet the needs of large-scale and high availability. It provides automatic failover and master-slave switching mechanism to ensure system availability and data consistency when a node fails.
    A distributed tool based on Redis, with basic distributed objects and advanced and abstract distributed services, brings solutions to most distributed problems for every programmer who tries to reinvent the distributed wheel.

Blow up so many concepts, please show me code. ok Next, we use the atomicity and consistency of inventory implemented by the Redisson library.

  1. Add dependencies: pom.xmlAdd the following dependencies to the file to use the Redisson library to implement distributed locks.
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.16.1</version>
</dependency>
  1. Configure Redisson: Add the Redisson configuration in the Spring Boot configuration file, for example application.properties.
# Redisson配置
spring.redisson.config=classpath:redisson.yaml

resourcesCreate a file in the directory , redisson.yamland configure Redis connection information and related configurations of distributed locks. Here is an example configuration:

singleServerConfig:
  address: "redis://localhost:6379"
  password: null
  database: 0
  connectionPoolSize: 64
  connectionMinimumIdleSize: 10
  subscriptionConnectionPoolSize: 50
  dnsMonitoringInterval: 5000
  lockWatchdogTimeout: 10000
  1. Create a StockServiceservice class called and modify decreaseStockthe methods to use distributed locks:
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;

import java.util.concurrent.TimeUnit;

@Service
public class StockService {
    
    
    private static final String STOCK_KEY = "stock:product123";
    private static final String LOCK_KEY = "lock:product123";

    @Autowired
    private ReactiveRedisTemplate<String, String> redisTemplate;

    @Autowired
    private RedissonClient redissonClient;

    public Mono<Boolean> decreaseStock(int quantity) {
    
    
        RLock lock = redissonClient.getLock(LOCK_KEY);
        return Mono.fromCallable(() -> {
    
    
          //==核心代码start==
            try {
    
    
                boolean acquired = lock.tryLock(1, 10, TimeUnit.SECONDS);
                if (acquired) {
    
    
                    Long stock = redisTemplate.opsForValue().get(STOCK_KEY).block();
                    if (stock != null && stock >= quantity) {
    
    
                        redisTemplate.opsForValue().decrement(STOCK_KEY, quantity).block();
                        return true;
                    }
                }
                return false;
            } finally {
    
    
                lock.unlock();
            }
           //==核心代码结束==
        });
    }

    public Mono<Long> getStock() {
    
    
        return redisTemplate.opsForValue().get(STOCK_KEY)
                .map(stock -> stock != null ? Long.parseLong(stock) : 0L);
    }
}

In StockService, we first redissonClient.getLockobtain a distributed lock object through the method RLock, and use tryLockthe method to try to acquire the lock. If the lock is successfully acquired, the inventory deduction operation is performed. After the operation is complete, the lock is released. By using distributed locks, we ensure that only one thread can perform inventory deduction operations in concurrent scenarios, thus ensuring atomicity and consistency.

3. Use Redis+Lua script to achieve

Use Lua script to realize the atomic operation of inventory deduction. Use Spring Data Redis RedisTemplateto interact with Redis, and DefaultRedisScriptdefine Lua scripts. By ScriptExecutorexecuting the Lua script, the logic of inventory deduction is implemented in the script.

decreaseStockIn the method, we define the Lua script, then create an DefaultRedisScriptobject, and specify the type of the script return value as Boolean. Next, we scriptExecutor.executeexecute the Lua script through the method and pass the script, the key (STOCK_KEY) and the parameter (quantity) as parameters.

getStockThe method is used Mono.fromSupplierto get the current inventory quantity, which has nothing to do with Lua scripts.

3.1 lua script

code logic

  1. By redis.call('GET', KEYS[1])obtaining the inventory quantity corresponding to the key (KEYS[1]) from Redis, and using tonumberit to convert it to a numeric type.
  2. Check whether the inventory is sufficient for deduction, that is, judge stockwhether it exists and is greater than or equal to the incoming deduction quantity (ARGV[1]).
  3. If stock is sufficient, use redis.call('DECRBY', KEYS[1], ARGV[1])deduction stock.
  4. If it returns true, it means the deduction is successful, otherwise it returns, falseit means the deduction failed.
-- 从Redis中获取当前库存
local stock = tonumber(redis.call('GET', KEYS[1]))

-- 检查库存是否足够扣减
if stock and stock >= tonumber(ARGV[1]) then
    -- 扣减库存
    redis.call('DECRBY', KEYS[1], ARGV[1])
    return true -- 返回扣减成功
else
    return false -- 返回扣减失败
end

3.2 Integration with SpringBoot

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.core.script.ScriptExecutor;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;

import java.util.Collections;

@Service
public class StockService {
    
    
    private static final String STOCK_KEY = "stock:product123";

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Autowired
    private ScriptExecutor<String> scriptExecutor;

    public Mono<Boolean> decreaseStock(int quantity) {
    
    
        String script = "local stock = tonumber(redis.call('GET', KEYS[1]))\n" +
                "if stock and stock >= tonumber(ARGV[1]) then\n" +
                "    redis.call('DECRBY', KEYS[1], ARGV[1])\n" +
                "    return true\n" +
                "else\n" +
                "    return false\n" +
                "end";

        RedisScript<Boolean> redisScript = new DefaultRedisScript<>(script, Boolean.class);
        return scriptExecutor.execute(redisScript, Collections.singletonList(STOCK_KEY), String.valueOf(quantity));
    }

    public Mono<Long> getStock() {
    
    
        return Mono.fromSupplier(() -> {
    
    
            String stock = redisTemplate.opsForValue().get(STOCK_KEY);
            return stock != null ? Long.parseLong(stock) : 0L;
        });
    }
}

4. Comparison of Lua scripting method and Redisson method

When using Lua scripts to perform inventory deduction operations, there is usually no need to explicitly lock. This is because the mechanism of Redis executing Lua scripts ensures the atomicity of scripts.

When Redis executes a Lua script, it executes the entire script as a single command. During execution, the execution of the script will not be interrupted, nor will it be interrupted by requests from other clients. This makes Lua scripts atomic during execution, ensuring consistency of operations even under high concurrency.

Therefore, in the above Lua script, we did not explicitly lock to protect the inventory deduction operation. By using Lua scripts, we take full advantage of Redis's atomic operation feature, avoiding the overhead and complexity of explicit locking.

It should be noted that if there are other concurrent operations that also need to deduct or modify the inventory, it may be necessary to consider the locking mechanism to ensure the atomicity of the operation. In this case, distributed locks can be used to control access to the inventory to ensure the correctness of concurrent operations.

There are some differences between the way of using Redisson and the way of using Lua script to implement inventory deduction. We make a table for clear comparison and easy understanding and memory. In fact, these two methods are relatively common in the real practice of the project. But which one to use depends on your company's technology accumulation and usage preferences.
So let's summarize. Choosing an appropriate method depends on specific needs and scenarios. If you need more flexible control, more distributed functions, or higher performance requirements, then using the Redisson library may be a good choice. And if you want to simplify the implementation and reduce dependencies, and the performance requirements are not very high, then it may be more appropriate to use Lua scripts.

Way implementation complexity flexibility performance overhead Distributed environment function
Redisson Need additional dependencies and configurations, write related codes Provide more functional options, such as timeout setting, automatic renewal, etc. Performance overhead that may involve network communication and distributed lock management Provide richer distributed functions
Lua script No additional dependencies, just write Lua scripts Relatively simple, focusing on inventory deduction logic Typically has lower latency and higher performance Focus on inventory deduction operations without the support of other distributed functions

5. Source address

https://github.com/wangshuai67/Redis-Tutorial-2023

6. Redis from entry to proficiency series of articles

7. Reference documents

redisson reference documentation https://redisson.org/documentation.html

Guess you like

Origin blog.csdn.net/wangshuai6707/article/details/132271722