redis 分布式锁(建议收藏)

更新时间:

💡一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新项目:《从零手撸:仿小红书(微服务架构)》 正在持续爆肝中,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...点击查看项目介绍 ;
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/ ;

截止目前, 星球 内专栏累计输出 82w+ 字,讲解图 3441+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,权限管理,Spring Cloud Alibaba 微服务等等,已有 2900+ 小伙伴加入学习 ,欢迎点击围观

在分布式系统中,多个服务节点可能同时访问共享资源,例如同一商品库存或数据库记录。这种情况下,如果没有协调机制,就可能导致数据竞争、重复操作或资源争用问题。例如,用户在电商平台抢购限量商品时,若多个请求同时减少库存,极可能导致库存值变为负数。为解决此类问题,Redis 分布式锁因其高性能和易用性,成为分布式系统中广泛采用的解决方案。本文将从基础概念、实现原理到实际案例,逐步解析这一技术的核心逻辑。


分布式锁的核心概念与必要性

什么是分布式锁?

分布式锁是分布式系统中用于协调多个节点对共享资源访问的机制,其作用类似于单机环境中的互斥锁。但与单机锁不同,分布式锁需要满足以下特性:

  1. 互斥性:同一时间只有一个节点能持有锁。
  2. 可重入性:同一节点在持有锁期间可重复获取锁(可选特性)。
  3. 故障恢复:当锁的持有者崩溃时,锁能被自动释放,避免资源永久锁定。

为什么需要分布式锁?

在单机环境中,Java的synchronizedReentrantLock可以轻松实现线程间的同步。但在分布式场景下,多个服务实例可能运行在不同物理机上,单机锁无法跨进程或跨机器生效。例如,当三个微服务实例同时尝试更新同一个数据库记录时,若没有分布式锁,数据库可能被多次更新,导致数据不一致。

分布式锁的常见场景

  • 秒杀系统:限制同一商品库存被多次扣减。
  • 分布式任务调度:确保同一时间只有一个节点执行定时任务。
  • 分布式缓存更新:避免多个节点同时刷新缓存,造成资源浪费。

Redis 分布式锁的实现原理

Redis 实现分布式锁的核心命令

Redis 提供了以下命令组合来实现分布式锁:

  1. SETNX(Set Not eXists):若键不存在,则设置键值对并返回1,否则返回0
  2. EXPIRE:为键设置过期时间,避免锁因持有者崩溃而永久占用资源。
  3. Lua 脚本:将多个命令原子化执行,确保操作的不可中断性。

基础实现逻辑(伪代码)

if SETNX(key, value) == 1:  
    EXPIRE(key, expire_time)  
    return True  
else:  
    return False  

上述逻辑存在两个问题:

  1. 分步操作的原子性问题SETNXEXPIRE是两个独立命令,若在执行中间发生网络延迟或崩溃,可能导致锁未设置过期时间,引发“死锁”。
  2. 随机值验证:若锁的持有者崩溃后,其他节点可能过早获取锁,但若未验证当前锁是否属于自己,可能误释放其他进程的锁。

基于 Lua 脚本的原子性优化

通过 Lua 脚本将SETNXEXPIRE合并为原子操作:

local result = redis.call('SET', KEYS[1], ARGV[1], 'NX', 'EX', tonumber(ARGV[2]))  
return result  

调用示例(Java代码):

String lockKey = "product:1001:lock";  
String lockValue = UUID.randomUUID().toString();  
long expireTime = 10; // 单位秒  

// 使用 Lua 脚本实现原子操作  
String script = "if redis.call('SET', KEYS[1], ARGV[1], 'NX', 'EX', ARGV[2]) == 'OK' then return 1 else return 0 end";  
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);  
Long result = redisTemplate.execute(redisScript, Collections.singletonList(lockKey), lockValue, String.valueOf(expireTime));  

关键设计要素解析

  1. 随机值(Lock Value)
    每个请求生成唯一值(如UUID),在释放锁时,仅当当前值与该随机值一致时才执行删除操作。这避免了误删其他进程的锁。
    if GET(key) == my_value:  
        DEL(key)  
    
  2. 过期时间(Expire)
    即使持有者崩溃,锁也会在过期时间后自动释放,防止“死锁”。但需注意:
    • 过期时间应略大于业务操作的最长时间,否则可能因正常执行未完成而提前释放锁。
    • Redis 的过期时间是近似值,存在一定的误差(通常在毫秒级)。

分布式锁的代码实现与常见问题

完整代码示例(Java)

以下是一个完整的分布式锁实现类:

public class RedisDistributedLock {  
    private final RedisTemplate<String, String> redisTemplate;  
    private final String lockKey;  
    private final long expireTime;  
    private final String lockValue;  

    public RedisDistributedLock(RedisTemplate<String, String> redisTemplate, String lockKey, long expireTime) {  
        this.redisTemplate = redisTemplate;  
        this.lockKey = lockKey;  
        this.expireTime = expireTime;  
        this.lockValue = UUID.randomUUID().toString();  
    }  

    public boolean acquire() {  
        String script = "if redis.call('GET', KEYS[1]) == false then "  
                + "return redis.call('SET', KEYS[1], ARGV[1], 'NX', 'EX', tonumber(ARGV[2])) "  
                + "else return 0 end";  
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);  
        Long result = (Long) redisTemplate.execute(  
                redisScript,  
                Collections.singletonList(lockKey),  
                lockValue,  
                String.valueOf(expireTime)  
        );  
        return result != null && result == 1L;  
    }  

    public void release() {  
        String script = "if redis.call('GET', KEYS[1]) == ARGV[1] "  
                + "then return redis.call('DEL', KEYS[1]) else return 0 end";  
        redisTemplate.execute(  
                new DefaultRedisScript<>(script, Long.class),  
                Collections.singletonList(lockKey),  
                lockValue  
        );  
    }  
}  

常见问题与解决方案

问题 1:锁的“误释放”

现象:进程 A 获取锁后,因网络延迟未收到响应,此时进程 B 获取锁并释放,导致进程 A 的锁被误删。
解决

  • 使用随机值校验,仅当锁的值与当前进程的值一致时才删除。
  • 在释放锁时,使用 Lua 脚本确保原子性:
    if redis.call("GET", KEYS[1]) == ARGV[1] then  
        return redis.call("DEL", KEYS[1])  
    else  
        return 0  
    end  
    

问题 2:锁的“饥饿问题”

现象:当多个客户端持续竞争锁时,某些客户端可能因“永远未获得锁”而阻塞。
解决

  • acquire方法中添加重试机制,设置最大重试次数或超时时间。
  • 使用 Redis 的SET命令的PX(毫秒级过期)参数,缩短重试间隔。

问题 3:Redis 主从同步延迟导致的锁失效

现象:主节点设置的锁因主从同步延迟未生效,导致从节点提前释放锁。
解决

  • 使用 Redis Cluster 或哨兵模式,确保高可用性。
  • 在客户端代码中,通过READONLY命令强制从主节点读取数据。

实际案例:电商秒杀系统的库存控制

场景描述

某电商平台在双十一活动中,一款手机库存为 100 件。用户通过秒杀页面提交订单时,需确保库存不被超额扣减。

实现方案

  1. 锁的粒度设计:按商品ID设置锁,如product:1001:lock
  2. 业务流程
    • 客户端尝试获取锁。
    • 成功后,查询数据库库存,若库存>0则扣减并生成订单。
    • 释放锁。

代码示例(简化版)

public class SeckillService {  
    private final RedisDistributedLock lock;  
    private final ProductRepository productRepo;  

    public SeckillService(ProductRepository productRepo) {  
        this.productRepo = productRepo;  
        this.lock = new RedisDistributedLock(  
                redisTemplate,  
                "product:1001:lock",  
                10 // 秒  
        );  
    }  

    public boolean placeOrder() {  
        if (!lock.acquire()) {  
            throw new RuntimeException("获取锁失败");  
        }  
        try {  
            Product product = productRepo.findById(1001);  
            if (product.getStock() > 0) {  
                product.setStock(product.getStock() - 1);  
                productRepo.save(product);  
                return true;  
            } else {  
                return false;  
            }  
        } finally {  
            lock.release();  
        }  
    }  
}  

优化点

  • 异步扣减库存:将库存扣减操作改为异步,通过消息队列(如 RabbitMQ)处理,减少锁的持有时间。
  • 缓存预热:将库存信息缓存到 Redis,减少数据库压力。

总结

Redis 分布式锁通过原子命令和 Lua 脚本,提供了一种轻量、高效且可靠的分布式协调方案。其核心在于:

  1. 原子性:确保锁的获取与过期设置同时完成。
  2. 容错性:通过随机值和过期时间,避免死锁和误删。
  3. 灵活性:适用于秒杀、任务调度等高频场景。

对于开发者而言,需注意以下要点:

  • 避免锁的粒度过细(如按订单ID锁),导致性能下降。
  • 设置合理的过期时间,需略长于业务操作耗时。
  • 结合重试机制与超时控制,平衡系统可用性与一致性。

随着分布式系统复杂度的增加,Redis 分布式锁仍是开发者应对并发问题的首选工具之一。通过本文的讲解,希望读者能掌握其实现原理,并在实际项目中灵活应用。

最新发布