分布式锁在 Spring Cloud 中的实现方式

一、引言

在 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("获取锁失败");
        }
    }
}

(五)应用场景
订单处理:在电商系统的订单服务中,当处理订单创建、支付等操作时,可能涉及到对库存、用户账户余额等共享资源的操作。使用基于数据库的分布式锁,可以确保同一时刻只有一个订单操作能够对相关资源进行修改,避免数据不一致问题。例如,在扣减库存时,防止超卖现象。
数据同步:在分布式系统中,不同服务之间可能需要进行数据同步。例如,一个服务负责从外部数据源拉取数据,然后更新到数据库中,其他服务依赖这些数据进行业务处理。使用数据库锁可以保证在数据同步过程中,其他服务不会同时对相关数据进行不一致的操作。
(六)优缺点

  • 优点
    实现简单:直接利用数据库的唯一索引特性,容易理解和实现。
    兼容性好:只要能操作数据库,几乎所有的编程语言和框架都能使用这种方式实现分布式锁。
  • 缺点
    性能瓶颈:数据库的读写操作相对较慢,尤其是在高并发场景下,频繁的数据库操作会成为性能瓶颈。
    单点故障:数据库一旦出现故障,整个分布式锁机制将不可用,影响业务系统的正常运行。
    锁超时问题:如果没有额外的机制来处理锁超时,一旦解锁操作失败,锁记录会一直存在,导致其他线程无法获取锁。
    非重入性:默认情况下,同一个线程在未释放锁之前无法再次获取锁,需要额外的逻辑来实现重入性。

三、基于 Redis 的分布式锁实现:

(一)依赖
在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 分布式锁。只有获取到锁的服务才能更新缓存,其他服务等待锁释放后再进行操作,确保缓存数据的一致性。
(六)优缺点

  • 优点
    性能高效:Redis 是基于内存的数据库,读写速度非常快,适合高并发场景下的分布式锁实现。
    集群支持:Redis 可以部署为集群模式,避免单点故障问题,提高系统的可用性。
    实现方便:Redis 提供了丰富的命令和工具,使得分布式锁的实现相对简单。
  • 缺点
    锁过期风险:虽然可以设置锁的过期时间,但如果业务执行时间超过了锁的过期时间,可能会导致其他线程获取到锁,从而引发数据不一致问题。
    网络问题:在分布式环境中,网络波动可能导致锁的获取和释放操作失败,需要额外的重试机制来保证可靠性。
    Redis 版本兼容性:不同版本的 Redis 在功能和性能上可能存在差异,需要注意兼容性问题。

四、基于 Zookeeper 的分布式锁实现:

(一)依赖
在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 上对应的锁,获取成功后进行注册操作,注册完成再释放锁,这样就能防止多个服务实例同时注册造成的数据冲突或者注册信息混乱等情况。
(六)优缺点

  • 优点
    高可靠性:Zookeeper 本身具有高可靠性,基于其实现的分布式锁可以有效解决单点问题,在节点出现故障时能够保证锁机制依然正常工作,通过选举机制等确保服务的持续可用,从而保障分布式系统中共享资源访问的协调一致性。
    支持锁的重入与阻塞:能够很好地实现锁的可重入性,同一个线程可以多次获取同一把锁,符合很多复杂业务场景下的需求。同时支持阻塞获取锁,当锁被其他线程占用时,当前线程可以等待,直到获取到锁为止,使得业务逻辑在等待锁的过程中更加有序和可控。
    解决死锁问题:其内部机制可以避免服务宕机导致的锁无法释放而产生的死锁情况,例如通过临时节点的特性,当客户端与 Zookeeper 集群的连接断开时,对应的临时节点会被自动删除,释放锁资源,防止死锁的出现。
    实现相对简单直观:借助 Curator 等框架提供的 API,实现分布式锁的逻辑相对简洁明了,开发者可以较容易地将其集成到 Spring Cloud 项目中,用于协调分布式环境下的资源访问控制。
  • 缺点
    性能开销:相较于基于 Redis 的分布式锁实现,Zookeeper 在创建和释放锁的过程中,由于要动态创建、销毁瞬时节点等操作,涉及到较多的网络通信以及 Zookeeper 内部的协调流程,性能上会稍显逊色,在高并发场景下可能会成为系统性能的瓶颈。
    复杂度较高:需要对 Zookeeper 的相关原理有一定了解,比如节点类型、会话机制、选举机制等,才能更好地理解和处理使用过程中可能出现的问题,例如网络抖动导致的临时节点异常删除等并发问题,这对开发和运维人员的技术要求相对较高,增加了系统的整体复杂度。
    资源占用:Zookeeper 集群自身需要占用一定的系统资源来维持其正常运行和协调功能,在大规模分布式系统中,如果大量使用基于 Zookeeper 的分布式锁,可能会加重 Zookeeper 集群的负担,影响整个系统的资源利用效率。

总结:

在 Spring Cloud 分布式系统中,基于 Zookeeper 的分布式锁实现有着独特的优势和适用场景。它凭借高可靠性、对锁重入和阻塞的良好支持以及解决死锁问题的能力,在对数据一致性、任务执行顺序等要求较高的场景中表现出色,如分布式任务调度和服务注册与发现的一致性维护等方面。然而,其性能开销相对较大、使用复杂度较高以及资源占用等问题也需要开发者在选择时充分考虑。在实际应用中,需要根据具体的业务需求、系统规模以及性能要求等因素,权衡其优缺点,来决定是否采用基于 Zookeeper 的分布式锁实现方式,从而确保分布式系统中共享资源访问的高效、稳定和有序。

你可能感兴趣的:(Java开发,分布式,spring,cloud,spring,java,架构,后端)