前言
随着技术快速发展,数据规模增大,分布式系统越来越普及,一个应用往往会部署在多台机器上(多节点),在有些场景中,为了保证数据不重复,要求在同一时刻,同一任务只在一个节点上运行,即保证某一方法同一时刻只能被一个线程执行。在单机环境中,应用是在同一进程下的,只需要保证单进程多线程环境中的线程安全性,通过 JAVA 提供的 volatile、ReentrantLock、synchronized 以及 concurrent 并发包下一些线程安全的类等就可以做到。而在多机部署环境中,不同机器不同进程,就需要在多进程下保证线程的安全性了。因此,分布式锁应运而生。
常见分布式锁方案对比
分类 | 方案 | 实现原理 | 优点 | 缺点 |
---|---|---|---|---|
基于数据库 | 基于mysql 表唯一索引 | 1.表增加唯一索引 2.加锁:执行insert语句,若报错,则表明加锁失败 3.解锁:执行delete语句 | 完全利用DB现有能力,实现简单 | 1.锁无超时自动失效机制,有死锁风险 2.不支持锁重入,不支持阻塞等待 3.操作数据库开销大,性能不高 |
基于数据库 | 基于MongoDB findAndModify原子操作 | 1.加锁:执行findAndModify原子命令查找document,若不存在则新增 2.解锁:删除document | 实现也很容易,较基于MySQL唯一索引的方案,性能要好很多 | 1.大部分公司数据库用MySQL,可能缺乏相应的MongoDB运维、开发人员 2.锁无超时自动失效机制 |
基于分布式协调系统 | 基于ZooKeeper | 1.加锁:在/lock目录下创建临时有序节点,判断创建的节点序号是否最小。若是,则表示获取到锁;否,则则watch /lock目录下序号比自身小的前一个节点 2.解锁:删除节点 | 1.由zk保障系统高可用 2.Curator框架已原生支持系列分布式锁命令,使用简单 | 需单独维护一套zk集群,维保成本高 |
基于缓存 | 基于redis命令 | 1. 加锁:执行setnx,若成功再执行expire添加过期时间 2. 解锁:执行delete命令 | 实现简单,相比数据库和分布式系统的实现,该方案最轻,性能最好 | 1.setnx和expire分2步执行,非原子操作;若setnx执行成功,但expire执行失败,就可能出现死锁 2.delete命令存在误删除非当前线程持有的锁的可能 3.不支持阻塞等待、不可重入 |
基于缓存 | 基于redis Lua脚本能力 | 1. 加锁:执行SET lock_name random_value EX seconds NX 命令 2. 解锁:执行Lua脚本,释放锁时验证random_value – ARGV[1]为random_value, KEYS[1]为lock_nameif redis.call(“get”, KEYS[1]) == ARGV[1] then return redis.call(“del”,KEYS[1])else return 0end | 同上;实现逻辑上也更严谨,除了单点问题,生产环境采用用这种方案,问题也不大。 | 不支持锁重入,不支持阻塞等待 |
表格中对比了几种常见的方案,redis+lua基本可应付工作中分布式锁的需求。然而,redisson分布式锁实现方案(传送门),相比以上方案,redisson保持了简单易用、支持锁重入、支持阻塞等待、Lua脚本原子操作,具有明显的优势。
分布式锁需满足四个条件
首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
- 互斥性。在任意时刻,只有一个客户端能持有锁。
- 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
- 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了,即不能误解锁。
- 具有容错性。只要大多数Redis节点正常运行,客户端就能够获取和释放锁。
Redisson实现分布式锁
Redisson 是什么?
如果你之前是在用 Redis 的话,那使用 Redisson 的话将会事半功倍,Redisson 提供了使用 Redis 的最简单和最便捷的方法。
Redisson 的宗旨是促进使用者对 Redis 的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。
Redisson 是一个在 Redis 的基础上实现的 Java 驻内存数据网格(In-Memory Data Grid)。
- Netty 框架:Redisson 采用了基于 NIO 的Netty框架,不仅能作为 Redis 底层驱动客户端,具备提供对 Redis 各种组态形式的连接功能,对 Redis 命令能以同步发送、异步形式发送、异步流形式发送或管道形式发送的功能,LUA脚本执行处理,以及处理返回结果的功能
- 基础数据结构:将原生的 Redis
Hash
,List
,Set
,String
,Geo
,HyperLogLog
等数据结构封装为 Java 里大家最熟悉的映射(Map)
,列表(List)
,集(Set)
,通用对象桶(Object Bucket)
,地理空间对象桶(Geospatial Bucket)
,基数估计算法(HyperLogLog)
等结构, - 分布式数据结构:这基础上还提供了分布式的
多值映射(Multimap)
,本地缓存映射(LocalCachedMap)
,有序集(SortedSet)
,计分排序集(ScoredSortedSet)
,字典排序集(LexSortedSet)
,列队(Queue)
,阻塞队列(Blocking Queue)
,有界阻塞列队(Bounded Blocking Queue)
,双端队列(Deque)
,阻塞双端列队(Blocking Deque)
,阻塞公平列队(Blocking Fair Queue)
,延迟列队(Delayed Queue)
,布隆过滤器(Bloom Filter)
,原子整长形(AtomicLong)
,原子双精度浮点数(AtomicDouble)
,BitSet
等 Redis 原本没有的分布式数据结构。 - 分布式锁:Redisson 还实现了 Redis文档中提到像分布式锁
Lock
这样的更高阶应用场景。事实上 Redisson 并没有不止步于此,在分布式锁的基础上还提供了联锁(MultiLock)
,读写锁(ReadWriteLock)
,公平锁(Fair Lock)
,红锁(RedLock)
,信号量(Semaphore)
,可过期性信号量(PermitExpirableSemaphore)
和闭锁(CountDownLatch)
这些实际当中对多线程高并发应用至关重要的基本部件。正是通过实现基于 Redis 的高阶应用方案,使 Redisson 成为构建分布式系统的重要工具。 - 节点:Redisson 作为独立节点可以用于独立执行其他节点发布到
分布式执行服务
和分布式调度服务
里的远程任务。
原理机制
集成Spring Boot 项目
-
引入依赖 【可引入Spring Boot 封装好的starter】
<!-- https://mvnrepository.com/artifact/org.redisson/redisson --> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.5.4</version> </dependency>
-
添加配置类
@Configuration public class MyRedissonConfig { @Bean(destroyMethod = "shutdown") public RedissonClient redissonClient(){ // 创建配置 记得加redis:// Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379"); // 根据配置创建RedissClient客户端 RedissonClient redissonClient = Redisson.create(config); return redissonClient; } }
基于 AOP 的 Redis 分布式锁
在实际的使用过程中,分布式锁可以封装好后使用在方法级别,这样就不用每个地方都去获取锁和释放锁,使用起来更加方便。
首先定义个注解:
package com.tom.lock.redisson.spring.boot.autoconfigure;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;
import org.springframework.core.annotation.AliasFor;
/**
* @author handsometong
* @date 2022年11月14日 下午3:10:36
* @version 1.0.0
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface LockAction {
/** 锁的资源,key。支持spring El表达式*/
@AliasFor("key")
String value() default "'default'";
@AliasFor("value")
String key() default "'default'";
/** 锁类型*/
LockType lockType() default LockType.REENTRANT_LOCK;
/** 获取锁等待时间,默认3秒*/
long waitTime() default 3000L;
/** 锁自动释放时间,默认30秒*/
long leaseTime() default 30000L;
/** 时间单位(获取锁等待时间和持锁时间都用此单位)*/
TimeUnit unit() default TimeUnit.MILLISECONDS;
}
定义切面(spring boot配置方式)
package com.tom.lock.redisson.spring.boot.autoconfigure;
import java.lang.reflect.Method;
import com.tom.redisson.spring.boot.autoconfigure.RedissonAutoConfiguration;
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.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
/**
* @author handsometong
* @date 2022年11月14日 下午3:11:22
* @version 1.0.0
*/
@Aspect
@Configuration
@ConditionalOnBean(RedissonClient.class)
@AutoConfigureAfter(RedissonAutoConfiguration.class)
public class RedissonDistributedLockAspectConfiguration {
private final Logger logger = LoggerFactory.getLogger(RedissonDistributedLockAspectConfiguration.class);
@Autowired
private RedissonClient redissonClient;
private ExpressionParser parser = new SpelExpressionParser();
private LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
@Pointcut("@annotation(com.tom.lock.redisson.spring.boot.autoconfigure.LockAction)")
private void lockPoint(){
}
@Around("lockPoint()")
public Object around(ProceedingJoinPoint pjp) throws Throwable{
Method method = ((MethodSignature) pjp.getSignature()).getMethod();
LockAction lockAction = method.getAnnotation(LockAction.class);
String key = lockAction.value();
Object[] args = pjp.getArgs();
key = parse(key, method, args);
RLock lock = getLock(key, lockAction);
if(!lock.tryLock(lockAction.waitTime(), lockAction.leaseTime(), lockAction.unit())) {
logger.debug("get lock failed [{}]", key);
return null;
}
//得到锁,执行方法,释放锁
logger.debug("get lock success [{}]", key);
try {
return pjp.proceed();
} catch (Exception e) {
logger.error("execute locked method occured an exception", e);
} finally {
lock.unlock();
logger.debug("release lock [{}]", key);
}
return null;
}
/**
* @description 解析spring EL表达式
* @author handsometong
* @date 2022年11月9日 上午10:41:01
* @version 1.0.0
* @param key 表达式
* @param method 方法
* @param args 方法参数
* @return
*/
private String parse(String key, Method method, Object[] args) {
String[] params = discoverer.getParameterNames(method);
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);
}
private RLock getLock(String key, LockAction lockAction) {
switch (lockAction.lockType()) {
case REENTRANT_LOCK:
return redissonClient.getLock(key);
case FAIR_LOCK:
return redissonClient.getFairLock(key);
case READ_LOCK:
return redissonClient.getReadWriteLock(key).readLock();
case WRITE_LOCK:
return redissonClient.getReadWriteLock(key).writeLock();
default:
throw new RuntimeException("do not support lock type:" + lockAction.lockType().name());
}
}
}
spring boot starter还需要在 resources/META-INF 中添加 spring.factories 文件
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.tom.lock.redisson.spring.boot.autoconfigure.RedissonDistributedLockAspectConfiguration
使用方法
直接在方法上增加 @LockAction注解 (支持spring El表达式)**
LockAction("'test'.concat(#user.id)")
public void update(UserVO user){
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
logger.error("exp", e);
}
}
源码地址 :https://github.com/handsometong/redission-lock.git
redis分布式锁源码:https://github.com/handsometong/redis-lock
参考文献: