惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

MyScale Blog
MyScale Blog
博客园 - 叶小钗
小众软件
小众软件
宝玉的分享
宝玉的分享
Apple Machine Learning Research
Apple Machine Learning Research
Jina AI
Jina AI
Last Week in AI
Last Week in AI
量子位
腾讯CDC
罗磊的独立博客
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - Franky
博客园 - 【当耐特】
Hugging Face - Blog
Hugging Face - Blog
有赞技术团队
有赞技术团队
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
V
Visual Studio Blog
爱范儿
爱范儿
月光博客
月光博客
博客园 - 三生石上(FineUI控件)
雷峰网
雷峰网
V
V2EX
WordPress大学
WordPress大学
Google DeepMind News
Google DeepMind News
阮一峰的网络日志
阮一峰的网络日志
K
Kaspersky official blog
Vercel News
Vercel News
L
LINUX DO - 热门话题
The Hacker News
The Hacker News
The Register - Security
The Register - Security
IT之家
IT之家
C
Cybersecurity and Infrastructure Security Agency CISA
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
人人都是产品经理
人人都是产品经理
Martin Fowler
Martin Fowler
S
SegmentFault 最新的问题
N
News | PayPal Newsroom
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Attack and Defense Labs
Attack and Defense Labs
S
Securelist
T
The Blog of Author Tim Ferriss
GbyAI
GbyAI
Microsoft Security Blog
Microsoft Security Blog
H
Hacker News: Front Page
博客园 - 聂微东
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Project Zero
Project Zero
I
InfoQ
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
PCI Perspectives
PCI Perspectives

CAYZLH

英雄联盟的昨日今天与符文大乱斗 折腾的尽头是极简 MacBook上备忘录APP的快捷键和手势 让Google屏蔽某些搜索结果 WSL自定义安装Ubuntu typora自动上传图片配置 自建giscus服务 Windows下结束指定端口的进程 批量修改maven多模块版本号 使用winsw部署SpringBoot项目 Gson简易指南 docker搭建Consul集群 使用Docker部署SpringBoot项目 Docker使用redis镜像 Docker使用rabbitmq Docker使用zookeeper Docker使用MySQL Dockerfile常用指令 Docker免sudo操作 雷鸟电视去广告 使用ADB卸载MIUI系统应用 利用GitHub做图床 将网站变成灰色 Github上传大文件 Android远程调试命令adb vscode快捷键的使用 SpringBoot文件上传异常处理 SpringBoot封装JedisUtils工具类 SpringBoot自动部署脚本 使用Spring读取文件的几种方式 Spring统一异常返回 利用Github做Maven私服 Redis常用指令 Maven常用指令 使用mysqldump导出数据 SpringBoot动态切换多数据源 Java8的日期处理实践 lambda表达式语法 Stream表达式语法 使用Optional优雅地判空 Android Support vs AndroidX 「Vue」Runtime-Complier和Runtime-only的区别 RecyclerView使用记录 动态设置布局之LayoutInflater Redis分布式锁的几种方案 打造一个舒服的写作环境(Hexo) 深入了解与使用ThreadLocal Maven中dependencyManagement的作用 Redis配置认证密码 nginx简单配置示例 Linux(macOS)换源 Linux搭建Git服务器 Linux配置ssh使用公钥登录远程服务器 Linux下find与exec的使用 Linux排查Java问题工具单 Linux常用命令 MySql数据库优化细节 API签名验证方案 Java集合框架 Redis基础知识总结 Spring事务管理 Redis为什么这么快 SpringBoot实现Jwt单点登录 Spring面向切面编程(知识梳理) SpringBoot异步请求和异步调用 Mybatis中用到的几种设计模式 Docker Swarm RocketMQ相关流程图/原理图 SonarQube的安装与使用 宝塔面板部署vue项目 Tomcat的三种接收请求处理方式 Docker知识扫盲 安装Jenkins并用于部署SpringBoot项目 Tomcat单机多实例部署 Maven中Scope的分类 Springboot集成Shiro(前后端分离) Linux搭建frp服务(内网穿透) Redis高逼格指令 Docker私有仓库的搭建与使用 Dockerfile使用介绍 Redis的使用场景 Docker的网络模式
SpringBoot实现Redis分布式锁
Ant丶 · 2020-05-30 · via CAYZLH

最近项目中有用到redis实现的分布式锁, 但是胆码写起来比较繁琐, 就想着整一套注解的方式实现的分布式锁

前言

分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁。本文介绍基于Redis实现分布式锁。

为什么需要分布式锁

在单机时代,虽然不需要分布式锁,但也面临过类似的问题,只不过在单机的情况下,如果有多个线程要同时访问某个共享资源的时候,我们可以采用线程间加锁的机制,即当某个线程获取到这个资源后,就立即对这个资源进行加锁,当使用完资源之后,再解锁,其它线程就可以接着使用了。例如,在JAVA中,甚至专门提供了一些处理锁机制的一些API(synchronize/Lock等)。

但是到了分布式系统的时代,这种线程之间的锁机制,就没作用了,系统可能会有多份并且部署在不同的机器上,这些资源已经不是在线程之间共享了,而是属于进程之间共享的资源。

因此,为了解决这个问题,我们就必须引入「分布式锁」。

分布式锁,是指在分布式的部署环境下,通过锁机制来让多客户端互斥的对共享资源进行访问。

分布式锁要满足哪些要求呢?

  • 排他性:在同一时间只会有一个客户端能获取到锁,其它客户端无法同时获取
  • 避免死锁:这把锁在一段有限的时间之后,一定会被释放(正常释放或异常释放)
  • 高可用:获取或释放锁的机制必须高可用且性能佳

可靠性(From)

首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:

  1. 互斥性。在任意时刻,只有一个客户端能持有锁。
  2. 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
  3. 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
  4. 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了

原理

基于Redis实现的锁机制,主要是依赖Redis自身的原子操作,例如:

SET user_key user_value NX PX 100

redis从2.6.12版本开始,SET命令才支持这些参数:
NX:只在在键不存在时,才对键进行设置操作,SET key value NX 效果等同于 SETNX key value
PX millisecond:设置键的过期时间为millisecond毫秒,当超过这个时间后,设置的键会自动失效

上述代码示例是指,
redis中不存在user_key这个键的时候,才会去设置一个user_key键,并且给这个键的值设置为 user_value,且这个键的存活时间为100ms

为什么这个命令可以帮我们实现锁机制呢?
因为这个命令是只有在某个key不存在的时候,才会执行成功。那么当多个进程同时并发的去设置同一个key的时候,就永远只会有一个进程成功。
当某个进程设置成功之后,就可以去执行业务逻辑了,等业务逻辑执行完毕之后,再去进行解锁。

解锁很简单,只需要删除这个key就可以了,不过删除之前需要判断,这个key对应的value是当初自己设置的那个。

另外,针对redis集群模式的分布式锁,可以采用redis的Redlock机制。

实现

创建一个SpringBoot工程

修改pom.xml文件, 添加如下依赖包:

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j</artifactId>
<version>1.3.8.RELEASE</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
<version>1.4.7.RELEASE</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

定义一个注解类

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface DistributeLock {





@AliasFor("name")
String name() default "'default'";





@AliasFor("value")
String value() default "'default'";




long keepMills() default 5000;




LockFailAction action() default LockFailAction.CONTINUE;

public enum LockFailAction{

GIVEUP,

CONTINUE;
}




long sleepMills() default 200;




int retryTimes() default 5;

}

定义接口

public interface IDistributedLock {
public static final long TIMEOUT_MILLIS = 5000;

public static final int RETRY_TIMES = Integer.MAX_VALUE;

public static final long SLEEP_MILLIS = 500;

public boolean lock(String key);

public boolean lock(String key, int retryTimes);

public boolean lock(String key, int retryTimes, long sleepMillis);

public boolean lock(String key, long expire);

public boolean lock(String key, long expire, int retryTimes);

public boolean lock(String key, long expire, int retryTimes, long sleepMillis);

public boolean releaseLock(String key);
}

定义抽象类

public abstract class AbstractDistributedLockImpl implements IDistributedLock {

@Override
public boolean lock(String key) {
return lock(key, TIMEOUT_MILLIS, RETRY_TIMES, SLEEP_MILLIS);
}

@Override
public boolean lock(String key, int retryTimes) {
return lock(key, TIMEOUT_MILLIS, retryTimes, SLEEP_MILLIS);
}

@Override
public boolean lock(String key, int retryTimes, long sleepMillis) {
return lock(key, TIMEOUT_MILLIS, retryTimes, sleepMillis);
}

@Override
public boolean lock(String key, long expire) {
return lock(key, expire, RETRY_TIMES, SLEEP_MILLIS);
}

@Override
public boolean lock(String key, long expire, int retryTimes) {
return lock(key, expire, retryTimes, SLEEP_MILLIS);
}

}

定义Redis分布式锁实现类

public class RedisDistributedLock extends AbstractDistributedLockImpl {

private static final Logger logger = getLogger(RedisDistributedLock.class);

private RedisTemplate<Object, Object> redisTemplate;

private ThreadLocal<String> lockFlag = new ThreadLocal<>();

private static final String UNLOCK_LUA;

private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";

static {
UNLOCK_LUA = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
}

public RedisDistributedLock(RedisTemplate<Object, Object> redisTemplate) {
super();
this.redisTemplate = redisTemplate;
}

@Override
public boolean lock(String key, long expire, int retryTimes, long sleepMillis) {
boolean result = setRedis(key, expire);

while((!result) && retryTimes-- > 0){
try {
logger.debug("lock failed, retrying..." + retryTimes);
Thread.sleep(sleepMillis);
} catch (InterruptedException e) {
return false;
}
result = setRedis(key, expire);
}
return result;
}

@Override
public boolean releaseLock(String key) {

try {
List<String> keys = new ArrayList<>();
keys.add(key);
List<String> args = new ArrayList<>();
args.add(lockFlag.get());




Long result = redisTemplate.execute((RedisCallback<Long>) redisConnection -> {
Object nativeConnection = redisConnection.getNativeConnection();


if (nativeConnection instanceof JedisCluster) {
return (Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA, keys, args);
}


else if (nativeConnection instanceof Jedis) {
return (Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA, keys, args);
}
return 0L;
});

return result != null && result > 0;
} catch (Exception e) {
logger.error("release lock occured an exception", e);
} finally {

lockFlag.remove();
}
return false;
}

private boolean setRedis(String key, long expire) {
try {
String result = redisTemplate.execute((RedisCallback<String>) redisConnection -> {
JedisCommands commands = (JedisCommands) redisConnection.getNativeConnection();
String uuid = UUID.randomUUID().toString();
lockFlag.set(uuid);
return commands.set(key, uuid, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expire);
});
return !StringUtils.isEmpty(result);
} catch (Exception e) {
logger.error("set redis occured an exception", e);
}
return false;
}

}

装配DistributeLock

@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class DistributedLockAutoConfiguration {

@Bean
@ConditionalOnBean(RedisTemplate.class)
public IDistributedLock redisDistributedLock(RedisTemplate<Object, Object> redisTemplate){
return new RedisDistributedLock(redisTemplate);
}

}

定义切面

@Aspect
@Configuration
@ConditionalOnClass(IDistributedLock.class)
@AutoConfigureAfter(DistributedLockAutoConfiguration.class)
public class DistributedLockAspectConfiguration {

private static final Logger logger = getLogger(DistributedLockAspectConfiguration.class);

@Autowired
private IDistributedLock distributedLock;

private ExpressionParser parser = new SpelExpressionParser();

private LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();




@Pointcut("@annotation(com.cayzlh.distributedlock.annotations.DistributeLock)")
private void lockPoint() {
}








@Around("lockPoint()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Method method = ((MethodSignature) pjp.getSignature()).getMethod();
DistributeLock lockAction = method.getAnnotation(DistributeLock.class);
String logKey = getLogKey(lockAction, pjp, method);

int retryTimes = lockAction.action().equals(DistributeLock.LockFailAction.CONTINUE) ? lockAction.retryTimes() : 0;
boolean lock = distributedLock.lock(logKey, lockAction.keepMills(), retryTimes, lockAction.sleepMills());
if (!lock) {
logger.debug("get lock failed : " + logKey);
return null;
}


logger.debug("get lock success : " + logKey);
try {
return pjp.proceed();
} catch (Exception e) {
logger.error("execute locked method occured an exception", e);
} finally {
boolean releaseResult = distributedLock.releaseLock(logKey);
logger.debug("release lock : " + logKey + (releaseResult ? " success" : " failed"));
}
return null;
}









private String getLogKey(DistributeLock lockAction, ProceedingJoinPoint pjp, Method method) {
String name = lockAction.name();
String value = lockAction.value();
Object[] args = pjp.getArgs();
return parse(name, method, args) + "_" + parse(value, method, args);
}









private String parse(String key, Method method, Object[] args) {
String[] params = discoverer.getParameterNames(method);
if (null == params || params.length == 0 || !key.contains("#")) {
return key;
}
EvaluationContext context = new StandardEvaluationContext();
for (int i = 0; i < params.length; i++) {
context.setVariable(params[i], args[i]);
}
return parser.parseExpression(key).getValue(context, String.class);
}

}

配置文件

server.port=8080

spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.jedis.pool.max-idle=8
spring.redis.jedis.pool.min-idle=0
spring.redis.jedis.pool.max-active=8
spring.redis.jedis.pool.max-wait=-1ms
spring.redis.timeout=20ms
spring.redis.password=

配置log4j配置文件


server=

logFilePath=logs
log4j.rootCategory=DEBUG,stdout,debugLog,infoLog,errorLog


log4j.logger.consoleLogger=stdout

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Threshold=DEBUG
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=[%p] %d %c - %m%n
log4j.appender.stdout.ImmediateFlush=true


log4j.logger.debugLog=DEBUG, debugLog

log4j.appender.debugLog=org.apache.log4j.DailyRollingFileAppender
log4j.appender.debugLog.File=${logFilePath}/debug.log
log4j.appender.debugLog.layout=org.apache.log4j.PatternLayout
log4j.appender.debugLog.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS} %5p %c{1}:%L - %m%n
log4j.appender.debugLog.DatePattern='.'yyyy-MM-dd
log4j.appender.debugLog.ImmediateFlush=true
log4j.appender.debugLog.Threshold=DEBUG
log4j.appender.debugLog.encoding=UTF-8
log4j.appender.debugLog.filter.debugFilter=org.apache.log4j.varia.LevelRangeFilter
log4j.appender.debugLog.filter.debugFilter.LevelMin=DEBUG
log4j.appender.debugLog.filter.debugFilter.LevelMax=DEBUG

源码在这.