问题描述
项目中很多地方使用Lua脚本,Lua脚本的优势之一是可以保证操作的原子性,就是在执行多个redis操作时,可以保证同时成功或者失败。
Lua脚本在很多场景都可以使用,例如用户登录、秒杀扣减库存,凡是同时执行多个redis操作又需要保证原子性的场景都是可以使用的。那么怎么使用Lua脚本呢?
下面通过保存用户token和token续期两个场景,最直观得来学习lua脚本的应用
技术栈和版本
项目中通过spring-data-redis来调用Lua脚本,maven配置如下
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>2.1.9.RELEASE</version>
</dependency>
TokenDTO
package com.penn.front.sys.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Date;
/**
* @author xiaopeng.zhang
* @since 2020-07-21 10:20
* token
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TokenDTO implements Serializable {
/**
* 用户ID
*/
private Long userId;
/**
* 用户token
*/
private String token;
/**
* 过期时间
*/
private Date expireTime;
}
保存Token
因为这个项目需要区分用户在多平台登录,所有这里有platform参数字段,这段代码是在登录成功后调用,逻辑主要是这样
- DefaultRedisScript 指定执行lua脚本类
- ResourceScriptSource 指定lua文件
- List keys 定义key
- Object[] argv 定义参数
- redisTemplate.execute 执行脚本
/**
* 新增缓存
* @param tokenDTO
*/
public void set(String platform, TokenDTO tokenDTO) {
// 获取当前登录平台 01-app, 02-pc
// 定义lua脚本
DefaultRedisScript redisScript = new DefaultRedisScript<>();
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/createToken.lua")));
// 定义lua脚本key参数
List<String> keys = Lists.newArrayList();
keys.add(RedisKeys.getUserTokenKey(tokenDTO.getToken()));
keys.add(RedisKeys.getUserTokenRelationKey(tokenDTO.getUserId(), platform));
// 定义lua脚本的argv参数
Object[] argv = {
tokenDTO.getUserId(),
tokenDTO.getToken(),
DateUtil.format(tokenDTO.getExpireTime(), "yyyyMMddHHmmss"),
ApiConstant.EXPIRE.intValue()
};
// 执行lua脚本
redisTemplate.execute(redisScript, keys, argv);
}
createToken.lua脚本
我们看脚步里面其实有多个call操作,这就是跟分别执行redis的区别
--
-- 创建token和用户的关系
-- User: penn
-- Date: 2020/7/24
-- Time: 20:40
-- To change this template use File | Settings | File Templates.
--
local token_key = KEYS[1];
local user_token_relation_key = KEYS[2];
local v_userId = ARGV[1];
local v_token = ARGV[2];
local v_expireTime = ARGV[3];
local expire = ARGV[4];
local del_key_prefix = 'sys:security:user:token:'
local token = redis.call('HGET', user_token_relation_key, 'token')
local del_key = ''
if type(token) == 'string'
then
del_key = del_key_prefix .. string.gsub(token,'\"','')
redis.call('del', del_key)
end
redis.call('HMSET', token_key, 'userId', v_userId, 'token', v_token, 'expireTime', v_expireTime);
redis.call('EXPIRE', token_key, expire);
redis.call('HMSET', user_token_relation_key, 'token', v_token);
redis.call('EXPIRE', user_token_relation_key, expire);
附带一个token需要的实现
token续期
/**
* 刷新缓存有效期
* @param userId
* @param token
*/
public void expire(String platform, Long userId, String token, String expireTime) {
// 定义lua脚本
DefaultRedisScript redisScript = new DefaultRedisScript<>();
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/expireToken.lua")));
// 定义lua脚本key参数
List<String> keys = Lists.newArrayList();
keys.add(RedisKeys.getUserKey(userId));
keys.add(RedisKeys.getUserTokenKey(token));
keys.add(RedisKeys.getUserTokenRelationKey(userId, platform));
// 定义lua脚本的argv参数
Object[] argv = {
expireTime,
ApiConstant.EXPIRE.intValue()
};
// 执行lua脚本
redisTemplate.execute(redisScript, keys, argv);
}
expireToken.lua
--
-- 创建token和用户的关系
-- User: penn
-- Date: 2022/7/24
-- Time: 20:40
-- To change this template use File | Settings | File Templates.
--
local user = KEYS[1];
local token_key = KEYS[2];
local user_token_relation_key = KEYS[3];
local v_expireTime = ARGV[1];
local expire = ARGV[2];
redis.call('EXPIRE', user, expire);
redis.call('HMSET', token_key, 'expireTime', v_expireTime);
redis.call('EXPIRE', token_key, expire);
redis.call('EXPIRE', user_token_relation_key, expire);
Lua实现秒杀抢占库存
参考地址