Redis从2.6版本开始引入对Lua脚本的支持,通过在Redis服务器中嵌入Lua环境,Redis客户端可以使用Lua脚本,直接在服务端原子的执行多个Redis命令。
Lua
Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。
为什么用
刚需
- 原子操作:Redis 使用单个 Lua 解释器去运行所有脚本,并且,Redis 也保证脚本会以原子性(atomic)的方式执行:当某个脚本正在运行的时候,不会有其他脚本或 Redis 命令被执行。
优势
- 高效且灵活:Redis的事务实现是基于观察者模式的check-and-set(乐观锁)。相对而言,引入Lua脚本的方式,可以轻松实现在事务方式中难以(无法)实现的业务。
- 减少网络开销:合并多次执行命令的网络请求,用一个请求完成,减少了网络资源开销和响应时间。
- 复用性:客户端发送的脚本会永久存储在Redis中,这意味着其他客户端可以复用这一脚本而不需要使用代码完成同样的逻辑。此特性基于 Script Load 命令。
应用
比如有这样一个需求:每当有新的用户注册到我的平台,为其分配一个客户经理,要求每分配30次,切换一个客户经理,我们只有两个客户经理可用。这个注册服务部署了N个实例。你可以考虑一下如何使用Redis的事务实现。
JAVA实现
/** 通过计数器获取客户经理姓名的lua脚本 */
/** 客户经理资源被抽象成node(环形/单向) */
static final String ASSIGN_ACCOUNT_MANAGER_LUA_SCRIPT = "local node1 = {next = nil, value = '王尼玛经理'}\n" +
"local node2 = {next = node1, value = '王速卡经理'}\n" +
"node1.next = node2\n" +
"local currentNode = node1\n" +
"\n" +
"local currentAccountManagerkey = KEYS[1]\n" +
"local currentAccountManager = redis.call('get', currentAccountManagerkey)\n" +
"\n" +
"if currentAccountManager then\n" +
" if currentNode.value ~= currentAccountManager then\n" +
" currentNode = currentNode.next\n" +
" end\n" +
"else\n" +
" currentAccountManager = node1.value\n" +
"end\n" +
"\n" +
"local counterKey = KEYS[2]\n" +
"local counter = redis.call('incr', counterKey)\n" +
"\n" +
"if(tonumber(counter) == 30) then\n" +
" redis.call('set', counterKey, 0)\n" +
" redis.call('set', currentAccountManagerkey, currentNode.next.value)\n" +
"end\n" +
"\n" +
"return currentAccountManager";
/** 客户经理姓名key*/
static final String CURRENT_ACCOUNT_MANAGER_KEY = "assignAccountManager:account_manager";
/** 客户经理分配计数器key*/
static final String COUNTER_KEY = "assignAccountManager:assign_counter";
jedis方式
@Component
public class JedisUtil {
private JedisUtil() {
}
@Autowired
private JedisPool jedisPool;
/**
* 注册脚本。将脚本 script 添加到脚本缓存中,但并不立即执行这个脚本
* 如果给定的脚本已经在缓存里面了,那么不执行任何操作。
* 在脚本被加入到缓存之后,通过 EVALSHA 命令,可以使用脚本的 SHA1 校验和来调用这个脚本。
* 脚本可以在缓存中保留无限长的时间,直到执行 SCRIPT FLUSH 为止。
* @param script 脚本字符串
* @return 脚本 sha id
*/
public String scriptLoad(String script) {
Assert.hasLength(script, "parameter [script] must not be null");
Jedis jedis = null;
String luaLoad = null;
try {
jedis = jedisPool.getResource();
luaLoad = jedis.scriptLoad(script);
return luaLoad;
} catch (Exception e) {
} finally {
if(jedis != null){
jedis.close();
}
}
return null;
}
/**
* 根据给定的 sha1 校验码,执行缓存在服务器中的脚本
* @param scriptShaId 脚本 sha id
* @param keys key列表
* @param args arg列表
* @return 脚本执行结果
*/
public Object evalsha(String scriptShaId, List<String> keys, List<String> args) {
Assert.hasLength(scriptShaId, "parameter [scriptShaId] must not be null");
Assert.notNull(keys, "parameter [keys] must not be null");
Assert.notNull(args, "parameter [args] must not be null");
Jedis jedis = null;
Object val = null;
try {
jedis = jedisPool.getResource();
if (jedis.scriptExists(scriptShaId)) {
val = jedis.evalsha(scriptShaId, keys, args);
return val;
}
} catch (Exception e) {
} finally {
if(jedis != null){
jedis.close();
}
}
return null;
}
}
/**
* 为新注册的用户分配客户经理
* @return 客户经理姓名
*/
public String assignAccountManagerForNewAccount() {
List<String> keys = Arrays.asList(CURRENT_ACCOUNT_MANAGER_KEY, COUNTER_KEY);
return (String) jedis.evalsha(jedis.scriptLoad(ASSIGN_ACCOUNT_MANAGER_LUA_SCRIPT), keys, Collections.<String>emptyList());
}
RedisTemplate方式
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 为新注册的用户分配客户经理
* @return 客户经理姓名
*/
public String assignAccountManagerForNewAccount() {
List<String> keys = Arrays.asList(CURRENT_ACCOUNT_MANAGER_KEY, COUNTER_KEY);
// 指定 lua 脚本,并且指定返回值类型
DefaultRedisScript<String> redisScript = new DefaultRedisScript<>(ASSIGN_ACCOUNT_MANAGER_LUA_SCRIPT, String.class);
// 参数一:redisScript,参数二:key列表
return redisTemplate.execute(redisScript, keys);
}
相对于文字描述,代码和适量的注释可以直抒胸臆