Redis提供的命令虽然不少, 但是严格的说计算能力还是比较有限的. 好在Redis2.6版本后引入Lua脚本, 大大增强了这方面的计算能力. 最重要的是执行lua脚本还具备原子性, 所以在对一致性要求高的环境下, lua脚本或许是个不错的选择.本文通过具体的场景来简介下lua脚本的使用.这里,我们基于Springboot框架的RedisTemplate来操作redis.
问题抽象:
下面代码片段,帮助你理解上述的场景:
@Autowired
private DemoDao demoDao;
@Autowired
private StringRedisTemplate redisTemplate;
// snippet 1;
public long getCounter(String countKey) {
Long count = redisTemplate.opsForValue().get(countKey);
if (null == count || count <= 0L) {
count = demoDao.countByXXAndYY(XX, YY);
if (count > 0) {
redisTemplate.opsForValue().set(countKey, String.valueOf(count), 15, TimeUnit.MINUTES);
}
}
return count;
}
// snippet 2;
public void update() {
...
demoDao.updateByXXAndYY(XX, YY);
redisTemplate.opsForValue().increment(countKey, 1L);
...
}
上述代码有问题吗?问题在哪儿? 不妨看下面的操作:
127.0.0.1:6379> get incr_key
(nil)
127.0.0.1:6379> incr incr_key
(integer) 1
127.0.0.1:6379> get incr_key
"1"
如果在第15分钟, 缓存失效了, key被删除, 但这是当产品更新时,又执行了increment操作;由于redis在执行"incr"命令时, 如果key不存在,那就创建并自增;那么在接下来的15分钟内,你会发现缓存中值,不可信了。
寻求解决方法
要解决上面的问题,一种可行的方法是,在在incr操作时,判断下key是否存在,仅在存在的情况下,才执行increment; 但问题是 exist和incr是两个指令,既不能保证他们都执行成功,更不能保证他们的原子性。
这时lua脚本就可以展现它的优越性了。判断+更新的lua脚本如下:
static {
incrScript = "if (redis.call('exists', KEYS[1])) == 1 then \n"
+ "return redis.call('incr', KEYS[1]) \n"
+ "else return 0 end";
}
需要注意的是,Lua中只有nil和false才是假,其余值,包括空字符串和0,都被认为是真值,所有if语句的判断不能简单的写为"if redis.call(‘exists’, KEYS[1]) then …"
那么redisTemplate如何执行它了?
private Long incrementOnExist(String key) {
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(incrScript);
redisScript.setResultType(Long.class);
Long result = redisTemplate.execute(redisScript, Collections.singletonList(key), null);
log.info("increment key:{} result:{}", key, result);
return result;
}
当然,上面是默认使用Lettuce驱动的connection.如果不是使用上述驱动,需要拿到redis所使用的链接,然后执行。
Long result = redisTemplate.execute(new RedisCallback() {
@Override
public Long doInRedis(RedisConnection redisConnection) throws DataAccessException {
Object connection = redisConnection.getNativeConnection();
//集群模式
if (connection instanceof JedisCluster) {
return (Long) ((JedisCluster) connection).eval(incrScript, Collections.singletonList(key), Collections.EMPTY_LIST);
}
//单机模式
else if (connection instanceof Jedis) {
return (Long) ((Jedis) connection).eval(incrScript, Collections.singletonList(key), Collections.EMPTY_LIST);
}
return null;
}
});