找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

268

积分

0

好友

34

主题
发表于 昨天 03:18 | 查看: 10| 回复: 0

目录

  1. 基础概念
  2. 实现方案与代码实现
    • 2.1 SET 命令实现
    • 2.2 StringRedisTemplate 实现
    • 2.3 Redlock 算法
    • 2.4 Redisson 框架
    • 2.5 Spring Boot 注解方式实现
  3. 优缺点对比
  4. 失效情况分析与解决方案
  5. 最佳实践案例
  6. 总结

1. 基础概念

分布式锁用于协调多个节点对共享资源的互斥访问,常用于秒杀、抢红包、库存扣减等高并发场景。Redis 因其高性能、支持原子性操作(如SET NX PX)而成为实现分布式锁的常用工具。

一个可靠的分布式锁需满足以下核心要求:

  • 互斥性:任意时刻只有一个客户端能持有锁。
  • 锁超时释放:设置合理的过期时间,避免因客户端宕机导致死锁。
  • 安全性:锁只能被其持有者释放,防止误删。
  • 高性能与高可用:加解锁操作应低延迟,且在部分节点故障时仍能正常工作。

2. 实现方案与代码实现

2.1 SET 命令实现(基础方案)

原理: 利用 Redis 的 SET key value NX PX milliseconds 命令实现原子性加锁。解锁时,需通过 Lua 脚本验证持有者身份后再删除,确保安全。

代码示例(Java + Jedis)

import redis.clients.jedis.Jedis;
import java.util.UUID;

public class RedisLock {
    private static final String LOCK_KEY = "resource_lock";
    private static final int EXPIRE_TIME = 30000; // 30s

    public static boolean tryLock(Jedis jedis, String lockKey, String uniqueId, int expireTime) {
        String result = jedis.set(lockKey, uniqueId, "NX", "PX", expireTime);
        return "OK".equals(result);
    }

    public static void unlock(Jedis jedis, String lockKey, String uniqueId) {
        String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        jedis.eval(luaScript, 1, lockKey, uniqueId);
    }

    // 使用示例
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost");
        String uniqueId = UUID.randomUUID().toString();
        if (tryLock(jedis, LOCK_KEY, uniqueId, EXPIRE_TIME)) {
            try {
                // 执行业务逻辑
            } finally {
                unlock(jedis, LOCK_KEY, uniqueId);
            }
        }
    }
}

2.2 StringRedisTemplate 实现(Spring Boot 推荐)

原理: 在 Spring Boot 项目中,利用 Spring Data Redis 提供的 StringRedisTemplate 实现加解锁,同样结合 Lua 脚本来保证解锁操作的安全性。

代码示例(Spring Boot)

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Service
public class RedisLockService {
    private final StringRedisTemplate redisTemplate;

    public RedisLockService(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public boolean tryLock(String lockKey, long expireTime) {
        String uniqueId = UUID.randomUUID().toString();
        Boolean result = redisTemplate.opsForValue().setIfAbsent(
            lockKey, uniqueId, expireTime, TimeUnit.MILLISECONDS);
        return Boolean.TRUE.equals(result);
    }

    public void unlock(String lockKey, String uniqueId) {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
        redisTemplate.execute(redisScript, Collections.singletonList(lockKey), uniqueId);
    }

    // 使用示例
    public void processResource() {
        String lockKey = "resource_lock";
        if (tryLock(lockKey, 30000)) {
            try {
                // 执行业务逻辑
            } finally {
                unlock(lockKey, redisTemplate.opsForValue().get(lockKey));
            }
        }
    }
}

2.3 Redlock 算法(多实例方案)

原理: 为了提升可用性,Redlock 算法要求在多个独立的 Redis 实例上同时获取锁,当从大多数(N/2+1)实例上成功获取锁时,才算加锁成功。这可以有效应对单点故障。

代码示例(伪代码)

def acquire_redlock(lock_name, expire_time):
    instances = [redis.Redis(host=f'redis{i}') for i in range(5)]
    success_count = 0
    for instance in instances:
        if instance.set(lock_name, "locked", nx=True, px=expire_time):
            success_count += 1
    return success_count >= 3  # 多数成功即成功

2.4 Redisson 框架(推荐方案)

Redisson 是一个在 Redis 基础上实现的 Java 驻内存数据网格客户端,它提供了丰富的数据结构和分布式服务,其分布式锁实现非常成熟,支持可重入、自动续期、多种锁类型等特性。

使用步骤

  1. 添加 Maven 依赖
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.27.2</version>
</dependency>
  1. 配置 Redis 连接(application.yml)
spring:
  redis:
    host: localhost
    port: 6379
    timeout: 2000ms
  1. 使用 RLock 接口
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;

@Service
public class OrderService {
    @Autowired
    private RedissonClient redissonClient;

    public void processOrder(String orderId) {
        RLock lock = redissonClient.getLock("order_lock:" + orderId);
        try {
            boolean isLocked = lock.tryLock(30, 30, TimeUnit.SECONDS);
            if (!isLocked) {
                throw new RuntimeException("获取锁失败");
            }
            // 执行业务逻辑
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            lock.unlock();
        }
    }
}

2.5 Spring Boot 注解方式实现

2.5.1 实现原理

通过自定义注解 + AOP 切面的方式,将分布式锁的加锁、解锁逻辑与业务代码解耦。底层通常集成 Redisson,从而获得可重入、自动续期等高阶功能。

2.5.2 实现步骤
1. 定义自定义注解
import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DistributedLock {
    String value(); // 锁 key 的 SpEL 表达式(如 #userId)
    int waitTime() default 30; // 等待加锁时间(秒)
    int leaseTime() default 30; // 锁持有时间(秒)
}
2. 实现 AOP 切面
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;

@Aspect
@Component
public class DistributedLockAspect {
    @Autowired
    private RedissonClient redissonClient;
    private final ExpressionParser parser = new SpelExpressionParser();

    @Around("@annotation(distributedLock)")
    public Object around(ProceedingJoinPoint pjp, DistributedLock distributedLock) throws Throwable {
        // 解析 SpEL 表达式获取锁 key
        String lockKey = parseSpEL(distributedLock.value(), pjp.getArgs());
        RLock lock = redissonClient.getLock(lockKey);
        try {
            // 尝试加锁
            boolean isLocked = lock.tryLock(distributedLock.waitTime(), distributedLock.leaseTime(), TimeUnit.SECONDS);
            if (!isLocked) {
                throw new RuntimeException("获取分布式锁超时");
            }
            return pjp.proceed(); // 执行目标方法
        } finally {
            lock.unlock(); // 释放锁
        }
    }

    // 解析 SpEL 表达式
    private String parseSpEL(String expression, Object[] args) {
        StandardEvaluationContext context = new StandardEvaluationContext();
        for (int i = 0; i < args.length; i++) {
            context.setVariable("arg" + i, args[i]);
        }
        return parser.parseExpression(expression).getValue(context, String.class);
    }
}
3. 配置 Spring Boot 启用 AOP
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@EnableAspectJAutoProxy(exposeProxy = true) // 必须启用 exposeProxy
public class AopConfig {}
4. 使用示例
@Service
public class OrderService {
    @DistributedLock(value = "#userId", waitTime = 10, leaseTime = 30)
    public void createOrder(String userId) {
        // 执行业务逻辑(无需手动加锁)
    }
}
2.5.3 注解失效的常见场景与解决方案
场景 原因 解决方案
1. 同类方法调用 AOP 代理失效(Spring 无法拦截内部调用) 使用 ((OrderService) AopContext.currentProxy()).createOrder(userId)
2. 未启用 AOP 未配置 @EnableAspectJAutoProxy 或未扫描切面类 添加配置并确保切面类被 Spring 扫描
3. 事务传播行为不匹配 @Transactional 方法嵌套调用可能导致锁提前释放 为分布式锁方法单独开启新事务(@Transactional(propagation = Propagation.REQUIRES_NEW)
4. 异常未捕获 方法抛出异常后未正确释放锁 Redisson 已自动处理,但需注意避免在 finally 块前退出
5. SpEL 表达式错误 value 中的表达式解析失败(如 #userId 不存在) 检查方法参数名是否匹配,或使用 args[0] 替代

3. 优缺点对比

方案 互斥性 自动续期 可重入 高可用 性能 适用场景
SET 命令 ⭐⭐⭐⭐⭐ 低并发、简单的单点业务
StringRedisTemplate ⭐⭐⭐⭐ 基于 Spring Boot 的轻量级项目
Redlock ⭐⭐⭐ 对强一致性和高可用性要求极高的场景
Redisson ⭐⭐⭐⭐ 复杂的 Java/Spring Boot 业务系统
注解方式 ⭐⭐⭐⭐ 追求快速开发、代码简洁的 Spring Boot 项目

4. 失效情况分析与解决方案

4.1 死锁(Lock Not Released)

  • 原因:持有锁的客户端异常崩溃,未能执行解锁指令。
  • 解决方案:务必设置合理的锁超时时间(TTL)。对于执行时间不确定的任务,可使用 Redisson 的 Watchdog 机制自动续期。

4.2 误删他人锁(Wrong Unlock)

  • 原因:客户端A的锁超时释放后,客户端B获取了锁,此时客户端A又尝试执行删除操作,导致B的锁被误删。
  • 解决方案:加锁时设置唯一值(如 UUID),解锁时使用 Lua 脚本先校验该值再删除。

4.3 主从切换丢锁

  • 原因:在 Redis 主从架构中,锁信息写入主节点后,在同步到从节点前主节点宕机,发生故障转移,新主节点上无此锁数据。
  • 解决方案:对于一致性要求极高的场景,考虑使用 Redlock 算法或改用 ZooKeeper、etcd 等 CP 型系统。

4.4 业务执行时间超过锁 TTL

  • 原因:业务逻辑执行时间过长,超过了锁的过期时间,锁自动释放,导致其他客户端可进入临界区。
  • 解决方案:合理评估并设置足够长的 TTL,或使用具备自动续期(Watchdog)功能的客户端(如 Redisson)。

4.5 注解失效的典型场景分析

场景 1:同类方法调用导致锁失效

@Service
public class OrderService {
    @DistributedLock(value = "#userId")
    public void createOrder(String userId) {
        // 业务逻辑
    }
    public void batchCreateOrders(String userId) {
        createOrder(userId); // ❌ 同类调用,AOP 代理失效,锁不生效
    }
}

解决方案

public void batchCreateOrders(String userId) {
    ((OrderService) AopContext.currentProxy()).createOrder(userId); // ✅ 通过代理调用
}

场景 2:事务传播行为导致锁提前释放

@Service
public class OrderService {
    @DistributedLock(value = "#userId")
    @Transactional
    public void createOrder(String userId) {
        // 业务逻辑
    }
    @Transactional
    public void processOrder(String userId) {
        createOrder(userId); // ❌ 同事务中调用,事务提交前锁可能因连接关闭而提前释放
    }
}

解决方案:让分布式锁方法运行在独立事务中。

@DistributedLock(value = "#userId")
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createOrder(String userId) {
    // 独立事务
}

5. 最佳实践案例

场景:秒杀系统扣减库存(Redisson + Spring Boot)

@Service
public class SeckillService {
    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private StockService stockService;

    public String seckill(String productId, String userId) {
        RLock lock = redissonClient.getLock("seckill_lock:" + productId);
        try {
            boolean isLocked = lock.tryLock(3, 30, TimeUnit.SECONDS);
            if (!isLocked) {
                return "系统繁忙,请稍后重试";
            }
            // 双重检查库存
            Integer stock = stockService.getStock(productId);
            if (stock <= 0) {
                return "库存不足";
            }
            // 扣减库存并创建订单
            stockService.deduct(productId);
            createOrder(productId, userId);
            return "秒杀成功";
        } catch (Exception e) {
            return "秒杀失败";
        } finally {
            lock.unlock();
        }
    }
}

6. 总结

  • 方案选择:对于生产环境,优先选择 Redisson,它功能完善,能规避大多数手动实现的坑。在纯 Spring Boot 环境中,StringRedisTemplate + Lua 是轻量可靠的备选。
  • 注解化封装:通过 自定义注解 + AOP 的方式,可以极大简化代码,使业务逻辑更清晰,是团队协作和快速开发的利器。
  • 避免手写低级错误:切勿手动组合 SETNXEXPIRE 等非原子命令,务必进行持有者身份校验。
  • 锁粒度精细化:根据业务按用户ID、商品ID等维度加锁,而非使用全局大锁,以提升系统并发能力。
  • 设计兜底方案:分布式锁并非银弹。在业务层结合数据库唯一约束、版本号乐观锁或幂等性设计,即使锁偶尔失效,系统也能保持最终正确性。

核心原则“能不用锁就不用锁,能用数据库约束等轻量级方案就不用分布式锁。” 分布式锁会引入复杂度并降低性能,应在确有必要时谨慎使用。

您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区(YunPan.Plus) ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-3 13:44 , Processed in 0.059247 second(s), 38 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 CloudStack.

快速回复 返回顶部 返回列表