这个文章分为3篇:一、实现一个redis的分布式锁;二、基于分布式锁实现一个spring-boot-starter系列;三、spring-boot-starter的例子
1.新建工程redis-distributed-lock-core
此工程是基于单机模式的分布式锁,主要是基于redis的setnx
命令实现的。
pom.xml文件如下:
<?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">
<parent>
<artifactId>redis-distributed-lock</artifactId>
<groupId>com.snowalker</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.snowalker</groupId>
<artifactId>redis-distributed-lock-core</artifactId>
<version>1.0-SNAPSHOT</version>
<name>redis-distributed-lock-core</name>
<!-- FIXME change it to the project's website -->
<url>http://www.example.com</url>
<properties>
<aspectjweaver>1.8.10</aspectjweaver>
<aspecjrt>1.8.10</aspecjrt>
<apache-common-lang3>3.4</apache-common-lang3>
<google-guava>20.0</google-guava>
<jedis>2.9.0</jedis>
<slf4j-api>1.7.25</slf4j-api>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<!-- aspectjrt -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${aspecjrt}</version>
</dependency>
<!-- aspectjweaver -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>${aspectjweaver}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${apache-common-lang3}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${google-guava}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>${jedis}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j-api}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.18.RELEASE</version>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-core -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>4.3.18.RELEASE</version>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.5.4</version>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
<plugins>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.0.0</version>
</plugin>
<!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_jar_packaging -->
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.20.1</version>
</plugin>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>2.5.2</version>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
工具类PropertiesUtil.java
package com.snowalker.util;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Properties;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author snowalker
* @date 2017-9-21
* @describe 配置文件读取
*/
public class PropertiesUtil {
private static final Logger log = LoggerFactory.getLogger(PropertiesUtil.class);
private static Properties props;
static {
String fileName = "redis.properties";
props = new Properties();
try {
props.load(new InputStreamReader(PropertiesUtil.class.getClassLoader().getResourceAsStream(fileName),"UTF-8"));
} catch (IOException e) {
log.error("配置文件读取异常",e);
}
}
public static String getProperty(String key){
String value = props.getProperty(key.trim());
if(StringUtils.isBlank(value)){
return null;
}
return value.trim();
}
public static String getProperty(String key,String defaultValue){
String value = props.getProperty(key.trim());
if(StringUtils.isBlank(value)){
value = defaultValue;
}
return value.trim();
}
}
2.主要功能如下:
RedisLock
类是一个注解类,主要是想将来基于注解去调用这个分布式锁,比较方便。
package com.snowalker.annotation;
import java.lang.annotation.*;
/**
* @author snowalker
* @date 2018-7-9
* @desc redisLock自定义注解
*/
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface RedisLock {
String value() default "redis-lock";
}
RedisPool
类主要是基于JedisPool
实现的连接池,主要是配置一些属性,比如最大连接数,最大空闲连接数等。
package com.snowalker.config;
import com.snowalker.util.PropertiesUtil;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
/**
* @author snowalker
* @date 2018-7-9
* @desc Redis单机模式连接池
*/
public class RedisPool {
/**jedis连接池*/
private static JedisPool pool;
/**最大连接数*/
private static Integer maxTotal;
/**最大空闲连接数,在jedispool中最大的idle状态(空闲的)的jedis实例的个数*/
private static Integer maxIdle;
/**最小空闲连接数,在jedispool中最小的idle状态(空闲的)的jedis实例的个数*/
private static Integer minIdle;
/**在取连接时测试连接的可用性,在borrow一个jedis实例的时候,是否要进行验证操作,如果赋值true。则得到的jedis实例肯定是可以用的。*/
private static Boolean testOnBorrow;
/**再还连接时不测试连接的可用性,在return一个jedis实例的时候,是否要进行验证操作,如果赋值true。则放回jedispool的jedis实例肯定是可以用的。*/
private static Boolean testOnReturn;
/**redis服务端ip*/
private static String redisIp;
/**redis服务端port*/
private static Integer redisPort;
/**redis连接超时时间*/
private static Integer redisTimeout;
static {
init();
}
private static void init() {
maxTotal = Integer.parseInt(PropertiesUtil.getProperty("redis.max.total", "20"));
maxIdle = Integer.parseInt(PropertiesUtil.getProperty("redis.max.idle","20"));
minIdle = Integer.parseInt(PropertiesUtil.getProperty("redis.min.idle","10"));
testOnBorrow = Boolean.parseBoolean(PropertiesUtil.getProperty("redis.test.borrow","true"));
testOnReturn = Boolean.parseBoolean(PropertiesUtil.getProperty("redis.test.return","true"));
redisTimeout = Integer.valueOf(PropertiesUtil.getProperty("redis.server.timeout", "3000"));
redisIp = PropertiesUtil.getProperty("redis.ip");
if (redisIp == null) {
throw new RuntimeException("请检查redis服务端ip配置项redis.ip是否配置");
}
redisPort = Integer.parseInt(PropertiesUtil.getProperty("redis.port"));
if (redisPort == null) {
throw new RuntimeException("请检查redis服务端port配置项redis.port是否配置");
}
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(maxTotal);
config.setMaxIdle(maxIdle);
config.setMinIdle(minIdle);
config.setTestOnBorrow(testOnBorrow);
config.setTestOnReturn(testOnReturn);
/**连接耗尽的时候,是否阻塞,false会抛出异常,true阻塞直到超时。默认为true*/
config.setBlockWhenExhausted(true);
pool = new JedisPool(config, redisIp, redisPort, redisTimeout);
}
public static Jedis getJedis(){
return pool.getResource();
}
public static void close(Jedis jedis){
jedis.close();
}
public static void returnBrokenResource(Jedis jedis){
pool.returnBrokenResource(jedis);
}
public static void returnResource(Jedis jedis){
pool.returnResource(jedis);
}
}
RedisPoolUtil
类主要实现对RedisPool
类的操作,比如设置,删除等操作。
package com.snowalker.config;
import redis.clients.jedis.Jedis;
/**
* @author snowalker
* @date 2018-7-9
* @desc 封装单机版Jedis工具类
*/
public class RedisPoolUtil {
private RedisPoolUtil(){}
private static RedisPool redisPool;
public static String get(String key){
Jedis jedis = null;
String result = null;
try {
jedis = RedisPool.getJedis();
result = jedis.get(key);
} catch (Exception e){
e.printStackTrace();
} finally {
if (jedis != null) {
jedis.close();
}
return result;
}
}
public static Long setnx(String key, String value){
Jedis jedis = null;
Long result = null;
try {
jedis = RedisPool.getJedis();
result = jedis.setnx(key, value);
} catch (Exception e){
e.printStackTrace();
} finally {
if (jedis != null) {
jedis.close();
}
return result;
}
}
public static String getSet(String key, String value){
Jedis jedis = null;
String result = null;
try {
jedis = RedisPool.getJedis();
result = jedis.getSet(key, value);
} catch (Exception e){
e.printStackTrace();
} finally {
if (jedis != null) {
jedis.close();
}
return result;
}
}
public static Long expire(String key, int seconds){
Jedis jedis = null;
Long result = null;
try {
jedis = RedisPool.getJedis();
result = jedis.expire(key, seconds);
} catch (Exception e){
e.printStackTrace();
} finally {
if (jedis != null) {
jedis.close();
}
return result;
}
}
public static Long del(String key){
Jedis jedis = null;
Long result = null;
try {
jedis = RedisPool.getJedis();
result = jedis.del(key);
} catch (Exception e){
e.printStackTrace();
} finally {
if (jedis != null) {
jedis.close();
}
return result;
}
}
}
lock包:主要是基于jedis和redisson实现的分布式锁,这里主要以jedis实现为例,具体的可以参考代码。
DistributedLock
接口主要定义了加锁,释放锁的方法,实现交给了RedisDistributedLock
子类。
package com.snowalker.lock;
/**
* @author snowalker
* @date 2018-7-9
* @desc Redis分布式锁接口声明
*/
public interface DistributedLock {
/**
* 加锁
* @param lockName
* @return
*/
boolean lock(String lockName);
/**
* 解锁
* @param lockName
*/
boolean release(String lockName);
}
RedisDistributedLock
具体实现如下:
package com.snowalker.lock;
import com.snowalker.config.RedisPoolUtil;
import com.snowalker.util.PropertiesUtil;
import com.sun.org.apache.bcel.internal.generic.IF_ACMPEQ;
import com.sun.org.apache.regexp.internal.RE;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author snowalker
* @date 2018-7-9
* @desc redis分布式锁核心实现
*/
public class RedisDistributedLock implements DistributedLock {
/**默认锁超时时间为10S*/
private static final int EXPIRE_SECONDS = 50;
private static final Logger log = LoggerFactory.getLogger(RedisDistributedLock.class);
private RedisDistributedLock() {
}
private volatile static RedisDistributedLock redisDistributedLock;
// 使用了懒汉式单例模式
public static RedisDistributedLock getInstance() {
if (redisDistributedLock == null) {
synchronized (RedisDistributedLock.class) {
redisDistributedLock = new RedisDistributedLock();
}
}
return redisDistributedLock;
}
/**
* 加锁
*
* @param lockName
* @return 返回true表示加锁成功,执行业务逻辑,执行完毕需要主动释放锁,否则就需要等待锁超时重新争抢
* 返回false标识加锁失败,阻塞并继续尝试获取锁
*/
@Override
public boolean lock(String lockName) {
// 1.使用setNx开始加锁
log.info("开始获取Redis分布式锁流程,lockName={},CurrentThreadName={}", lockName, Thread.currentThread().getName());
long lockTimeout = Long.parseLong(PropertiesUtil.getProperty("redis.lock.timeout", "5"));
// redis中锁的值为:当前时间+超时时间
Long lockResult = RedisPoolUtil.setnx(lockName, String.valueOf(System.currentTimeMillis() + lockTimeout));
if (lockResult != null && lockResult.intValue() == 1){
log.info("setNx获取分布式锁[成功],threadName={}", Thread.currentThread().getName());
RedisPoolUtil.expire(lockName,EXPIRE_SECONDS);
return true;
}else {
log.info("setNx获取分布式锁[失败],threadName={}", Thread.currentThread().getName());
return false;
}
}
private boolean tryLock(String lockName, long lockTimeout) {
/**
* 2.加锁失败后再次尝试
* 2.1 获取锁失败,继续判断,判断时间戳,看是否可以重置并获取锁
* setNx 结果小于当前时间,表明锁已过期,可以再次尝试加锁
*/
String lockValueStr = RedisPoolUtil.get(lockName);
Long lockValueATime = Long.parseLong(lockValueStr);
log.info("lockValueATime为:" + lockValueATime);
if (lockValueStr != null && lockValueATime < System.currentTimeMillis()){
/**
* 2.2 再次用当前时间戳getset--->将给定 key 的值设为 value, 并返回 key 的旧值(old value)
* 通过getset 重设锁对应的值:新的当前时间+超时时间,并返回旧的锁对应值
*/
String getSetResult = RedisPoolUtil.getSet(lockName, String.valueOf(System.currentTimeMillis() + lockTimeout));
log.info("lockValueBTime为:" + Long.parseLong(getSetResult));
if (getSetResult == null || (getSetResult != null && StringUtils.equals(lockValueStr, getSetResult))) {
/**
* 2.3 旧值判断,是否可以获取锁
* 当key没有旧值时,即key不存在时,返回nil -> 获取锁,设置锁过期时间
*/
log.info("获取Redis分布式锁[成功],lockName={},CurrentThreadName={}",
lockName, Thread.currentThread().getName());
RedisPoolUtil.expire(lockName, EXPIRE_SECONDS);
return true;
}else{
log.info("获取锁失败,lockName={},CurrentThreadName={}",
lockName, Thread.currentThread().getName());
return false;
}
}else {
/**3.锁未超时,获取锁失败*/
log.info("当前锁未失效!!!!,竞争失败,继续持有之前的锁,lockName={},CurrentThreadName={}",
lockName, Thread.currentThread().getName());
return false;
}
}
/**
* 解锁
*
* @param lockName
*/
@Override
public boolean release(String lockName) {
Long result = RedisPoolUtil.del(lockName);
if (result != null && result.intValue() == 1) {
log.info("删除Redis分布式锁成功,锁已释放, key= :{}", lockName);
return true;
}
log.info("删除Redis分布式锁失败,锁未释放, key= :{}", lockName);
return false;
}
}
3.特色:主要是基于AOP编程实现了一个解析Handler
RedisLockHandler
类使用环绕切的切面方式。
package com.snowalker.handler;
import com.snowalker.annotation.RedisLock;
import com.snowalker.lock.RedisDistributedLock;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* @author snowalker
* @date 2018-7-9
* @desc Redis分布式锁注解解析器
*/
@Aspect
@Component
public class RedisLockHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(RedisLockHandler.class);
// 切点就是注解类
@Pointcut("@annotation(com.snowalker.annotation.RedisLock)")
public void redisLock(){}
@Around("@annotation(redisLock)")
public void around(ProceedingJoinPoint joinPoint, RedisLock redisLock) {
LOGGER.info("[开始]执行RedisLock环绕通知,获取Redis分布式锁开始");
String lockName = redisLock.value();
RedisDistributedLock redisDistributedLock = RedisDistributedLock.getInstance();
if (redisDistributedLock.lock(lockName)){
try {
LOGGER.info("获取Redis分布式锁[成功]");
joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
LOGGER.info("释放Redis分布式锁[成功]");
redisDistributedLock.release(lockName);
}else{
LOGGER.error("获取Redis分布式锁[失败]");
}
LOGGER.error("[结束]执行RedisLock环绕通知");
}
}
这一步就算完成了,只是简单的实现了功能。