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

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

前言

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

Redis为什么这么快?

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

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

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

<!-- more-->

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项目并引入如下依赖

<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>io.funkye</groupId>
    <artifactId>redis-lock-spring-boot-starter</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>redis-lock-spring-boot-starter</name>
    <url>http://maven.apache.org</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
            <version>2.1.8.RELEASE</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.9.1</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
            <version>2.1.8.RELEASE</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <version>2.1.8.RELEASE</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
</project>

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 redis设计 spring boot redis starter_spring

进行开发

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<String, Object> redisLockTemplate(JedisConnectionFactory jedisLockConnectionFactory) {
        RedisTemplate<String, Object> 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<K, V> {

    /**
     * -分布式锁实现,只有锁的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<K, V> implements IRedisLockService<K, V> {

    @Autowired
    private RedisTemplate<K, V> 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<String, String> 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即可引入我们的项目到自己项目去了.如下:

<dependency>
			<groupId>io.funkye</groupId>
			<artifactId>redis-lock-spring-boot-starter</artifactId>
			<version>0.0.1-SNAPSHOT</version>
		</dependency>

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.spring.web.json.Json> 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解决了大部分横切性的问题,用反射可以很好的动态加载类,用注解可以很好的知道执行规则,执行方案等等.