Redis分布式锁的正确用法

更新日期: 2025-12-30 阅读: 35 标签: Redis

多个服务同时操作同一个资源时,需要一种协调机制。比如秒杀活动中扣减库存,或者多个用户抢同一个红包。Redis分布式锁就是解决这种问题的常用工具。它利用Redis的高性能和原子操作特性,确保同一时间只有一个客户端能执行关键操作。

一个可靠的分布式锁需要满足几个基本要求:

  1. 互斥性:同一时间只能有一个客户端持有锁

  2. 超时释放:锁必须有有效期,避免死锁

  3. 安全释放:只能由锁的持有者释放锁

  4. 高可用:锁服务要稳定可靠

下面介绍几种实现方式,从简单到复杂。


基础实现:SET命令

Redis的SET命令有个特殊用法:SET key value NX PX milliseconds。NX表示只在键不存在时设置,PX设置过期时间(毫秒)。这个命令是原子性的,很适合实现锁。

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

public class RedisLock {
    // 尝试获取锁
    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", 6379);
        String lockKey = "order_lock";
        String clientId = UUID.randomUUID().toString(); // 唯一标识
        
        try {
            if (tryLock(jedis, lockKey, clientId, 30000)) {
                // 这里执行需要加锁的业务逻辑
                System.out.println("执行业务操作");
            }
        } finally {
            unlock(jedis, lockKey, clientId);
        }
    }
}

注意几个要点:

  1. 每个客户端使用唯一标识(如UUID),防止误删别人的锁

  2. 使用Lua脚本保证检查和删除是原子操作

  3. 一定要在finally中释放锁


Spring Boot项目中的实现

如果你用Spring Boot,可以用StringRedisTemplate,写起来更简洁。

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

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) {
        // 使用Lua脚本保证原子性
        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),
            uniqueId
        );
    }
}


使用Redisson框架

手动实现分布式锁要考虑很多细节,比如锁续期、可重入等。Redisson是一个成熟的Redis客户端,内置了分布式锁的实现,推荐使用。

首先添加依赖:

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.27.2</version>
</dependency>

配置Redis连接:

spring:
  redis:
    host: localhost
    port: 6379

使用Redisson的锁:

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class OrderService {
    @Autowired
    private RedissonClient redissonClient;
    
    public void processOrder(String orderId) {
        RLock lock = redissonClient.getLock("order:" + orderId);
        
        try {
            // 尝试获取锁,最多等待10秒,锁持有30秒
            boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS);
            if (locked) {
                // 执行业务逻辑
                System.out.println("处理订单: " + orderId);
            } else {
                System.out.println("获取锁失败");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

Redisson的优点:

  1. 自动续期:业务没执行完,锁快过期时会自动续期

  2. 可重入:同一个线程可以多次获取同一把锁

  3. 高可用:支持主从、哨兵、集群模式


实际应用案例:库存扣减

假设有个秒杀系统,多个用户同时抢购同一商品。

@Service
public class SeckillService {
    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private StockService stockService;
    
    public String seckill(String productId, String userId) {
        // 按商品ID加锁,不同商品可以并发处理
        String lockKey = "seckill:" + productId;
        RLock lock = redissonClient.getLock(lockKey);
        
        try {
            // 快速失败,等待不超过1秒
            if (!lock.tryLock(1, 10, TimeUnit.SECONDS)) {
                return "抢购太火爆,请重试";
            }
            
            // 检查库存
            int stock = stockService.getStock(productId);
            if (stock <= 0) {
                return "商品已售完";
            }
            
            // 扣减库存
            boolean success = stockService.reduceStock(productId, 1);
            if (success) {
                // 创建订单
                createOrder(productId, userId);
                return "抢购成功";
            } else {
                return "库存不足";
            }
            
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return "系统异常";
        } finally {
            if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}


需要注意的问题

1. 锁过期时间设置

锁的过期时间不能太短,否则业务没执行完锁就释放了。也不能太长,否则客户端崩溃后锁要很久才能自动释放。一般设置在10-30秒,具体根据业务执行时间调整。

2. 避免死锁

一定要在finally块中释放锁。即使业务代码抛出异常,也要确保锁被释放。

3. 主从切换问题

在Redis主从架构中,如果主节点写入锁后宕机,从节点可能还没同步锁数据就升级为主节点,导致锁丢失。对一致性要求高的场景,可以考虑使用Redlock算法(多个独立的Redis实例),或者用ZooKeeper等CP系统。

4. 锁粒度要合适

锁的粒度越细,并发度越高。比如按用户ID加锁比全局一把锁性能更好。但也要避免锁太多消耗过多资源。


什么时候不用分布式锁

分布式锁不是万能解药。有些场景可以用更简单的方式:

  1. 数据库唯一约束:防重复提交可以用数据库唯一索引

  2. 乐观锁:更新数据时加版本号检查

  3. 消息队列:通过队列串行处理请求

原则是:能不用锁就不用锁,能用简单方案就不用复杂方案。


总结

Redis分布式锁是解决分布式系统并发问题的有效工具。对于简单场景,可以直接用SET命令实现。对于Spring Boot项目,StringRedisTemplate更方便。对于生产环境,推荐使用Redisson,它功能完善,稳定性好。

关键是要理解锁的原理,设置合理的过期时间,确保锁的安全释放。同时要考虑锁粒度、性能影响和替代方案。正确使用分布式锁,能让你的系统更稳定可靠。

本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!

链接: https://fly63.com/article/detial/13313

在使用redis-cluster之前你需要知道这些事

前段时间基础架构组、DBA还有云盘团队一起推广了phpredis的RedisCluster的线上使用,目前线上业务已经稳定,单业务的规模水平是:Qps平均15W,数据量在700G左右。现对这段时间的工作和所遇到的一些常见问题进行简单总结

Redis 5种主要数据类型和命令

redis常用数据结构strig、list、hash、set、zset,这是最常用的5中redis数据结构,其实还有些不太常用的数据结构比如:HyperLogLog、GeoHash、PubSub等

Redis可视化管理工具有哪些?

redis 是当前非常流行的缓存数据库,得益于其简单的 key-value 模式的数据存储和丰富的数据类型与事件机制使得 redis 成为当前后端开发中不可或缺的利器。下面推荐一些好用的 redis 的管理工具

Redis 中 Lua 脚本的应用和实践

前段时间组内有个投票的产品,上线前考虑欠缺,导致被刷票严重。后来,通过研究,发现可以通过 redis lua 脚本实现限流,这里将 redis lua 脚本相关的知识分享出来,讲的不到位的地方还望斧正。

Redis在Php项目中的实际应用场景

Redis在Php项目中的实际应用场景:商品维度计数、用户维度计数、存储社交关系、用作缓存代替memcached、反spam系统、用户Timeline/Feeds、最新列表&排行榜、消息通知、将Redis用作消息队列

PHP使用redis防止大并发下二次写入

php调用redis进去读写操作,大并发下会出现:读取key1,没有内容则写入内容,但是大并发下会出现同时多个php进程写入的情况,这个时候需要加一个锁,即获取锁的php进程有权限写。

Redis的主从复制

Redis配置成主从模式,主库(Master)只负责写数据,从库(Slave)只负责读数据。一个主库可以拥有多个从库,但一个从库只能隶属于一个主库。

Redis常用命令

连接操作命令,持久化,远程服务控制,对value操作的命令,String,List,Set,Hash,Redis 发布订阅命令,Redis 事务命令,查看keys个数,清空数据库

分布式锁的redis缓存使用方式

目前有很多成熟的缓存产品,包括Redis,memcached等。这里以Redis为例来分析下使用缓存实现分布式锁的方案。主要的实现方式是使用Jedis.setNX方法来实现。以上实现方式同样存在几个问题:

Redis的正确使用姿势

说到分布式缓存,可能大多数人脑海浮现的就是redis了,为什么redis能够在竞争激烈的缓存大战中脱颖而出呢?原因无非有一下几点:性能好,丰富的特性跟数据结构,api操作简单。但是用的人多了,就会出现很多不规范或者疏忽的地方,严重的时候甚至会导致生产事故

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!