定义Redis上锁、解锁实现类
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Component
@Slf4j
public class RedisLockServer {
@Resource
private StringRedisTemplate stringRedisTemplate;
public Boolean setLock(String lockKey, String value, long time) {
if(time<=0){
// 不设置过期时间
return stringRedisTemplate.opsForValue().setIfAbsent(lockKey, value);
}
return stringRedisTemplate.opsForValue().setIfAbsent(lockKey, value, time, TimeUnit.MILLISECONDS);
}
public void deleteLock(String lockKey, String value) {
List<String> lockKeys = Collections.singletonList(lockKey);
String lua = "if redis.call('get', KEYS[1]) == ARGV[1] then redis.call('del', KEYS[1]) return 1 else return 0 end";
RedisScript<Long> luaScript = RedisScript.of(lua, Long.class);
// 删除锁
stringRedisTemplate.execute(luaScript, lockKeys, value);
}
}
定义“自定义注解”,用于切面实现分布式锁
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedisLock {
/**
* 上锁的方法中,key所在参数的位置,索引从0开始
* @return
*/
int keyNum();
/**
* 上锁时长,默认设置时间
* @return
*/
long lockTime() default 0;
/**
* 尝试时间,设置时间内通过自旋一致尝试获取锁,默认0ms
* @return
*/
long tryTime() default 0;
}
字段说明:
keyNum
表示在被@RedisLock
修饰的方法上,第几个参数表示上锁的key
,默认第一个参数,也可以指定key的位置,并且参数的名称必须要为keyName
// key为第一个参数值
void test(String keyName){}
// key为第三个参数值
@RedisLock(keyNum=2)
void test(int a,int b,String keyName){}
lockTime
表示上锁过期时间,默认为0
,表示不进行上锁处理; public Boolean setLock(String lockKey, String value, long time) {
if(time<=0){
// 不设置过期时间
return stringRedisTemplate.opsForValue().setIfAbsent(lockKey, value);
}
return stringRedisTemplate.opsForValue().setIfAbsent(lockKey, value, time, TimeUnit.MILLISECONDS);
}
tryTime
表示尝试获取锁的时间,当设置的时间内,通过自旋的方式一直获取锁切面类定义,代码如下:
import lhz.lx.config.RedisLockServer;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.CodeSignature;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.util.UUID;
@Aspect
@Component
@Slf4j
public class RedisLockAspect {
@Resource
private RedisLockServer redisLockServer;
private static final ThreadLocal<String> VALUE_THREAD = new ThreadLocal<>();
private static final ThreadLocal<String> KEY_THREAD = new ThreadLocal<>();
private static final ThreadLocal<Boolean> LOCK_THREAD = new ThreadLocal<>();
@Pointcut("@annotation(lhz.lx.aspect.RedisLock)")
public void lockPoint() {
}
/**
* 环绕通知,调用目标方法
*/
@Around("lockPoint()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
// 记录方法执行开始时间
long startTime = System.currentTimeMillis();
Object[] args = proceedingJoinPoint.getArgs();
if(args.length<=0){
throw new RuntimeException("keyName不存在!");
}
String[] argNames = ((CodeSignature) proceedingJoinPoint.getSignature()).getParameterNames();
Signature signature = proceedingJoinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
RedisLock lock = method.getAnnotation(RedisLock.class);
int keyNum = lock.keyNum();
if(!"keyName".equals(argNames[keyNum])){
throw new RuntimeException("keyName不存在!");
}
String key = args[keyNum].toString();
long lockTime = lock.lockTime();
long tryTime = lock.tryTime();
String value = UUID.randomUUID().toString();
VALUE_THREAD.set(value);
KEY_THREAD.set(key);
log.info("分布式锁上锁,key:{},value:{},lockTime:{}",key,value,lockTime);
Boolean setLock = redisLockServer.setLock(key, value, lockTime);
while (!setLock){
// 重试
setLock = redisLockServer.setLock(key, value, lockTime);
if(System.currentTimeMillis()-startTime>tryTime) {
LOCK_THREAD.set(false);
log.error("上锁失败");
throw new RuntimeException("上锁失败!");
}
}
log.info("分布式锁上锁成功,key:{},value:{},lockTime:{}",key,value,lockTime);
LOCK_THREAD.set(true);
// 调用目标方法
return proceedingJoinPoint.proceed();
}
/**
* 处理完请求后执行
*
* @param joinPoint 切点
*/
@AfterReturning(value = "lockPoint()", returning = "jsonResult")
public void doAfterReturning(JoinPoint joinPoint, Object jsonResult) {
handleData();
}
/**
* 拦截异常操作
*
* @param joinPoint 切点
* @param e 异常
*/
@AfterThrowing(value = "lockPoint()", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
handleData();
}
private void handleData() {
try {
String value = VALUE_THREAD.get();
String key = KEY_THREAD.get();
if(LOCK_THREAD.get()) {
log.info("分布式锁解锁,key:{},value:{}", key, value);
redisLockServer.deleteLock(key, value);
}
} catch (Exception exception) {
exception.printStackTrace();
} finally {
VALUE_THREAD.remove();
KEY_THREAD.remove();
LOCK_THREAD.remove();
}
}
}
因为说明AOP无法拦截类内部的方法之间的调用,需要对启动类加上@EnableAspectJAutoProxy
配置,代码如下:
@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true)
public class RedisDemoApplication {
public static void main(String[] args) {
SpringApplication.run(RedisDemoApplication.class, args);
}
}
比如方法直接的调用,A方法调用B方法,使用方式如下:
public class RedisServiceImpl implements RedisService{
public void A() {
// 方法类调用
RedisServiceImpl service = (RedisServiceImpl) AopContext.currentProxy();
service.B();
}
@RedisLock
public void B() {
}
}
注意: 在调用使用分布式锁的方法时,需要进try…catch…,并且在catch中处理上锁失败的情况;
TestController:
@RestController
@RequestMapping
@Slf4j
public class TestController {
@Resource
private RedisService redisService;
@GetMapping(value = "/test")
public String test() {
redisService.test();
return "success";
}
@GetMapping(value = "/test2")
public String test2() {
String key = UUID.randomUUID().toString();
redisService.test2(key);
return "success";
}
}
RedisService :
public interface RedisService {
/**
* 方法内部调用使用锁
*/
void test();
/**
* 方法直接调用使用锁
* @param keyName
*/
void test2(String keyName);
}
RedisServiceImpl:
@Service
@Slf4j
public class RedisServiceImpl implements RedisService{
/**
* 方法内部调用使用锁
*/
@Override
public void test() {
log.info("方法内部调用使用锁");
// 调用内部方法
RedisServiceImpl service = (RedisServiceImpl) AopContext.currentProxy();
// 在调用使用分布式锁的方法时,需要进try...catch...,并且在catch中处理上锁失败的情况
try {
service.testLock("test11");
}catch (Exception e){
e.printStackTrace();
}
}
/**
* 方法直接调用使用锁
*/
@Override
@RedisLock(keyNum = 0)
public void test2(String keyName) {
log.info("方法直接调用使用锁");
}
/**
* 被上锁的方法中,一定要存在一个叫"keyName"的参数
* @param keyName
*/
@RedisLock
public void testLock(String keyName) throws InterruptedException {
log.info(keyName);
}
}
测试二:
设置tryTime,并且tryTime小于lockTime
;当这样配置时,在第一个线程没有结束时,第二个线程,超过tryTime
就会出现上锁失败;
修改代码如下:
@RedisLock(keyNum = 0,lockTime = 3000,tryTime = 2000)
public void testLock(String keyName) throws InterruptedException {
log.info(keyName);
TimeUnit.SECONDS.sleep(5);
}
快速请求两次
接口,截图如下:
通过截图可以看到,在第一个线程上锁后,过了2000ms
出现了上锁失败的提示;
测试三:
设置tryTime,并且tryTime大于lockTime;当这样配置时,不会出现上锁失败,并且第二个线程会一直等到第一个线程结束;
修改代码如下:
@RedisLock(keyNum = 0,lockTime = 3000,tryTime = 4000)
public void testLock(String keyName) throws InterruptedException {
log.info(keyName);
TimeUnit.SECONDS.sleep(5);
}
通过截图可以看到,在第一个线程上锁后,超过了3000ms
后,第二个线程开始上锁成功