Redis分布式锁解决接口幂等的两种方案
Redis分布式锁解决接口幂等的两种方案一、背景二、基础知识三、解决方案四、实验五、说在最后的话一、背景还在为不了解分布式锁而烦恼吗?还在为众多微服务接口不幂等而发愁吗?如果是,并且有兴趣同我一起学习,那请接着看本文,通过本文能够学习到分布式锁的基本原理、如何实现分布式锁以及使用分布式锁解决接口幂等问题。二、基础知识本文是通过使用 Redis 实现分布式锁,当然也可用使用各大数据库,比如 ...
一、背景
还在为不了解分布式锁而烦恼吗?还在为众多微服务接口不幂等而发愁吗?如果是,并且有兴趣同我一起学习,那请接着看本文,通过本文能够学习到分布式锁的基本原理、如何实现分布式锁以及使用分布式锁解决接口幂等问题。
二、基础知识
本文是通过使用 Redis 实现分布式锁,当然也可用使用各大数据库,比如 Mysql、Oracle 自持的行级锁、大厂的 Zookeeper 等方案实现。
-
分布式锁的基本思想
我们既然称其为“锁”,那就是说只有唯一的一把钥匙才能将锁打开,将这种思想放到我们软件设计上来,那就是在同一时间内只有同一个进程或者线程拥有这个”钥匙“来锁住资源,防止其他进程或线程用”钥匙“锁住同一个资源。当然,这是一种通俗的理解,在我们软件工程中,“锁”是要更复杂,更难以掌握的。 -
Redis 实现分布式锁原理
Redis 主要是利用命令redis.call()
、SETNX
和PEXPIRE
实现分布式锁的,但是因为是两个分开的命令,单独执行这两个命令肯定是非原子性,根据答墨菲定理未来一定会发生非原子的操作。好在一点是的 Redis 可以使用 Lua 脚本将单独的多个命令统一顺序执行,命令EVAL
。通过 EVAL 命名可以执行多个命令,这些命名要么都成功,要么都失败(这就是我们想要的事务的原子性啊)。关于 Lua 脚本如何使用,Redis 官网有示例,可以点击 Lua 脚本使用 学习。如果觉得 Lua 太难,那就感谢 Redis 帮我们实现了分布式锁框架Redisson
吧,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 = LoggerFactory.getLogger(ApiIdempotentAspect.class);
private final RedisLockUtil redisLockUtil;
@Autowired
public ApiIdempotentAspect(RedisLockUtil redisLockUtil) {
this.redisLockUtil = redisLockUtil;
}
@Pointcut("@annotation(com.gridsum.techpub.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
* @version 1.0
* @date 2020/4/20 11:21
**/
@Component
public class RedisLockUtil {
private final Logger logger = LoggerFactory.getLogger(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) {
this.redisTemplate = 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("locking... redisK = {}", key);
try {
//执行脚本
Object result = redisTemplate
.execute(LOCK_LUA_SCRIPT, Collections.singletonList(key), value, expire);
//存储本地变量
if(LOCK_SUCCESS.equals(result)) {
logger.info("locked... 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("locked... redisK = {}", key);
return true;
}
logger.warn("{} times try to acquire lock", count + 1);
count++;
} catch (Exception e) {
logger.error("acquire redis occurred an exception", e);
}
}
logger.info("fail to acquire lock {}", key);
return false;
}
} catch (Throwable e1) {
logger.error("acquire redis occurred an exception", e1);
}
return false;
}
/**
* 释放KEY
* @param key 释放本请求对应的锁的key
* @param value 释放本请求对应的锁的value
* @return 释放锁 true 成功
*/
public boolean unlock(String key, String value) {
key = KEY_PREFIX + key;
logger.info("unlock... 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
logger.error("release lock failed");
}
} catch (Throwable e) {
logger.error("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 = LoggerFactory.getLogger(RedisLockUtil.class);
private final RedissonClient redissonClient;
@Autowired
public RedisLockUtil(@Qualifier("customRedisson") RedissonClient redissonClient) {
this.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("locking... redisK = {}", key);
RLock fairLock = redissonClient.getFairLock(key + ":" + value);
try {
boolean tryLock = fairLock.tryLock(0, expire, TimeUnit.MILLISECONDS);
if (tryLock) {
logger.info("locked... redisK = {}", key);
return fairLock;
} else {
//重试获取锁
logger.info("retry to acquire lock: [redisK = {}]", key);
int count = 0;
while(count < retryTimes) {
try {
Thread.sleep(retryInterval);
tryLock = fairLock.tryLock(0, expire, TimeUnit.MILLISECONDS);
if(tryLock) {
logger.info("locked... redisK = {}", key);
return fairLock;
}
logger.warn("{} times try to acquire lock", count + 1);
count++;
} catch (Exception e) {
logger.error("acquire redis occurred an exception", e);
break;
}
}
logger.info("fail to acquire lock {}", key);
}
} catch (Throwable e1) {
logger.error("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");
return true;
}
} catch (Throwable e) {
logger.error("release lock occurred an exception", e);
}
return false;
}
}
四、实验
- 通过 jmeter 进行压测(15 线程 * 循环 10 次),结果达到预期。
五、说在最后的话
-
本文必须保证 key + value 的唯一性。
-
如有说错的地方,还请在下方评论留言(每个评论我都会去认真查看),不吝赐教,我也是一直在学习中。
更多推荐
所有评论(0)