Lua官网
http://doc.redisfans.com/script/eval.html
问题重现:预约功能,用jemter测试,30个不同用户同时预约,2个用户重复发送请求,即重复发送消息场景,结果导致重复消费
避免重复消费方式
1.如第6行,重复原因:第10行,业务逻辑耗时比较长,还没执行第11行,导致6行,执行通过,重复的数据都进来了
2.如第5行+12行,高并发下,加分布式锁,重复请求(具体场景是什么),只有一个能执行业务逻辑。这样不够,6行还是要保留,保证幂等。
分析:分布式锁只能保证高并发下幂等行,无法保证持久幂等。这里注意:分布式锁与existKey,都不是一个固定值,保证10业务逻辑可以并发执行。
高并发与幂等行没有关系。
1 String key = RedisEnum.APPOINT_RECORD_KEY.toKey(reqVO.getLiveId(), reqVO.getUid()); 2 String uniqueVal = UUID.randomUUID().toString(); 3 String lockKey = RedisEnum.APPOINT_RECORD_LOCK_KEY.toKey(reqVO.getLiveId(), reqVO.getUid()); 4 try { 5 if (redisManager.tryLock(lockKey, uniqueVal, APPOINT_RECORD_LOCK_KEY.expired)) { 6 Boolean exist = redisManager.existsKey(key); 7 if (Boolean.TRUE.equals(exist)) { 8 log.error("saveAppoint exception already appointed reqParam:{}", JSON.toJSONString(reqVO)); 9 throw ExceptionUtils.throwException(LIVE_APPOINTED); } 10 todo 处理业务逻辑 11 redisManager.put(key, RedisEnum.APPOINT_RECORD_KEY.expired, JSON.toJSONString(reqVO)); } } catch (Exception e) { log.error("Saving appoint exception", e); throw ExceptionUtils.throwException(LIVE_APPOINTED); } finally { 12 redisManager.releaseLock(lockKey, uniqueVal); } return false;
参考:
lua实现
加锁:
public boolean tryLock(String lockKey, String randomUnValue, int expireTime) {
Jedis jedis = null;
boolean var6;
try {
jedis = this.jedisPool.getResource();
Long result = (Long)jedis.eval(LuaSecript.SETNX_AND_EXPIRE.getScript(), 1, new String[]{lockKey, randomUnValue, String.valueOf(expireTime)});
if (result != 1L) {
return false;
}
var6 = true;
} catch (Exception var10) {
this.logger.error("Try lock failed", var10);
return false;
} finally {
if (jedis != null) {
jedis.close();
}
}
return var6;
}
public enum LuaSecript {
SETNX_AND_EXPIRE("local is_existed = redis.call('setnx', KEYS[1], ARGV[1]);if (is_existed == 1) then redis.call('expire', KEYS[1], ARGV[2]);return 1; end; return 0;"),
DEL_KEY_AND_EQUAL_VAL("if (redis.call('get', KEYS[1]) == ARGV[1]) then return redis.call('del', KEYS[1]);else return 0;end; "),
ZRANGEBYSCORE_ZREM("local data = redis.call('ZRANGEBYSCORE', KEYS[1], ARGV[1], ARGV[2], 'WITHSCORES', 'LIMIT', ARGV[3], ARGV[4]);if table.getn(data) > 0 then for k,v in pairs(data) do if (k % 2) ~=0 then redis.call('ZREM', KEYS[1], v);end;end;return data;end;");
private String script;
private LuaSecript(String script) {
this.script = script;
}
public String getScript() {
return this.script;
}
}
解锁:
public boolean releaseLock(String lockKey, String randomUnValue) {
Jedis jedis = null;
boolean var5;
try {
jedis = this.jedisPool.getResource();
jedis.eval(LuaSecript.DEL_KEY_AND_EQUAL_VAL.getScript(), 1, new String[]{lockKey, randomUnValue});
return true;
} catch (Exception var9) {
this.logger.error("Release lock failed", var9);
var5 = false;
} finally {
if (jedis != null) {
jedis.close();
}
}
return var5;
}
依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>