摘要:本文将介绍如何使用Redis
分布式锁来确保在高并发环境下,确保某个接口只有一个线程能够执行。通过使用Redis
的SETNX
命令,我们可以实现一个简单的分布式锁,从而避免多个线程同时访问共享资源。
一、背景
在高并发的系统中,为了保证数据的一致性和完整性,我们经常需要对某些接口进行互斥访问。例如,在电商系统中,当用户下单时,我们需要确保同一时间只有一个用户能够完成支付操作。为了实现这一目标,我们可以使用分布式锁。Redis
作为一种高性能的内存数据库,非常适合用于实现分布式锁。
二、Redis
分布式锁的原理
Redis
分布式锁的原理是:在Redis
中设置一个特定的键值对,当多个线程同时尝试获取锁时,只有一个线程能够成功设置该键值对,其他线程则无法设置成功。这样,我们就可以确保同一时间只有一个线程能够执行被锁住的接口。
三、实现Redis
分布式锁的步骤
① 使用SETNX
命令尝试设置锁。SETNX
命令在设置键值对时,只有当键不存在时才能设置成功。如果键已经存在,则设置失败。这样,我们就可以确保同一时间只有一个线程能够成功设置锁。
② 如果设置锁成功,则执行被锁住的接口。在执行完接口后,需要释放锁,以便其他线程可以获取锁并执行接口。释放锁的方法是删除键值对。
③ 如果设置锁失败,则说明已经有其他线程正在执行被锁住的接口。此时,线程可以选择等待一段时间后再次尝试获取锁,或者直接放弃执行。
四、示例代码
4. RedisUtils
工具类
public class RedisUtils {
private final static Logger LOGGER = LoggerFactory.getLogger(RedisUtils.class);
/**
* 默认redis过期时间3s
*/
private static final long expireTime = 3000L;
private static RedisTemplate redisTemplate;
/**
* 加锁,自定义过期时间
*/
public static boolean lock(final String key, Long expireTime) {
if (key == null) {
return false;
}
String value = GeneralUtils.generateUUID();
Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, value, expireTime, TimeUnit.MILLISECONDS);
if (Boolean.TRUE.equals(flag)) {
return true;
} else {
throw new BusinessException("业务繁忙,请稍后重试");
}
}
/**
* 释放锁
*/
public static boolean unlock(final String key) {
if (key == null) {
return false;
}
return Boolean.TRUE.equals(redisTemplate.delete(key));
}
}
4.2 自定义RedisLock
注解
/**
* 本注解基于RedisUtils封装
* expireTime 过期时间 如果注解中不赋值,则使用默认值,单位毫秒
* lockKey 用于指定被注解方法参数中做为锁的KEY的参数名称。如果不指定,则使用类名+方法名做为KEY
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisLock {
long expireTime() default 30000;
String lockKey() default "";
}
4.3 RedisLockAspect
切面类
@Aspect
@Component
@Slf4j
public class RedisLockAspect {
@Pointcut("@annotation(luoxs.common.annotation.RedisLock)")
private void anyMethod() {
}
@Around("anyMethod()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Object result;
Method method = ((MethodSignature) pjp.getSignature()).getMethod();
RedisLock redisLock = method.getAnnotation(RedisLock.class);
if (redisLock == null) {
Method realMethod = pjp.getTarget().getClass().getDeclaredMethod(method.getName(), method.getParameterTypes());
redisLock = realMethod.getAnnotation(RedisLock.class);
}
String lockKey = redisLock.lockKey();
long expireTime = redisLock.expireTime();
if (ObjectUtil.isEmpty(lockKey)) {
//如果未设置key字段则使用类名+方法名做为KEY
lockKey = method.getDeclaringClass().getName() + method.getName();
}
try {
//加锁
RedisUtils.lock(lockKey, expireTime);
//执行
result = pjp.proceed();
} catch (Exception e) {
log.error(e.getMessage(),e);
throw e;
} finally {
RedisUtils.unlock(lockKey);
}
return result;
}
}
4.4 新建一个TestController
进行测试
@RestController
@RequestMapping("/test")
public class TestController{
@RedisLock(lockKey = "testRedisLock")
@GetMapping("/testRedisLock")
@ApiOperation("Redis分布式锁确保接口唯一性测试")
public BaseResponse<Void> testRedisLock() throws InterruptedException{
//方便测试睡30秒
Thread.sleep(30000);
return BaseResponse.ok();
}
}
4.5 测试结果
个人比较喜欢用Postman
进行测试:第一个线程调用之后,紧接着启用另一个线程进行调用
第二个线程返回了我们想要的信息,如下图所示:
第一个线程开始调用后30秒返回了执行的结果,如下图所示:
五、总结
通过使用Redis
分布式锁,我们可以确保在高并发环境下,某个接口只有一个线程能够执行。这对于保证数据的一致性和完整性非常重要。