Redis之Lua脚本:原子性的执行exsits&incr

Redis提供的命令虽然不少, 但是严格的说计算能力还是比较有限的. 好在Redis2.6版本后引入Lua脚本, 大大增强了这方面的计算能力. 最重要的是执行lua脚本还具备原子性, 所以在对一致性要求高的环境下, lua脚本或许是个不错的选择.本文通过具体的场景来简介下lua脚本的使用.这里,我们基于Springboot框架的RedisTemplate来操作redis.

问题抽象:

  • 使用redis计数, 计数器的key会在生命周期是15分钟;
  • 每次优先查询redis中的计数器,当计数值不存在时,从数据库中计算,并重新设置redis值;
  • 每次产品有更新时, 将redis的计数器自增1个步长;

下面代码片段,帮助你理解上述的场景:

@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;
	}
});

你可能感兴趣的:(JAVA,分布式,redis,java,redis,缓存)