java - redis - incr and expire lua
使用原子自增和 expire 搭配实现原子操作
@Bean(name = "customStringRedisTemplate") public RedisTemplate<String, String> customStringRedisTemplate(RedisConnectionFactory factory) { // 要求使用默认序列化, 否则有可能会抛出 user_script:1: ERR value is not an integer or out of range 异常 return new StringRedisTemplate(factory); }@Autowired </span><span style="color: rgba(0, 0, 255, 1)">private</span> RedisTemplate<String, String><span style="color: rgba(0, 0, 0, 1)"> customStringRedisTemplate; @Test </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> customLock() { String key </span>= "cmq_im_dispatch_user_"<span style="color: rgba(0, 0, 0, 1)">;
// customStringRedisTemplate.delete(key);
String incrByAndExpireLua = "if redis.call('incrBy', KEYS[1], ARGV[1]) == tonumber(ARGV[1]) then\n" +
"redis.call('expire', KEYS[1], ARGV[2])\n" +
"return tonumber(ARGV[1])\n" +
"else\n" +
"return 0\n" +
"end";
if (!customStringRedisTemplate.hasKey(key)) {
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(incrByAndExpireLua, Long.class);
// 要求这里输入的参数都需要为字符串
Long increment = customStringRedisTemplate.execute(redisScript, Collections.singletonList(key), NumberUtils.INTEGER_ONE.toString(), "300");
// 即使在并发情况下也只允许一个线程进入
if (NumberUtils.LONG_ONE.equals(increment)) {
try {
log.info("{} 线程进入", Thread.currentThread().getName());
} finally {
customStringRedisTemplate.delete(key);
}
}
}
}
对于 lua 脚本的分析:
- "redis.call('incrBy', KEYS[1], ARGV[1])" , 表示当前调用 "incrBy" 命令, 并指定 key 为输入参数的第一个参数 key,value 为第一个参数 value
- 由于 "incrBy" 会返回增加后的结果, 如果当前 key 不存在, 则会生成新的 key, 且默认 value 为 0 并增加输入的 value; 因此对于判断 "== tonumber(ARGV[1])", 这里要求 "ARGV[1]" 需要不等于 0, 否则无法判断是否是第一次操作
- 对于 "if option then operate else operate" , 就是简单的 if else 判断操作, 如果是第一次操作, 则在调用 expire 命令设置过期时间, 原因在于对于 "incrBy" 生成的新的 key 默认过期时间为 -1 表示永远存活;
- 对于 return 操作表示当前操作会返回数据, 对于返回数值类型数据操作, 要求 java 必须使用 Long 类型接收; 也可以直接返回布尔类型数据