一、原因
1、在分布式项目中,用户触发插入、更新等操作,我们只需要其中一个服务执行,如果不加分布式锁,后果很严重
二、方法
1、分布锁一般通过redis实现,主要通过setnx函数向redis保存一个key,value等于保存时的时间戳,并设置过期时间,然后返回true;
2、当获得锁超过等待时间返回false;
3、通过key获取redis保存的时间戳,如果value不为空,并且当前时间戳减去-value值超过锁过期时间返回false;
4、如果一次没有获得锁,则每隔一定时间(10ms或者20ms)再获取一次,直到超过等待时间返回false。
三、具体实现
4.0.0
com.cn.dl
springboot-aop-test
0.0.1-SNAPSHOT
jar
springboot-aop-test
Demo project for Spring Boot
org.springframework.boot
spring-boot-starter-parent
2.1.0.RELEASE
UTF-8
UTF-8
1.8
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-starter-aop
org.springframework
spring-expression
5.0.0.RELEASE
org.springframework.boot
spring-boot-starter-data-redis
org.projectlombok
lombok
commons-lang
commons-lang
2.6
com.alibaba
fastjson
1.2.47
org.springframework.boot
spring-boot-maven-plugin
1、RedisLockTestAnnotation
package com.cn.dl.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Created by yanshao on 2018/11/23.
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisLockTestAnnotation {
String redisKey();
}
2、RedisLockTestAspectUtils
package com.cn.dl.utils;
import com.cn.dl.annotation.RedisLockTestAnnotation;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* Created by yanshao on 2018/11/23.
*/
@Aspect
@Component
@Slf4j
public class RedisLockTestAspectUtils {
@Autowired
RedisLock redisLockUtil;
@Around("@annotation(redisLock)")
public Object redisLockTest(ProceedingJoinPoint point, RedisLockTestAnnotation redisLock){
String lockKey = null;
boolean flag = false;
try {
//根据
String paramterIndex = redisLock.redisKey().substring(redisLock.redisKey().indexOf("#") + 1);
int index = Integer.parseInt(paramterIndex);
//获取添加注解方法中的参数列表
Object[] args = point.getArgs();
//生成redis的key:
// TODO: 2018/11/23 根据固定为:REDIS_TEST_#数字,必须是参数列表对应的下表,从0开始,并且小于参数列表的长度
lockKey = redisLock.redisKey().replace("#"+paramterIndex,args[index].toString());
log.info("redis key:{}",lockKey);
//set到redis
flag = redisLockUtil.lock(lockKey,args[index].toString());
log.info("redis save result:{}",flag);
//执行添加了注解的方法并返回
if(flag){
Object result = point.proceed();
return result;
}
}catch (Exception e){
e.printStackTrace();
}catch (Throwable throwable) {
throwable.printStackTrace();
}finally {
//最后在finally中删除
if(flag){
redisLockUtil.unlock(lockKey,"");
}
}
return null;
}
}
3、RedisLock
package com.cn.dl.utils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.concurrent.TimeUnit;
/**
* Created by yanshao on 2018/9/30.
*/
@Component
@Slf4j
public class RedisLock {
@Autowired
StringRedisTemplate redisTemplate;
private static final long EXPIRE = 60 * 1000L;
private static final long TIMEOUT = 10 * 1000L;
public boolean lock(String key,String value){
log.info("获取锁 kye:{},value:{}",key,value);
//请求锁时间
long requestTime = System.currentTimeMillis();
while (true){
//等待锁时间
long watiTime = System.currentTimeMillis() - requestTime;
//如果等待锁时间超过10s,加锁失败
if(watiTime > TIMEOUT){
log.info("等待锁超时 kye:{},value:{}",key,value);
return false;
}
if(redisTemplate.opsForValue().setIfAbsent(key,String.valueOf(System.currentTimeMillis()))){
//获取锁成功
log.info("获取锁成功 kye:{},value:{}",key,value);
//设置超时时间,防止解锁失败,导致死锁
redisTemplate.expire(key,EXPIRE, TimeUnit.MILLISECONDS);
return true;
}
String valueTime = redisTemplate.opsForValue().get(key);
if(! StringUtils.isEmpty(valueTime) && System.currentTimeMillis() - Long.parseLong(valueTime) > EXPIRE){
//加锁时间超过过期时间,删除key,防止死锁
log.info("锁超时, key:{}, value:{}", key, value);
try{
redisTemplate.opsForValue().getOperations().delete(key);
}catch (Exception e){
log.info("删除锁异常 key:{}, value:{}", key, value);
e.printStackTrace();
}
return false;
}
//获取锁失败,等待20毫秒继续请求
try {
log.info("等待20 nanoSeconds key:{},value:{}",key,value);
TimeUnit.NANOSECONDS.sleep(20);
} catch (InterruptedException e) {
log.info("等待20 nanoSeconds 异常 key:{},value:{}",key,value);
e.printStackTrace();
}
}
}
/**
* 分布式加锁
* @param key
* @param value
* @return
* */
public boolean secKilllock(String key,String value){
/**
* setIfAbsent就是setnx
* 将key设置值为value,如果key不存在,这种情况下等同SET命令。
* 当key存在时,什么也不做。SETNX是”SET if Not eXists”的简写
* */
if(redisTemplate.opsForValue().setIfAbsent(key,value)){
//加锁成功返回true
return true;
}
String currentValue = redisTemplate.opsForValue().get(key);
/**
* 下面这几行代码的作用:
* 1、防止死锁
* 2、防止多线程抢锁
* */
if(! StringUtils.isEmpty(currentValue)
&& Long.parseLong(currentValue) < System.currentTimeMillis()){
String oldValue = redisTemplate.opsForValue().getAndSet(key,value);
if(! StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue)){
return true;
}
}
return false;
}
/**
* 解锁
* @param key
* @param value
* */
public void unlock(String key,String value){
try{
redisTemplate.opsForValue().getOperations().delete(key);
}catch (Exception e){
e.printStackTrace();
}
}
}
4、RedisConfig
package com.cn.dl.config;
/**
* Created by yanshao on 2018/11/23.
*/
public interface RedisConfig {
String REDIS_LOCK = "'REDIS_LOCK_'";
String REDIS_TEST = "REDIS_TEST_";
long REDIS_EXPIRE_TIME = 60L * 2;
}
5、RestConfiguration
加这个配置的原因是:保存到redis的key和value乱码问题,就是因为没有序列化!!!
package com.cn.dl.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* Created by yanshao on 2018/11/23.
*/
@Configuration
public class RestConfiguration {
@Autowired
private RedisTemplate redisTemplate;
@Bean
public RedisTemplate redisKeyValueSerializer() {
//redis key和value值序列化,不序列话发现查到的key和value乱码
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return redisTemplate;
}
}
6、StudentController
package com.cn.dl.controller;
import com.alibaba.fastjson.JSONObject;
import com.cn.dl.annotation.RedisLockTestAnnotation;
import com.cn.dl.config.RedisConfig;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* Created by yanshao on 2018/11/23.
*/
@RestController
@RequestMapping("/student")
public class StudentController {
@PostMapping("update")
@RedisLockTestAnnotation(redisKey = RedisConfig.REDIS_TEST + "#0")
public JSONObject sutdentInfoUpdate(@RequestParam("studentId") String studentId,
@RequestParam("age") int age){
JSONObject result = new JSONObject();
result.put("update","success");
return result;
}
}
7、application.properties
server.port=7555
#redis
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=*****