以spring boot starter搭建redis分布式锁项目

spring boot starter搭建redis分布式锁项目及原理分析

本文作者:FUNKYE(陈健斌),杭州某互联网公司主程。

前言

demo项目地址

Redis 是完全开源免费的,遵守BSD协议,是一个高性能的key-value数据库。

Redis为什么这么快?

(一)纯内存操作,避免大量访问数据库,减少直接读取磁盘数据,redis 将数据储存在内存里面,读写数据的时候都不会受到硬盘 I/O 速度的限制,所以速度快;

(二)单线程操作,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;

(三)采用了非阻塞I/O多路复用机制

Redis 优势

  • 性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。

  • 丰富的数据类型 – Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。

  • 原子 – Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。

  • 丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性。

Redis 与其他 key - value 缓存产品有以下三个特点:

  • Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。

  • Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。

  • Redis支持数据的备份,即master-slave模式的数据备份。

 

项目搭建

1.首先我们创建一个正常的maven项目并引入如下依赖


    4.0.0io.funkye
    redis-lock-spring-boot-starter
    0.0.1-SNAPSHOT
    jarredis-lock-spring-boot-starter
    http://maven.apache.org
        UTF-8
    
        
            org.springframework.boot
            spring-boot-autoconfigure
            2.1.8.RELEASE
            provided
        
        
            redis.clients
            jedis
            2.9.1
            provided
        
        
            org.springframework.boot
            spring-boot-starter-aop
            2.1.8.RELEASE
            provided
        
        
            org.springframework.boot
            spring-boot-starter-data-redis
            2.1.8.RELEASE
            provided
        
    

2.创建src/main/java与src/main/resources文件夹

3.这里我们以我的demo包名为主,大家可以自定义:io.funkye.redis.lock.starter

4.在starter包下在创建我们需要的config,config.annotation(放入我们需要的注解),service及service.impl,aspect(用来使用aop增强)

如果大家创建好了,参照下图即可:

以spring boot starter搭建redis分布式锁项目_第1张图片

进行开发

1.我们先创建我们需要的装载redis配置的JedisLockProperties,创建在config包下

package io.funkye.redis.lock.starter.config;
​
import org.springframework.boot.context.properties.ConfigurationProperties;
​
@ConfigurationProperties(prefix = JedisLockProperties.JEDIS_PREFIX)
public class JedisLockProperties {
    public static final String JEDIS_PREFIX = "redis.lock.server";
​
    private String host;
​
    private int port;
​
    private String password;
​
    private int maxTotal;
​
    private int maxIdle;
​
    private int maxWaitMillis;
​
    private int dataBase;
​
    private int timeOut;
​
    public int getTimeOut() {
        return timeOut;
    }
​
    public void setTimeOut(int timeOut) {
        this.timeOut = timeOut;
    }
​
    public String getPassword() {
        return password;
    }
​
    public void setPassword(String password) {
        this.password = password;
    }
​
    public String getHost() {
        return host;
    }
​
    public void setHost(String host) {
        this.host = host;
    }
​
    public int getPort() {
        return port;
    }
​
    public void setPort(int port) {
        this.port = port;
    }
​
    public int getMaxTotal() {
        return maxTotal;
    }
​
    public void setMaxTotal(int maxTotal) {
        this.maxTotal = maxTotal;
    }
​
    public int getMaxIdle() {
        return maxIdle;
    }
​
    public void setMaxIdle(int maxIdle) {
        this.maxIdle = maxIdle;
    }
​
    public int getMaxWaitMillis() {
        return maxWaitMillis;
    }
​
    public void setMaxWaitMillis(int maxWaitMillis) {
        this.maxWaitMillis = maxWaitMillis;
    }
​
    public int getDataBase() {
        return dataBase;
    }
​
    public void setDataBase(int dataBase) {
        this.dataBase = dataBase;
    }
​
    public static String getJedisPrefix() {
        return JEDIS_PREFIX;
    }
​
    @Override
    public String toString() {
        return "JedisProperties [host=" + host + ", port=" + port + ", password=" + password + ", maxTotal=" + maxTotal
            + ", maxIdle=" + maxIdle + ", maxWaitMillis=" + maxWaitMillis + ", dataBase=" + dataBase + ", timeOut="
            + timeOut + "]";
    }
​
}

2.在starter包下创建我们的装配类RedisLockAutoConfigure

package io.funkye.redis.lock.starter;
​
import java.time.Duration;
​
import javax.annotation.PostConstruct;
​
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.data.redis.connection.RedisPassword;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.jedis.JedisClientConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
​
import io.funkye.redis.lock.starter.config.JedisLockProperties;
import redis.clients.jedis.Jedis;
​
//扫描我们的包,保证其被初始化完成
@ComponentScan(basePackages = {"io.funkye.redis.lock.starter.config", "io.funkye.redis.lock.starter.service",
    "io.funkye.redis.lock.starter.aspect"})
//保证配置类优先加载完
@EnableConfigurationProperties({JedisLockProperties.class})
//必须要有jedis的依赖才会初始化功能
@ConditionalOnClass(Jedis.class)
@Configuration
public class RedisLockAutoConfigure {
​
    @Autowired
    private JedisLockProperties prop;
    private static final Logger LOGGER = LoggerFactory.getLogger(RedisLockAutoConfigure.class);
​
    @PostConstruct
    public void load() {
        LOGGER.info("分布式事务锁初始化中........................");
    }
    //创建JedisConnectionFactory
    @Bean(name = "jedisLockConnectionFactory")
    public JedisConnectionFactory getConnectionFactory() {
        RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
        redisStandaloneConfiguration
            .setHostName(null == prop.getHost() || prop.getHost().length() <= 0 ? "127.0.0.1" : prop.getHost());
        redisStandaloneConfiguration.setPort(prop.getPort() <= 0 ? 6379 : prop.getPort());
        redisStandaloneConfiguration.setDatabase(prop.getDataBase() <= 0 ? 0 : prop.getDataBase());
        if (prop.getPassword() != null && prop.getPassword().length() > 0) {
            redisStandaloneConfiguration.setPassword(RedisPassword.of(prop.getPassword()));
        }
        JedisClientConfiguration.JedisClientConfigurationBuilder jedisClientConfiguration =
            JedisClientConfiguration.builder();
        jedisClientConfiguration.connectTimeout(Duration.ofMillis(prop.getTimeOut()));// connection timeout
        JedisConnectionFactory factory =
            new JedisConnectionFactory(redisStandaloneConfiguration, jedisClientConfiguration.build());
        LOGGER.info("分布式事务锁初始化完成:{}........................", prop);
        return factory;
    }
    //保证jedisLockConnectionFactory已被创建完成在做RedisTemplate初始化
    @DependsOn({"jedisLockConnectionFactory"})
    @Bean
    public RedisTemplate redisLockTemplate(JedisConnectionFactory jedisLockConnectionFactory) {
        RedisTemplate redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(jedisLockConnectionFactory);
        redisTemplate.setKeySerializer(new JdkSerializationRedisSerializer());
        redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

3.既然我们的工具类都已经写完了,那么需要实现我们用来做分布式事务的service,以下我们在service包下创建IRedisLockService

package io.funkye.redis.lock.starter.service;
​
import java.time.Duration;
​
/**
 * redis分布式锁实现
 * 功能不是很全哈,主要先用来实现分布式锁
 * @author funkye
 * @version 1.0.0
 */
public interface IRedisLockService {
​
    /**
     * -分布式锁实现,只有锁的key不存在才会返回true
     */
    public Boolean setIfAbsent(K key, V value, Duration timeout);
​
    void set(K key, V value, Duration timeout);
​
    Boolean delete(K key);
​
    V get(K key);
}

4.接着实现该service接口,创建RedisLockServiceImpl

package io.funkye.redis.lock.starter.service.impl;
​
import java.time.Duration;
​
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.DependsOn;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
​
import io.funkye.redis.lock.starter.service.IRedisLockService;
​
/**
 * -redis服务实现
 *
 * @author chenjianbin
 * @version 1.0.0
 */
@DependsOn({"redisLockTemplate"})
@Service("redisLockService")
public class RedisLockServiceImpl implements IRedisLockService {
​
    @Autowired
    private RedisTemplate redisLockTemplate;
​
    @Override
    public void set(K key, V value, Duration timeout) {
        redisLockTemplate.opsForValue().set(key, value, timeout);
    }
​
    @Override
    public Boolean delete(K key) {
        return redisLockTemplate.delete(key);
    }
​
    @Override
    public V get(K key) {
        return redisLockTemplate.opsForValue().get(key);
    }
​
    @Override
    public Boolean setIfAbsent(K key, V value, Duration timeout) {
        return redisLockTemplate.opsForValue().setIfAbsent(key, value, timeout);
    }
​
}

5.这下我们实现的差不多啦,接下来去config.annotation包下创建我们需要的注解类:RedisLock

package io.funkye.redis.lock.starter.config.annotation;
​
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
​
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisLock {
    /**
     * -锁值,默认为类全路径名+方法名
     */
    String key() default "";
​
    /**
     * -单位毫米,默认60秒后直接跳出
     */
    int timeoutMills() default 60000;
​
    /**
     * -尝试获取锁频率
     */
    int retry() default 50;
​
    /**
     * -锁过期时间
     */
    int lockTimeout() default 60000;
}
​

6.再从aspect包下创建:RedisClusterLockAspect类,用来实现aop切面功能,来实现分布式锁的功能

package io.funkye.redis.lock.starter.aspect;
​
import java.time.Duration;
​
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.DependsOn;
import org.springframework.stereotype.Component;
​
import io.funkye.redis.lock.starter.config.annotation.RedisLock;
import io.funkye.redis.lock.starter.service.IRedisLockService;
​
/**
 * -动态拦截分布式锁
 *
 * @author chenjianbin
 * @version 1.0.0
 */
@DependsOn({"redisLockService"})
@Aspect
@Component
public class RedisClusterLockAspect {
    private static final Logger LOGGER = LoggerFactory.getLogger(RedisClusterLockAspect.class);
​
    @Autowired
    private IRedisLockService redisLockService;
​
    @Pointcut("@annotation(io.funkye.redis.lock.starter.config.annotation.RedisLock)")
    public void annotationPoinCut() {}
​
    @Around("annotationPoinCut()")
    public void around(ProceedingJoinPoint joinPoint) throws InterruptedException {
        MethodSignature signature = (MethodSignature)joinPoint.getSignature();
        RedisLock annotation = signature.getMethod().getAnnotation(RedisLock.class);
        String key = annotation.key();
        if (key == null || key.length() <= 0) {
            key = joinPoint.getTarget().getClass().getName() + signature.getName();
        }
        Long startTime = System.currentTimeMillis();
        while (true) {
            //利用setIfAbsent特性来获取锁并上锁,设置过期时间防止死锁尝试
            if (redisLockService.setIfAbsent(key, "0", Duration.ofMillis(annotation.lockTimeout()))) {
                LOGGER.info("########## 得到锁:{} ##########", key);
                break;
            }
            if (System.currentTimeMillis() - startTime > annotation.timeoutMills()) {
                throw new RuntimeException("尝试获得分布式锁超时..........");
            }
            LOGGER.info("########## 尝试获取锁:{} ##########", key);
            Thread.sleep(annotation.retry());
        }
        try {
            return joinPoint.proceed();
        } catch (Throwable e) {
            LOGGER.error("出现异常:{}", e.getMessage());
            throw e;
        } finally {
            redisLockService.delete(key);
            LOGGER.info("########## 释放锁:{},总耗时:{}ms,{} ##########", key, (System.currentTimeMillis() - startTime));
        }
    }
}
​

7.src/main/resources下创建META-INF文件夹,在创建spring.factories(配置启动类)

org.springframework.boot.autoconfigure.EnableAutoConfiguration=io.funkye.redis.lock.starter.RedisLockAutoConfigure

注意!!!!!,如果路径不对是不会自动装载的.

8.至此我们的redis实现分布式锁项目搭建完成,直接通过mvn clean install -DskipTests=true即可引入我们的项目到自己项目去了.如下:

		
			io.funkye
			redis-lock-spring-boot-starter
			0.0.1-SNAPSHOT
		

9.引入自己项目后配置好

10.启动项目查看日志:

2020-01-18 11:43:32.732 [main] WARN  org.apache.dubbo.config.AbstractConfig -
				 [DUBBO] There's no valid metadata config found, if you are using the simplified mode of registry url, please make sure you have a metadata address configured properly., dubbo version: 2.7.4.1, current host: 192.168.14.51 
2020-01-18 11:43:32.772 [main] WARN  org.apache.dubbo.config.AbstractConfig -
				 [DUBBO] There's no valid metadata config found, if you are using the simplified mode of registry url, please make sure you have a metadata address configured properly., dubbo version: 2.7.4.1, current host: 192.168.14.51 
2020-01-18 11:43:33.800 [main] INFO  i.funkye.redis.lock.starter.RedisLockAutoConfigure -
				分布式事务锁初始化中........................ 
2020-01-18 11:43:33.848 [main] INFO  i.funkye.redis.lock.starter.RedisLockAutoConfigure -
				分布式事务锁初始化完成:JedisProperties [host=127.0.0.1, port=6379, password=123456, maxTotal=0, maxIdle=0, maxWaitMillis=0, dataBase=8, timeOut=0]........................ 
2020-01-18 11:43:34.303 [main] INFO  s.d.s.w.PropertySourcedRequestMappingHandlerMapping -
				Mapped URL path [/v2/api-docs] onto method [public org.springframework.http.ResponseEntity springfox.documentation.swagger2.web.Swagger2Controller.getDocumentation(java.lang.String,javax.servlet.http.HttpServletRequest)] 
2020-01-18 11:43:34.656 [main] INFO  o.s.scheduling.concurrent.ThreadPoolTaskExecutor -
				Initializing ExecutorService 'threadPoolTaskExecutor' 

11.使用注解并测试@RedisLock(key = "默认类路径+方法名)",timeoutMills=超时时间默认60秒,可自定义,retry=默认50毫秒重试获取锁,lockTimeout=锁过期时间,可自定义,默认60)

2020-01-18 11:45:04.503 [http-nio-0.0.0.0-28888-exec-6] INFO  i.f.r.lock.starter.aspect.RedisClusterLockAspect -
				########## key:findBannerPage,开始分布式上锁 ########## 
2020-01-18 11:45:04.512 [http-nio-0.0.0.0-28888-exec-6] INFO  i.f.r.lock.starter.aspect.RedisClusterLockAspect -
				########## 得到锁 ########## 
2020-01-18 11:45:04.540 [http-nio-0.0.0.0-28888-exec-6] INFO  i.f.r.lock.starter.aspect.RedisClusterLockAspect -
				########## 释放锁:findBannerPage,总耗时:37ms ########## 
2020-01-18 11:45:04.721 [http-nio-0.0.0.0-28888-exec-10] INFO  i.f.r.lock.starter.aspect.RedisClusterLockAspect -
				########## key:findBannerPage,开始分布式上锁 ########## 
2020-01-18 11:45:04.725 [http-nio-0.0.0.0-28888-exec-10] INFO  i.f.r.lock.starter.aspect.RedisClusterLockAspect -
				########## 得到锁 ########## 
2020-01-18 11:45:04.771 [http-nio-0.0.0.0-28888-exec-10] INFO  i.f.r.lock.starter.aspect.RedisClusterLockAspect -
				########## 释放锁:findBannerPage,总耗时:50ms ########## 
2020-01-18 11:45:04.884 [http-nio-0.0.0.0-28888-exec-3] INFO  i.f.r.lock.starter.aspect.RedisClusterLockAspect -
				########## key:findBannerPage,开始分布式上锁 ########## 
2020-01-18 11:45:04.892 [http-nio-0.0.0.0-28888-exec-3] INFO  i.f.r.lock.starter.aspect.RedisClusterLockAspect -
				########## 得到锁 ########## 
2020-01-18 11:45:04.935 [http-nio-0.0.0.0-28888-exec-3] INFO  i.f.r.lock.starter.aspect.RedisClusterLockAspect -
				########## 释放锁:findBannerPage,总耗时:51ms ########## 
2020-01-18 11:45:05.069 [http-nio-0.0.0.0-28888-exec-7] INFO  i.f.r.lock.starter.aspect.RedisClusterLockAspect -
				########## key:findBannerPage,开始分布式上锁 ########## 
2020-01-18 11:45:05.075 [http-nio-0.0.0.0-28888-exec-7] INFO  i.f.r.lock.starter.aspect.RedisClusterLockAspect -
				########## 得到锁 ########## 
2020-01-18 11:45:05.100 [http-nio-0.0.0.0-28888-exec-7] INFO  i.f.r.lock.starter.aspect.RedisClusterLockAspect -
				########## 释放锁:findBannerPage,总耗时:31ms ########## 

测试了几遍效果还不错.

 

原理分析并总结

1.原理很简单,首先上面前言已经介绍到,redis 是单线程的.

2.setIfAbsent方法是值不存在时才会返回true,利用redis单线程特性,所以获得锁只可能是一位,所以很轻松利用这个特性来实现分布式锁.如果不明白可以看下java关键字synchronized的底层实现,大概是当你去转换成汇编语言时,原理也是得到一个值0变为1,得到锁,其它的队列hold等待.

3.了解原理及一个工具的特性时,往往可以帮你节约很多时间,比如我们用aop解决了大部分横切性的问题,用反射可以很好的动态加载类,用注解可以很好的知道执行规则,执行方案等等.

你可能感兴趣的:(java,redis,spring,starter,分布式锁)