1. 程式人生 > >springboot-通過註解和aop實現分散式鎖

springboot-通過註解和aop實現分散式鎖

一、原因

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=*****