SpringBoot integrates Redis (as Cache cache) + Lua

SpringBoot integrates Redis

Developers only need to introduce the Spring Data Redis dependency, and then simply configure the basic information of redis, and the system will provide a RedisTemplate for developers to use.

Combined with the usage of Cache, the exciting Cache has been introduced in Spring 3.1. In Spring Boot, it is very convenient to use Redis as the Cache implementation to achieve data caching.

1. Project creation (web, cache and redis)

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

Configuration file (configured the port and address of Redis, and then gave the cache a name)

spring.redis.port=6380
spring.redis.host=192.168.66.128

spring.cache.cache-names=c1

2. Turn on caching

@SpringBootApplication
@EnableCaching
public class RediscacheApplication {<!-- -->
    public static void main(String[] args) {<!-- -->
        SpringApplication.run(RediscacheApplication.class, args);
    }
}

Spring Boot will automatically help us configure a RedisCacheManager in the background. The relevant configuration is completed in the org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration class.

@Configuration
@ConditionalOnClass(RedisConnectionFactory.class)
@AutoConfigureAfter(RedisAutoConfiguration.class)
@ConditionalOnBean(RedisConnectionFactory.class)
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)
class RedisCacheConfiguration {<!-- -->
\t
//The system will automatically provide a Bean of RedisCacheManager, which implements the Cache interface in Spring
//Use the cache annotations and interfaces in Spring directly, and the cache data will be automatically stored in Redis.
//In stand-alone Redis, this Bean system will automatically provide it. If it is a Redis cluster, this Bean needs to be provided by the developer.
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory,
ResourceLoader resourceLoader) {<!-- -->
RedisCacheManagerBuilder builder = RedisCacheManager
.builder(redisConnectionFactory)
.cacheDefaults(determineConfiguration(resourceLoader.getClassLoader()));
List<String> cacheNames = this.cacheProperties.getCacheNames();
if (!cacheNames.isEmpty()) {<!-- -->
builder.initialCacheNames(new LinkedHashSet<>(cacheNames));
}
returnthis.customizerInvoker.customize(builder.build());
}
}

3. Annotation using cache

@CacheConfig
Used on a class to describe the cache name used by all methods in the class. Of course, you can also configure the name directly on the specific cache annotation without using this annotation. The sample code is as follows:

@Service
@CacheConfig(cacheNames="c1")
public class UserService{<!-- -->

}

@Cacheable
This annotation is generally added to the query method, indicating that the return value of a method is cached. By default, the cached key is the parameter of the method, and the cached value is the return value of the method.

@Cacheable(key="#id") //Only use id as the cache key. If you have complex requirements for the key, you can customize the keyGenerator
public User getUserById(Integer id,String username){<!-- -->
\t
return getUserFromDBById(id);
}

When there are multiple parameters, multiple parameters are used as key by default.
If you only need one of the parameters to be the key, you can specify the key through the key attribute in the @Cacheable annotation.

Spring Cache provides the root object, which can achieve some complex effects without defining keyGenerator:

@CachePut
This annotation is usually added to the update method. When the data in the database is updated, the data in the cache will also be updated.
Using this annotation, the return value of the method can be automatically updated to the existing key. The sample code is as follows:

@CachePut(key = "#user.id")
public User updateUserById(User user) {<!-- -->
    return user;
}

@CacheEvict
This annotation is usually added to the deletion method. When the data in the database is deleted, the related cache data will also be automatically cleared.
When used, this annotation can also be configured to be deleted according to certain conditions (condition attribute) or configured to clear all caches (allEntries attribute). The sample code is as follows:

@CacheEvict()
public void deleteUserById(Integer id) {<!-- -->
    //Perform the deletion operation here. Deletion is to delete from the database.
}

Lua script

  • Lua scripts are executed in Redis, avoiding multiple communications between the client and the server. This can reduce network overhead and improve performance, especially when multiple Redis commands need to be executed to complete an operation. Atomicity: Redis guarantees atomic execution of Lua scripts without worrying about race conditions or concurrency issues.
  • Lua scripts can be used with Redis transactions to ensure atomic execution of a series of commands. This allows you to treat multiple operations as a single transaction, and either all succeed or all fail.
  • Lua scripts provide a way to perform complex operations in Redis, allowing you to combine multiple Redis commands in a single script. This is useful for handling complex business logic, such as calculating and updating distributed counters, implementing custom data structures, etc.
  • Using Lua scripts, you can implement complex atomic locks instead of just using Redis’s SETNX (set if not exists) command.
  • For large batches of data processing, Lua scripts can reduce the number of round trips between the client and the server, thereby significantly reducing network overhead.
  • By moving complex calculations to the server side, the burden on the client can be relieved and the load on the server can be reduced.
  • Redis natively supports Lua scripts, so no additional plugins or extensions are required.
  • Lua script is a common scripting language that is easy to write and maintain.
-- local variables
local age=30

-- global variables
name = "john"

--[[
Data type: integer, floating point number, string, Boolean value, nil
]]
localnum=42
local str = "Hello, Lua!"
local flag = true
local empty = nil
local person = {<!-- --> name = "John", age = 30 }

--[[
\tConditional statements
\tloop statement
]]
if age < 18 then
    print("underage")
elseif age >= 18 and age < 65 then
    print("adult")
else
    print("senior")
end

for i = 1, 5 do
    print(i)
end

local count = 0
while count < 3 do
    print("Number of loops: " .. count)
    count = count + 1
end

repeat
    print("Execute at least once")
until count > 5
--Table data structure, defined with {}, contains key-value pairs, which can be of any data type
local person = {<!-- -->name = "jordan",age = 23,hobbies = {<!-- -->"basketball","baseball"}}
print("Name:"..person.name)
print("Age:"..person.age)

-- Modular, loaded through require keyword

-- Standard library, file operation, network programming, regular expressions, event processing, etc., through built-in modules such as io/socket, etc.
-- String processing
local text = "Lua programming"
local sub = string.sub(text,1,3)
print(sub)

-- Error handling, use pcall Hanshu to wrap code blocks that may cause exceptions to catch and handle errors, usually used with assert
local success,result = pcall(function()
error("An error occurred!")
end)

if success then
print("Execution successful")
else
print("Error message:"..result)
end

Error return value: Lua scripts may encounter errors during execution, such as syntax errors in the script itself, or certain operations in the script failing. After Redis executes the Lua script, it will return the execution result of the script. You can check this result to see if there are any errors, usually the return value is a specific error identifier. For example, if the script executes successfully, the return value is usually OK, otherwise there will be a corresponding error message.
Exception handling: In a Spring Boot application, you can use exception handling to capture exceptions that may be thrown when Redis executes scripts. Spring Data Redis provides some exception classes, such as RedisScriptExecutionException, for handling errors during script execution. You can use a try-catch block to catch these exceptions and take appropriate action, such as logging an error message or performing an alternate action.

Application scenarios

1. Cache update

Store some data in the cache but need to update it periodically or based on conditions while ensuring that concurrency issues do not occur during updates
Using Lua scripts, you can atomically check the freshness of data, and if updates are needed, recalculate the data and update the cache in one atomic operation

local cacheKey = KEYS[1] --Get the cache key
local data = redis.call('GET',cacheKey) -- Try to get data from cache

if not data then
--The data is not in the cache, recalculate and set
data = calculateData()
redis.call('SET',cacheKey,data)
end
return data

2. Atomic operations

Multiple Redis commands need to be executed as an atomic operation to ensure that they are not interrupted in a multi-threaded or multi-process environment
Using Lua scripts, you can combine multiple commands into an atomic operation, such as implementing distributed locks, counters, rankings, etc.

local key = KEYS[1] -- Get the key name
local value = ARGV[1] -- Get parameter value
local current = redis.call('GET',key) -- Get the current value
if not current or tonumber(current) < tonumber(value) then
-- If the current value does not exist or the new value is greater, set the new value
redis.call('SET',key,value)
end
-- Consider a counter scenario where multiple clients need to atomically increment the count
local key = KEYS[1]
local increment = ARGV[1]
return redis.call('INCRBY',key,increment)

3. Data processing

Complex processing of data in Redis is required, such as statistics, filtering, aggregation, etc.
Using Lua scripts, you can perform complex data processing in Redis without having to transfer the data to the client for processing, reducing network overhead.

local keyPattern = ARGV[1] -- Get the key name matching pattern
local keys = redis.call('KEYS',keyPattern) -- Get matching keys
local result = {<!-- -->}
for i,key in ipairs(keys) do
local data = redis.call('GET',key) -- Get the data corresponding to each key
table.insert(result,processData(data))
end
return result
--[[
Lua scripts allow you to perform complex data processing on the Redis server side. This reduces the overhead of transferring data to the client for processing and allows you to perform more complex logic in Redis, thus improving performance
]]
local total = 0
for _,key in ipairs(KEYS) do
local value = redis.call('GET',key)
total = total + tonumber(value)
end
return total

4. Distributed lock

Using Lua scripts, you can perform complex data processing in Redis without having to transfer the data to the client for processing, reducing network overhead.
Using Lua scripts, you can atomically try to acquire the lock, avoiding race conditions, and then release the lock when you’re done

local lockKey = KEYS[1] -- Get the key name of the lock
local lockValue = ARGV[1] -- Get the lock value
local lockTimeout = ARGV[2] --timeout for acquiring lock
if redis.call('SET',lockKey,lockValue,'NX','PX',lockTimeout) then
-- The lock is acquired successfully and relevant operations are performed.
redis.call('DEL',lockKey)
return true
else
return false --Unable to acquire lock

5. Affairs

local key1 = KEYS[1]
local key2 = KEYS[2]
local value = ARGV[1]

redis.call('SET', key1, value)
redis.call('INCRBY', key2, value)

-- If any step here fails, the entire transaction will be rolled back

SpringBoot implements Lua script

Use of Spring Data Redis and Lettuce (or Jedis) clients

1. Add dependencies

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>io.lettuce.core</groupId>
    <artifactId>lettuce-core</artifactId> <!-- Or use Jedis -->
</dependency>

2. Configure Redis connection in application.properties or application.yml

spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=yourPassword

3. Create Lua script

myscript.lua

local a = tonumber(ARGV[1])
local b = tonumber(ARGV[2])
return a + b

4. Write code to execute Lua script

Use the StringRedisTemplate or LettuceConnectionFactory provided by Spring Data Redis

Two different examples to execute Lua scripts, one is to run the Lua script string directly, the other is to run the script file.

/**
Run Lua script string
*/
@Service
public class LuaScriptService{<!-- -->
\t
@Autowired
private StringRedisTemplate stringRedisTemplate;

public Integer executeLuaScriptFromString(){<!-- -->
\t\t
String luaScript = "local a = tonumber(ARGV[1])\\
local b = tonumber(ARGV[2])\\
return a + b";
\t\t
RedisScript<Integer> script = new DefaultRedisScript<>(luaScript,Integer.class);

String[] keys = new String[0];//Normally, there is no KEYS part
Object[] args = new Object[]{<!-- -->10,20};//Parameters passed to Lua script

Integer result = stringRedisTemplate.execute(script,keys,args);
return result;
}
}

Create a file myscript.lua and run the file through the class

@Service
public class LuaScriptService{<!-- -->
\t
@Autowired
private StringRedisTemplate.stringRedisTemplate;

@Autowired
private ResourceLoader resourceLoader;

public Integer executeLuaScriptFromFile(){<!-- -->
\t
Resource resource = resourceLoader.getResource("classpath:mysript.lua");
String luaScript;
try{<!-- -->
luaScript = new String(resource.getInputStream().readAllBytes());
}catch(Exception e){<!-- -->
throw new RuntimeException("Unable to read Lua script file.");
}

RedisScript<Integer> script = new DefaultRedisScript<>(luaScript, Integer.class);
        String[] keys = new String[0]; // Normally, there is no KEYS part
        Object[] args = new Object[]{<!-- -->10, 20}; // Parameters passed to Lua script
        Integer result = stringRedisTemplate.execute(script, keys, args);
        return result;
}
}