springboot-通过注解和aop实现分布式锁

版权声明:转发请注明,谢谢配合 https://blog.csdn.net/qq_31289187/article/details/84399880

一、原因

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

猜你喜欢

转载自blog.csdn.net/qq_31289187/article/details/84399880
今日推荐