Redis分布式锁解决接口幂等的两种方案

Redis分布式锁解决接⼝幂等的两种⽅案
Redis分布式锁解决接⼝幂等的两种⽅案
⼀、背景
还在为不了解分布式锁⽽烦恼吗?还在为众多微服务接⼝不幂等⽽发愁吗?如果是,并且有兴趣同我⼀起学习,那请接着看本⽂,通过本⽂能够学习到分布式锁的基本原理、如何实现分布式锁以及使⽤分布式锁解决接⼝幂等问题。
⼆、基础知识
本⽂是通过使⽤ Redis 实现分布式锁,当然也可⽤使⽤各⼤数据库,⽐如 Mysql、Oracle ⾃持的⾏级锁、⼤⼚的 Zookeeper 等⽅案实现。
分布式锁的基本思想
我们既然称其为“锁”,那就是说只有唯⼀的⼀把钥匙才能将锁打开,将这种思想放到我们软件设计上来,那就是在同⼀时间内只有同⼀个进程或者线程拥有这个”钥匙“来锁住资源,防⽌其他进程或线程⽤”钥匙“锁住同⼀个资源。当然,这是⼀种通俗的理解,在我们软件⼯程中,“锁”是要更复杂,更难以掌握的。
Redis 实现分布式锁原理
Redis 主要是利⽤命令 redis.call() 、SETNX 和 PEXPIRE 实现分布式锁的,但是因为是两个分开的命令,单独执⾏这两个命令肯定是⾮原⼦性,根据答墨菲定理未来⼀定会发⽣⾮原⼦的操作。好在⼀点是的 Redis 可以使⽤ Lua 脚本将单独的多个命令统⼀顺序执⾏,命令 EVAL。通过 EVAL 命名可以执⾏多个命令,这些命名要么都成功,要么都失败(这就是我们想要的事务的原⼦性啊)。关于Lua 脚本如何使⽤,Redis 官⽹有⽰例,可以点击 学习。如果觉得 Lua 太难,那就感谢 Redis 帮我们实现了分布式锁框架 Redisson 吧,。另外 Redisson 帮我们实现了更多细节问题,例如,通过加⼊ watchdog 监控锁的状态,当实例还在运⾏时⾃动帮你续约(实际就是通过命令 PEXPIRE 重新设定过期时间)。
三、解决⽅案
为了能够在多场景下复⽤,避免重复造轮⼦的现象,我们可以借助 Spring AOP 技术,通过⾃定义注解 @ApiIdempotent 来实现,写好后在打成 jar 放到我们的中央仓库,在项⽬上引⼊ jar ,再在需要控制接⼝幂等的 Controller ⽅法上加上我们的注解即可,⽅便快捷。我这下⾯⾃定义⼀个接⼝幂等的注解:
/**
* ⾃定义接⼝幂等注解
* @author ouyang
* @version 1.0
* @date 2020/4/20 11:21
**/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiIdempotent {
/**
* 过期时间,单位:ms。默认2000
*/
long expire() default 2000;
/**
* 重试次数,默认0
*/
int retryTimes() default 0;
/**
* 重试间隔时间,单位:ms,默认100
*/
long retryInterval() default 100;
}
本注解 @ApiIdempotent ⽀持⾃定义锁时间、重试加锁次数及重试间隔设置。
/
**
* @author ouyang
* @version 1.0
* @date 2020/4/20 11:21
**/
@Aspect
@Component
public class ApiIdempotentAspect {
private final Logger logger = Logger(ApiIdempotentAspect.class);
private final RedisLockUtil redisLockUtil;
@Autowired
public ApiIdempotentAspect(RedisLockUtil redisLockUtil) {
}
@Pointcut("@hpub.apiidempotent.annotation.ApiIdempotent)")    public void apiIdempotentPointCut() { }
@Around("apiIdempotentPointCut()")
public Object apiIdempotentAround(ProceedingJoinPoint point) throws Throwable {
// TODO lock
Object result = point.proceed();
// TODO unlock
return result;
}
}
基于 lua 脚本实现分布式锁解决接⼝幂等⽅案
/**
* 锁
* @author ouyang
* @author ouyang
* @version 1.0
* @date 2020/4/20 11:21
**/
@Component
public class RedisLockUtil {
private final Logger logger = Logger(RedisLockUtil.class);
private static final String KEY_PREFIX = "apiIdempotent:";
//定义获取锁的lua脚本
private static final DefaultRedisScript<Long> LOCK_LUA_SCRIPT = new DefaultRedisScript<>(
"if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then return redis.call('pexpire', KEYS[1], ARGV[2]) else return 0 end"            , Long.class
);
//定义释放锁的lua脚本
private static final DefaultRedisScript<Long> UNLOCK_LUA_SCRIPT = new DefaultRedisScript<>(
"if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return -1 end"
, Long.class
);
private static final Long LOCK_SUCCESS = 1L;
private static final Long LOCK_EXPIRED = -1L;
private RedisTemplate<String, Object> redisTemplate;
@Autowired
public RedisLockUtil(@Qualifier("customRedisTemplate") RedisTemplate<String, Object> redisTemplate) {
}
/**
* 加锁
* @param key 锁的 key
* @param value  value ( key + value 必须保证唯⼀)
* @param expire key 的过期时间,单位 ms
* @param retryTimes 重试次数,即加锁失败之后的重试次数
* @param retryInterval 重试时间间隔,单位 ms
* @return 加锁 true 成功
*/
public boolean lock(String key, String value, long expire, int retryTimes, long retryInterval) {
key = KEY_PREFIX + key;
logger.info(" redisK = {}", key);
try {
//执⾏脚本
Object result = redisTemplate
.
execute(LOCK_LUA_SCRIPT, Collections.singletonList(key), value, expire);
//存储本地变量
if(LOCK_SUCCESS.equals(result)) {
logger.info(" redisK = {}", key);
return true;
} else {
//重试获取锁
int count = 0;
while(count < retryTimes) {
try {
Thread.sleep(retryInterval);
result = redisTemplate
.execute(LOCK_LUA_SCRIPT, Collections.singletonList(key), value, expire);
if(LOCK_SUCCESS.equals(result)) {
logger.info(" redisK = {}", key);
return true;
}
logger.warn("{} times try to acquire lock", count + 1);
logger.warn("{} times try to acquire lock", count + 1);
count++;
} catch (Exception e) {
<("acquire redis occurred an exception", e);
}
}
logger.info("fail to acquire lock {}", key);
return false;
}
} catch (Throwable e1) {
<("acquire redis occurred an exception", e1);
}
return false;
}
/**
* 释放KEY
* @param key  释放本请求对应的锁的key
* @param value 释放本请求对应的锁的value
* @return 释放锁 true 成功
*/
msinfo
public boolean unlock(String key, String value) {
key = KEY_PREFIX + key;
logger.info(" redisK = {}", key);
try {
// 使⽤lua脚本删除redis中匹配value的key
Object result = redisTemplate
.
execute(UNLOCK_LUA_SCRIPT, Collections.singletonList(key), value);
//如果这⾥抛异常,后续锁⽆法释放
if (LOCK_SUCCESS.equals(result)) {
logger.info("release lock success. redisK = {}", key);
return true;
} else if (LOCK_EXPIRED.equals(result)) {
logger.warn("release lock exception, key has expired or released");
} else {
//其他情况,⼀般是删除KEY失败,返回0
<("release lock failed");
}
} catch (Throwable e) {
<("release lock occurred an exception", e);
}
return false;
}
}
缺点:
基于 Lua 脚本实现的分布式锁,锁的失效时间是⾃⼰设定的,需要根据接⼝的响应时间评个⼈经验设定合理的值,如果设定的失效时间过短,将可能导致该锁失效。
基于 Redisson 实现分布式锁解决接⼝幂等⽅案
/**
* 锁
* @author ouyang
* @version 1.0
* @date 2020/4/20 11:21
**/
@Component
public class RedisLockUtil {
private final Logger logger = Logger(RedisLockUtil.class);
private final Logger logger = Logger(RedisLockUtil.class);
private final RedissonClient redissonClient;
@Autowired
public RedisLockUtil(@Qualifier("customRedisson") RedissonClient redissonClient) {
}
/**
* 加锁
* @param key 锁的 key
* @param value  value ( key + value 必须保证唯⼀)
* @param expire key 的过期时间,单位 ms
* @param retryTimes 重试次数,即加锁失败之后的重试次数
* @param retryInterval 重试时间间隔,单位 ms
* @return 加锁 true 成功
*/
public RLock lock(String key, String value, long expire, int retryTimes, long retryInterval) {        logger.info(" redisK = {}", key);
RLock fairLock = FairLock(key + ":" +  value);
try {
boolean tryLock = Lock(0, expire, TimeUnit.MILLISECONDS);
if (tryLock) {
logger.info(" redisK = {}", key);
return fairLock;
} else {
//重试获取锁
logger.info("retry to acquire lock: [redisK = {}]", key);
int count = 0;
while(count < retryTimes) {
try {
Thread.sleep(retryInterval);
tryLock = Lock(0, expire, TimeUnit.MILLISECONDS);
if(tryLock) {
logger.info(" redisK = {}", key);
return fairLock;
}
logger.warn("{} times try to acquire lock", count + 1);
count++;
} catch (Exception e) {
<("acquire redis occurred an exception", e);
break;
}
}
logger.info("fail to acquire lock {}", key);
}
} catch (Throwable e1) {
<("acquire redis occurred an exception", e1);
}
return fairLock;
}
/
**
* 释放KEY
* @param fairLock 分布式公平锁
* @return 释放锁 true 成功
*/
public boolean unlock(RLock fairLock) {
try {
//如果这⾥抛异常,后续锁⽆法释放
if (fairLock.isLocked()) {
fairLock.unlock();
logger.info("release lock success");

本文发布于:2024-09-22 10:22:59,感谢您对本站的认可!

本文链接:https://www.17tex.com/tex/1/380292.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:分布式   重试   实现   命令   时间   钥匙   次数
留言与评论(共有 0 条评论)
   
验证码:
Copyright ©2019-2024 Comsenz Inc.Powered by © 易纺专利技术学习网 豫ICP备2022007602号 豫公网安备41160202000603 站长QQ:729038198 关于我们 投诉建议