springboot-通過註解和aop實現分散式鎖
阿新 • • 發佈:2018-12-11
一、原因
1、在分散式專案中,使用者觸發插入、更新等操作,我們只需要其中一個服務執行,如果不加分散式鎖,後果很嚴重
二、方法
1、分佈鎖一般通過redis實現,主要通過setnx函式向redis儲存一個key,value等於儲存時的時間戳,並設定過期時間,然後返回true;
2、當獲得鎖超過等待時間返回false;
3、通過key獲取redis儲存的時間戳,如果value不為空,並且當前時間戳減去-value值超過鎖過期時間返回false;
4、如果一次沒有獲得鎖,則每隔一定時間(10ms或者20ms)再獲取一次,直到超過等待時間返回false。
三、具體實現
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.cn.dl</groupId> <artifactId>springboot-aop-test</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>springboot-aop-test</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.0.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-expression</artifactId> <version>5.0.0.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>commons-lang</groupId> <artifactId>commons-lang</artifactId> <version>2.6</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.47</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
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=*****