在 Spring Cloud 构建的分布式系统中,多个服务实例可能会并发访问共享资源,为了保证数据的一致性和正确性,需要使用分布式锁来协调对共享资源的访问。本文将介绍几种常见的分布式锁在 Spring Cloud 中的实现方式,包括基于数据库、Redis 和 Zookeeper 的实现。
(一)依赖:
在pom.xml中添加数据库连接相关依赖,以 MySQL 为例:
mysql
mysql-connector-java
org.springframework.boot
spring-boot-starter-jdbc
(二)注解:
自定义一个注解用于标记需要加锁的方法,例如:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// 该注解用于标记需要进行数据库分布式锁控制的方法
// RetentionPolicy.RUNTIME表示注解在运行时可用
// ElementType.METHOD表示该注解可以应用在方法上
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DatabaseLock {
// 定义一个属性lockKey,用于指定锁的键
String lockKey();
}
(三)配置文件:
在application.yml中配置数据库连接信息:
spring:
datasource:
url: jdbc:mysql://localhost:3306/your_database
username: your_username
password: your_password
driver-class-name: com.mysql.cj.jdbc.Driver
(四)示例代码:
数据库锁工具类:
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
// 定义一个数据库锁工具类,用于获取和释放数据库锁
@Component
public class DatabaseLockUtil {
// 锁表名称
private static final String LOCK_TABLE_NAME = "lock_table";
// 锁键字段名称
private static final String LOCK_COLUMN_NAME = "lock_key";
// 注入JdbcTemplate用于执行SQL语句
private final JdbcTemplate jdbcTemplate;
// 构造函数,通过依赖注入获取JdbcTemplate实例
public DatabaseLockUtil(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
// 尝试获取锁的方法
public boolean acquireLock(String lockKey) {
// 插入锁记录的SQL语句,使用占位符防止SQL注入
String insertSql = "INSERT INTO " + LOCK_TABLE_NAME + " (" + LOCK_COLUMN_NAME + ") VALUES (?)";
try {
// 执行插入操作,如果插入成功,说明获取锁成功
jdbcTemplate.update(insertSql, lockKey);
return true;
} catch (org.springframework.dao.DuplicateKeyException e) {
// 如果捕获到唯一键冲突异常,说明锁已被其他线程占用,获取锁失败
return false;
}
}
// 释放锁的方法
public void releaseLock(String lockKey) {
// 删除锁记录的SQL语句,使用占位符防止SQL注入
String deleteSql = "DELETE FROM " + LOCK_TABLE_NAME + " WHERE " + LOCK_COLUMN_NAME + " =?";
// 执行删除操作,释放锁
jdbcTemplate.update(deleteSql, lockKey);
}
}
切面类:
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
// 定义一个切面类,用于在方法执行前后进行数据库锁的获取和释放操作
@Aspect
@Component
public class DatabaseLockAspect {
// 注入数据库锁工具类
private final DatabaseLockUtil databaseLockUtil;
// 构造函数,通过依赖注入获取DatabaseLockUtil实例
public DatabaseLockAspect(DatabaseLockUtil databaseLockUtil) {
this.databaseLockUtil = databaseLockUtil;
}
// 环绕通知,在方法执行前后进行增强处理
@Around("@annotation(databaseLock)")
public Object aroundMethod(ProceedingJoinPoint joinPoint, DatabaseLock databaseLock) throws Throwable {
// 获取注解中指定的锁键
String lockKey = databaseLock.lockKey();
// 尝试获取锁
boolean isLocked = databaseLockUtil.acquireLock(lockKey);
if (isLocked) {
try {
// 如果获取锁成功,执行目标方法
return joinPoint.proceed();
} finally {
// 无论目标方法执行结果如何,最终都要释放锁
databaseLockUtil.releaseLock(lockKey);
}
} else {
// 如果获取锁失败,抛出运行时异常,可根据业务需求进行更详细的处理
throw new RuntimeException("获取锁失败");
}
}
}
(五)应用场景:
订单处理:在电商系统的订单服务中,当处理订单创建、支付等操作时,可能涉及到对库存、用户账户余额等共享资源的操作。使用基于数据库的分布式锁,可以确保同一时刻只有一个订单操作能够对相关资源进行修改,避免数据不一致问题。例如,在扣减库存时,防止超卖现象。
数据同步:在分布式系统中,不同服务之间可能需要进行数据同步。例如,一个服务负责从外部数据源拉取数据,然后更新到数据库中,其他服务依赖这些数据进行业务处理。使用数据库锁可以保证在数据同步过程中,其他服务不会同时对相关数据进行不一致的操作。
(六)优缺点:
(一)依赖:
在pom.xml中添加 Redis 相关依赖:
org.springframework.boot
spring-boot-starter-data-redis
(二)注解:
可以复用前面自定义的DatabaseLock注解,也可以重新定义一个更具针对性的 Redis 锁注解,例如:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// 该注解用于标记需要进行Redis分布式锁控制的方法
// RetentionPolicy.RUNTIME表示注解在运行时可用
// ElementType.METHOD表示该注解可以应用在方法上
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RedisLock {
// 定义一个属性lockKey,用于指定锁的键
String lockKey();
// 定义一个属性expireTime,用于指定锁的过期时间,默认10秒
long expireTime() default 10;
}
(三)配置文件:
在application.yml中配置 Redis 连接信息:
spring:
redis:
host: localhost
port: 6379
password: your_password
database: 0
(四)示例代码:
Redis 锁工具类:
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
// 定义一个Redis锁工具类,用于获取和释放Redis分布式锁
@Component
public class RedisLockUtil {
// 注入RedisTemplate用于操作Redis
private final RedisTemplate redisTemplate;
// 构造函数,通过依赖注入获取RedisTemplate实例
public RedisLockUtil(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
// 尝试获取锁的方法
public boolean acquireLock(String lockKey, long expireTime) {
// 生成一个唯一的值,用于标识当前获取锁的线程
String value = UUID.randomUUID().toString();
// 获取ValueOperations对象,用于操作Redis的字符串类型数据
ValueOperations operations = redisTemplate.opsForValue();
// 使用setIfAbsent方法尝试设置锁,如果键不存在则设置成功,返回true;否则返回false
Boolean success = operations.setIfAbsent(lockKey, value, expireTime, TimeUnit.SECONDS);
return success!= null && success;
}
// 释放锁的方法
public void releaseLock(String lockKey, String value) {
// 使用Lua脚本确保释放锁的操作是原子性的
// 脚本逻辑:如果当前锁的值等于传入的值,则删除锁
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
// 执行Lua脚本,传入脚本和相关参数
redisTemplate.execute(new org.springframework.data.redis.core.script.DefaultRedisScript<>(script, Long.class),
new String[]{lockKey}, value);
}
}
切面类:
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
// 定义一个切面类,用于在方法执行前后进行Redis锁的获取和释放操作
@Aspect
@Component
public class RedisLockAspect {
// 注入Redis锁工具类
private final RedisLockUtil redisLockUtil;
// 构造函数,通过依赖注入获取RedisLockUtil实例
public RedisLockAspect(RedisLockUtil redisLockUtil) {
this.redisLockUtil = redisLockUtil;
}
// 环绕通知,在方法执行前后进行增强处理
@Around("@annotation(redisLock)")
public Object aroundMethod(ProceedingJoinPoint joinPoint, RedisLock redisLock) throws Throwable {
// 获取注解中指定的锁键
String lockKey = redisLock.lockKey();
// 获取注解中指定的锁过期时间
long expireTime = redisLock.expireTime();
// 生成一个唯一的值,用于标识当前获取锁的线程
String value = UUID.randomUUID().toString();
// 尝试获取锁
boolean isLocked = redisLockUtil.acquireLock(lockKey, expireTime);
if (isLocked) {
try {
// 如果获取锁成功,执行目标方法
return joinPoint.proceed();
} finally {
// 无论目标方法执行结果如何,最终都要释放锁
redisLockUtil.releaseLock(lockKey, value);
}
} else {
// 如果获取锁失败,抛出运行时异常,可根据业务需求进行更详细的处理
throw new RuntimeException("获取锁失败");
}
}
}
(五)应用场景:
高并发接口限流:在 Spring Cloud 微服务架构中,当某个接口面临高并发访问时,为了防止系统因过载而崩溃,可以使用 Redis 分布式锁实现限流。例如,通过限制每个用户在一定时间内对某个接口的访问次数,使用 Redis 锁来控制并发请求的数量,保证系统的稳定性。
分布式缓存更新:在分布式系统中,多个服务可能共享同一个缓存。当需要更新缓存数据时,为了避免缓存不一致问题,可以使用 Redis 分布式锁。只有获取到锁的服务才能更新缓存,其他服务等待锁释放后再进行操作,确保缓存数据的一致性。
(六)优缺点:
(一)依赖:
在pom.xml中添加 Zookeeper 和 Curator 相关依赖:
org.apache.curator
curator-framework
5.2.0
org.apache.curator
curator-recipes
5.2.0
(二)注解:
同样可以复用前面的注解,或者自定义一个 Zookeeper 锁注解:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// 该注解用于标记需要进行Zookeeper分布式锁控制的方法
// RetentionPolicy.RUNTIME表示注解在运行时可用
// ElementType.METHOD表示该注解可以应用在方法上
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ZookeeperLock {
// 定义一个属性lockPath,用于指定锁在Zookeeper中的路径
String lockPath();
// 定义一个属性waitTime,用于指定等待获取锁的时间,默认5秒
long waitTime() default 5000;
// 定义一个属性leaseTime,用于指定锁的租约时间,默认60秒
long leaseTime() default 60000;
}
(三)配置文件:
在application.yml中配置 Zookeeper 连接信息:
zookeeper:
connect-string: localhost:2181
(四)示例代码:
Zookeeper 锁工具类:
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
// 定义一个Zookeeper锁工具类,用于获取和释放Zookeeper分布式锁
@Component
public class ZookeeperLockUtil {
// Zookeeper客户端实例
private final CuratorFramework curatorFramework;
// 构造函数,通过依赖注入获取Zookeeper连接字符串,并创建Zookeeper客户端实例
public ZookeeperLockUtil(@Value("${zookeeper.connect-string}") String connectString) {
// 使用ExponentialBackoffRetry策略创建Zookeeper客户端,重试间隔为1秒,最多重试3次
this.curatorFramework = CuratorFrameworkFactory.newClient(connectString,
new ExponentialBackoffRetry(1000, 3));
// 启动Zookeeper客户端
curatorFramework.start();
}
// 尝试获取锁的方法
public boolean acquireLock(String lockPath, long waitTime, long leaseTime) throws Exception {
// 创建InterProcessMutex对象,用于获取和释放分布式锁
InterProcessMutex lock = new InterProcessMutex(curatorFramework, lockPath);
// 尝试获取锁,等待时间为waitTime,锁的租约时间为leaseTime
return lock.acquire(waitTime, leaseTime, TimeUnit.MILLISECONDS);
}
// 释放锁的方法
public void releaseLock(String lockPath) throws Exception {
// 创建InterProcessMutex对象,用于获取和释放分布式锁
InterProcessMutex lock = new InterProcessMutex(curatorFramework, lockPath);
// 如果当前线程持有锁,则释放锁
if (lock.isAcquiredInThisProcess()) {
lock.release();
}
}
}
切面类:
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
// 定义一个切面类,用于在方法执行前后进行Zookeeper锁的获取和释放操作
@Aspect
@Component
public class ZookeeperLockAspect {
// 注入Zookeeper锁工具类,用于实际操作Zookeeper来获取和释放锁
private final ZookeeperLockUtil zookeeperLockUtil;
// 构造函数,通过依赖注入获取ZookeeperLockUtil实例
public ZookeeperLockAspect(ZookeeperLockUtil zookeeperLockUtil) {
this.zookeeperLockUtil = zookeeperLockUtil;
}
// 环绕通知,在方法执行前后进行增强处理,使用@Around注解标注该方法,使其可以在目标方法执行前后进行逻辑干预
@Around("@annotation(zookeeperLock)")
public Object aroundMethod(ProceedingJoinPoint joinPoint, ZookeeperLock zookeeperLock) throws Throwable {
// 获取注解中指定的锁在Zookeeper中的路径,这个路径用于在Zookeeper中定位对应的锁节点
String lockPath = zookeeperLock.lockPath();
// 获取注解中指定的等待获取锁的时间,单位是毫秒,决定了当前线程在获取锁时愿意等待的最长时间
long waitTime = zookeeperLock.waitTime();
// 获取注解中指定的锁的租约时间,单位是毫秒,即锁被持有的最长有效时间,超时后锁会自动释放
long leaseTime = zookeeperLock.leaseTime();
// 尝试获取锁,调用ZookeeperLockUtil中的acquireLock方法,传入相应参数,根据返回结果判断是否获取成功
boolean isLocked = zookeeperLockUtil.acquireLock(lockPath, waitTime, leaseTime);
if (isLocked) {
try {
// 如果获取锁成功,执行目标方法,通过调用joinPoint的proceed方法来触发被切面拦截的实际业务方法继续执行
return joinPoint.proceed();
} finally {
// 无论目标方法执行结果如何(正常返回或抛出异常),最终都要释放锁,调用ZookeeperLockUtil的releaseLock方法释放对应的锁资源
zookeeperLockUtil.releaseLock(lockPath);
}
} else {
// 如果获取锁失败,抛出运行时异常,这里可以根据业务需求进行更详细的处理,比如记录日志、返回特定的错误提示等
throw new RuntimeException("获取锁失败");
}
}
}
(五)应用场景:
分布式任务调度:在 Spring Cloud 的多个服务中,可能存在一些定时任务,如数据统计、报表生成等。使用 Zookeeper 分布式锁可以确保这些任务在分布式环境下只有一个实例执行,避免任务重复执行带来的资源浪费和数据不一致问题。例如,有多个服务实例都配置了每天凌晨 2 点执行数据统计任务,通过 Zookeeper 分布式锁,在同一时刻只有一个服务实例能够获取到锁并执行该任务,其他实例则等待锁释放后再判断是否需要执行,从而保证了数据统计的准确性以及避免对系统资源的过度消耗。
服务注册与发现中的一致性维护:在使用 Spring Cloud Netflix Eureka 等服务注册与发现组件时,当服务实例进行注册或下线操作时,可能会出现并发问题。通过 Zookeeper 分布式锁,可以保证同一时刻只有一个服务实例能够进行注册或下线操作,维护服务注册中心的一致性和稳定性。比如,当一个新的服务实例启动要向注册中心注册自己的信息时,先去获取 Zookeeper 上对应的锁,获取成功后进行注册操作,注册完成再释放锁,这样就能防止多个服务实例同时注册造成的数据冲突或者注册信息混乱等情况。
(六)优缺点:
在 Spring Cloud 分布式系统中,基于 Zookeeper 的分布式锁实现有着独特的优势和适用场景。它凭借高可靠性、对锁重入和阻塞的良好支持以及解决死锁问题的能力,在对数据一致性、任务执行顺序等要求较高的场景中表现出色,如分布式任务调度和服务注册与发现的一致性维护等方面。然而,其性能开销相对较大、使用复杂度较高以及资源占用等问题也需要开发者在选择时充分考虑。在实际应用中,需要根据具体的业务需求、系统规模以及性能要求等因素,权衡其优缺点,来决定是否采用基于 Zookeeper 的分布式锁实现方式,从而确保分布式系统中共享资源访问的高效、稳定和有序。