文章目录
概念
分布式锁是一种在分布式系统中协调多个进程或线程对共享资源进行访问的同步机制。它的主要作用是保证在分布式环境下,对共享资源的访问具有互斥性,即同一时刻只有一个客户端能够获取到锁并访问共享资源,其他客户端需要等待锁释放后才能继续尝试获取锁
特点
- 互斥性:这是分布式锁最核心的特性,同一时刻只能有一个客户端持有锁,确保对共享资源的访问是排他的,避免多个客户端同时修改共享资源导致的数据不一致问题。
- 可重入性:允许同一个客户端在持有锁的情况下,再次获取该锁而不会被阻塞。这在一些递归调用或者嵌套操作的场景中非常有用,可以避免死锁的发生。
- 高可用性:分布式系统可能会面临各种故障,如网络故障、服务器故障等。因此,分布式锁需要具备高可用性,即使部分节点出现故障,仍然能够正常工作,保证系统的稳定性。
- 容错性:在出现异常情况(如客户端崩溃、网络中断等)时,分布式锁应该能够自动释放,避免出现死锁的情况,确保系统能够继续正常运行。
高性能:由于分布式锁的获取和释放操作通常会频繁进行,因此需要具备较高的性能,以减少对系统性能的影响
为什么使用Lua脚本
通过使用Lua脚本和Redis的原子操作,可以有效地实现分布式锁,以确保在分布式系统中数据的一致性和互斥访问。
Lua脚本在Redis中的执行是原子性的,即Redis会将整个脚本作为一个不可分割的整体执行,中间不会被其他命令插入。这意味着,通过Lua脚本可以将多个操作组合成一个原子操作,这对于分布式锁的实现至关重要。例如,释放锁的操作需要检查当前的锁是否属于当前线程,并在是的情况下删除锁,这两个操作必须是原子性的,否则可能会导致锁被错误释放。
实现原理
- 加锁原理
SET lock_key unique_value NX PX expire_time
加锁操作主要利用 Redis 的 SET 命令,并结合 NX(仅在键不存在时设置)和 PX(设置过期时间)。
- lock_key:代表锁的键名,多个客户端竞争的就是这个锁。
- unique_value:是客户端生成的唯一值,用于标识该客户端,确保只有持有该锁的客户端才能释放锁。
- NX:保证只有当 lock_key 不存在时,才能设置成功,也就是只有一个客户端能获取到锁。
- PX:用于设置锁的过期时间(单位为毫秒),防止客户端在持有锁期间出现异常,导致锁无法释放。
- 解锁原理
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
以上是一个简单的Lua脚本示例,用于释放分布式锁。这段脚本的逻辑是:首先检查给定的key(KEYS[1])对应的值是否等于提供的ARGV[1](通常是一个唯一标识符,如UUID),如果是,则删除这个key来释放锁(通过DEL命令);如果不是,或者key不存在,则返回0表示释放锁失败。
Redis+Lua的优点
1.实现简单
借助 Redis 的基本命令和 Lua 脚本的简单语法,就能轻松实现分布式锁的核心逻辑。开发者无需引入复杂的分布式协调系统,降低了开发和维护的难度。
2.性能高效
Redis 本身是基于内存的高性能键值存储系统,响应速度极快。而且 Lua 脚本在 Redis 中的执行是在服务器端完成的,减少了客户端与服务器之间的多次通信开销,提升了加锁和解锁操作的性能。
3. 保证原子性
Lua 脚本在 Redis 中是原子执行的,这意味着在执行脚本期间,Redis 不会执行其他命令。在分布式锁场景中,像加锁和解锁这类操作可以通过 Lua 脚本封装成一个原子操作,避免了多个命令执行过程中出现竞态条件。
4.过期机制
Redis 的 SET 命令可以设置键的过期时间,在加锁时可以同时指定锁的过期时间。这样即使持有锁的客户端发生故障,锁也会在过期时间后自动释放,避免了死锁的发生。
5.可扩展性
Redis 支持集群和分布式部署,能够通过增加节点来扩展系统的处理能力。基于 Redis 实现的分布式锁可以很方便地在不同的环境中进行扩展,以满足高并发场景的需求。
Redis+Lua的缺点
1.时钟漂移问题
在 Redis 的过期机制中,依赖系统时钟来判断锁是否过期。如果不同节点之间的时钟存在漂移,可能会导致锁提前或延迟释放,从而引发并发问题。
2.不适合长事务
由于 Redis 锁的过期时间是固定的,如果业务逻辑中的事务执行时间过长,可能会导致锁提前过期,从而引发并发问题 。
解决方式: 可以使用redisson(看门狗机制)
3.锁释放的可靠性
尽管使用 Lua 脚本可以保证解锁操作的原子性,但在某些极端情况下,如客户端崩溃或网络中断,可能会导致锁无法正常释放。虽然可以通过设置过期时间来解决部分问题,但在过期时间内,其他客户端仍然无法获取到锁。
4.单点故障风险
如果 Redis 采用单节点部署,一旦该节点出现故障,整个分布式锁系统将无法正常工作,导致业务受到影响。
5.网络延迟影响
虽然 Redis 本身性能很高,但客户端与 Redis 服务器之间的网络延迟可能会影响锁的获取和释放操作。在高并发场景下,网络延迟可能会导致锁的竞争加剧,降低系统的性能。
代码实现
自定义注解 (DistributedLockAnnotation)
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义注解,用于标记需要加分布式锁的方法
*/
@Target(ElementType.METHOD) // 该注解可以应用于方法上
@Retention(RetentionPolicy.RUNTIME) // 注解在运行时可见
public @interface DistributedLockAnnotation {
/**
* 锁的键名,必须由使用该注解的地方指定
* @return 锁的键名
*/
String lockKey();
/**
* 锁的过期时间,默认值为 5000 毫秒
* @return 锁的过期时间
*/
int expireTime() default 5000;
}
分布式锁切面类(DistributedLockAspect)
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import java.lang.reflect.Method;
/**
* 分布式锁切面类,用于处理使用 DistributedLockAnnotation 注解的方法
*/
@Aspect // 标记该类为切面类
@Component
public class DistributedLockAspect {
// 定义 Jedis 连接池对象
private final JedisPool jedisPool;
/**
* 构造函数,初始化 Jedis 连接池
*/
public DistributedLockAspect() {
// 创建 Jedis 连接池配置对象
JedisPoolConfig poolConfig = new JedisPoolConfig();
// 初始化 Jedis 连接池,连接到本地 Redis 服务器,端口为 6379
this.jedisPool = new JedisPool(poolConfig, "localhost", 6379);
}
/**
* 环绕通知,拦截所有使用 DistributedLockAnnotation 注解的方法
* @param joinPoint 连接点对象,包含被拦截方法的信息
* @return 被拦截方法的返回值
* @throws Throwable 可能抛出的异常
*/
@Around("@annotation(com.platform.toolsplatform.common.lock.DistributedLockAnnotation)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取方法签名
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 获取被拦截的方法
Method method = signature.getMethod();
// 获取方法上的 DistributedLockAnnotation 注解
DistributedLockAnnotation annotation = method.getAnnotation(DistributedLockAnnotation.class);
// 获取注解中指定的锁的键名
String lockKey = annotation.lockKey();
// 获取注解中指定的锁的过期时间
int expireTime = annotation.expireTime();
// 创建分布式锁对象
DistributedLock lock = new DistributedLock(jedisPool, lockKey, expireTime);
// 尝试获取锁
if (lock.acquire()) {
try {
// 执行被拦截的方法
return joinPoint.proceed();
} finally {
// 释放锁
lock.release();
}
} else {
// 输出未能获取到锁的信息
System.out.println("未能获取到锁");
return null;
}
}
}
分布式类实现(DistributedLock)
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import java.util.Collections;
import java.util.UUID;
/**
* 分布式锁类,利用 Redis 和 Lua 脚本来实现分布式锁功能
*/
public class DistributedLock {
// 定义设置锁成功时 Redis 返回的结果
private static final String LOCK_SUCCESS = "OK";
// 定义 Redis SET 命令的 NX 选项,仅在键不存在时设置
private static final String SET_IF_NOT_EXIST = "NX";
// 定义 Redis SET 命令的 PX 选项,用于设置键的过期时间(单位:毫秒)
private static final String SET_WITH_EXPIRE_TIME = "PX";
// 定义释放锁成功时 Lua 脚本的返回值
private static final Long RELEASE_SUCCESS = 1L;
// 定义用于解锁的 Lua 脚本
private static final String UNLOCK_LUA;
// 静态代码块,用于初始化解锁的 Lua 脚本
static {
// 创建一个 StringBuilder 对象,用于构建 Lua 脚本
StringBuilder sb = new StringBuilder();
// 检查锁的值是否与当前客户端的标识一致
sb.append("if redis.call(\"get\", KEYS[1]) == ARGV[1] ");
// 如果一致,则删除该锁
sb.append("then ");
sb.append(" return redis.call(\"del\", KEYS[1]) ");
// 如果不一致,则返回 0
sb.append("else ");
sb.append(" return 0 ");
sb.append("end ");
// 将 StringBuilder 中的内容转换为字符串并赋值给 UNLOCK_LUA
UNLOCK_LUA = sb.toString();
}
// 定义 Jedis 连接池对象
private final JedisPool jedisPool;
// 定义锁的键名
private final String lockKey;
// 定义客户端的唯一标识
private final String clientId;
// 定义锁的过期时间(单位:毫秒)
private final int expireTime;
/**
* 构造函数,初始化分布式锁对象
* @param jedisPool Jedis 连接池对象
* @param lockKey 锁的键名
* @param expireTime 锁的过期时间(单位:毫秒)
*/
public DistributedLock(JedisPool jedisPool, String lockKey, int expireTime) {
this.jedisPool = jedisPool;
this.lockKey = lockKey;
this.clientId = UUID.randomUUID().toString();
this.expireTime = expireTime;
}
/**
* 尝试获取锁
* @return 如果获取锁成功返回 true,否则返回 false
*/
public boolean acquire() {
// 使用 try-with-resources 语句确保 Jedis 连接在使用后自动关闭
try (Jedis jedis = jedisPool.getResource()) {
// 调用 Jedis 的 set 方法尝试获取锁,设置 NX 和 PX 选项
String result = jedis.set(lockKey, clientId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
// 判断是否设置成功
return LOCK_SUCCESS.equals(result);
}
}
/**
* 释放锁
* @return 如果释放锁成功返回 true,否则返回 false
*/
public boolean release() {
// 使用 try-with-resources 语句确保 Jedis 连接在使用后自动关闭
try (Jedis jedis = jedisPool.getResource()) {
// 执行 Lua 脚本释放锁,传入锁的键名和客户端标识
Object result = jedis.eval(UNLOCK_LUA, Collections.singletonList(lockKey), Collections.singletonList(clientId));
// 判断是否释放成功
return RELEASE_SUCCESS.equals(result);
}
}
}
使用教程
import org.springframework.stereotype.Service;
/**
* 测试类,演示如何使用 DistributedLockAnnotation 注解
*/
@Service // 标记该类为服务组件
public class TestClass {
/**
* 测试方法,使用 DistributedLockAnnotation 注解加分布式锁
*/
@DistributedLockAnnotation(lockKey = "my_lock")
public void testMethod() {
// 输出获取锁成功的信息
System.out.println("成功获取到锁,开始执行临界区代码");
// 模拟临界区代码执行
}
}
扩展
通过以上知识获取锁,释放锁的逻辑 可以延展出 Redis+Lua脚本可以实现限流的业务场景实现。
通过文献查阅得知:是可以稳定达到对上亿级别的高并发流量进行限流的