本文作者:FUNKYE(陈健斌),杭州某互联网公司主程。
demo项目地址
Redis 是完全开源免费的,遵守BSD协议,是一个高性能的key-value数据库。
(一)纯内存操作,避免大量访问数据库,减少直接读取磁盘数据,redis 将数据储存在内存里面,读写数据的时候都不会受到硬盘 I/O 速度的限制,所以速度快;
(二)单线程操作,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
(三)采用了非阻塞I/O多路复用机制
性能极高 – 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.0
io.funkye
redis-lock-spring-boot-starter
0.0.1-SNAPSHOT
jar
redis-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增强)
如果大家创建好了,参照下图即可:
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解决了大部分横切性的问题,用反射可以很好的动态加载类,用注解可以很好的知道执行规则,执行方案等等.