Redis实战篇:深入探索分布式锁的应用

在微服务与分布式系统中,多个进程或节点需要协调访问共享资源。如何保证在并发环境下,只有一个客户端能够对某个资源进行操作,是架构设计的一项重要挑战。分布式锁正是为了解决这一类问题而存在。作为一个高性能的内存数据库,Redis 以其原子命令和丰富的数据结构,天然适合用于实现分布式锁。本文将通过代码示例、图解与详细说明,帮助你从零开始掌握 Redis 分布式锁的原理与最佳实践。


目录

  1. 分布式锁概述
  2. 基于 SETNX 的简易分布式锁

    • 2.1 SETNX 原理与语义
    • 2.2 Java 代码示例(Jedis)
    • 2.3 存在的问题:死锁与误删
  3. 使用 Lua 脚本保证原子性与安全释放

    • 3.1 Lua 脚本原理解析
    • 3.2 Java 调用 Lua 脚本示例(Spring Data Redis)
    • 3.3 流程图解:加锁与解锁的时序
  4. Redisson:生产级分布式锁方案

    • 4.1 Redisson 简介
    • 4.2 Java 示例:使用 Redisson 实现公平锁与可重入锁
  5. 分布式锁常见应用场景

    • 5.1 限流与排队
    • 5.2 分布式任务调度
    • 5.3 资源抢购与秒杀系统
  6. 分布式锁的性能与注意事项

    • 6.1 锁粒度与加锁时长控制
    • 6.2 避免单点故障:哨兵与集群模式
    • 6.3 看门狗(Watchdog)机制与续期
  7. 完整实战示例:秒杀场景下的库存扣减

    • 7.1 需求描述与设计思路
    • 7.2 Lua 脚本实现原子库存扣减
    • 7.3 Java 端集成与高并发测试
  8. 总结与最佳实践

分布式锁概述

在单机程序中,我们常常使用操作系统提供的互斥锁(如 Java 中的 synchronizedReentrantLock)来保证同一 JVM 内线程对共享资源的互斥访问。但是在微服务架构下,往往多个服务实例部署在不同的机器或容器上,进程间无法直接使用 JVM 锁机制。此时,需要借助外部组件来协调——这就是分布式锁的用途。

分布式锁的核心目标

  1. 互斥(Mutual Exclusion)
    任意时刻,只有一个客户端持有锁,其他客户端无法同时获得锁。
  2. 可重入(Reentrancy,可选)
    如果同一客户端在持有锁的情况下再次请求锁,应当允许(可重入锁);否则可能陷入死锁。
  3. 阻塞与非阻塞

    • 阻塞式:若获取锁失败,客户端会阻塞、等待;
    • 非阻塞式:若获取锁失败,直接返回失败,让客户端决定重试或退出。
  4. 防止死锁
    若客户端在持有锁后崩溃或网络抖动导致无法释放锁,必须有过期机制自动释放,以避免其他客户端永远无法获取。
  5. 高可用与性能
    分布式锁的实现需要具备高可用性,不能成为系统瓶颈;在并发量非常高的场景下,需要保证性能足够好。

Redis 为分布式锁提供了天然支持:

  • 原子性命令(如 SETNXDEL 等)可用作加锁与解锁;
  • 内置过期时间(TTL),可避免死锁;
  • Lua 脚本可以将多步操作封装为原子执行;
  • 有成熟的客户端库(如 Redisson)封装了可靠的分布式锁机制。

接下来,我们将一步步深入,从最简单的 SETNX 实现,到 Lua 脚本优化,再到生产级 Redisson 应用,全面掌握 Redis 分布式锁的实践方法。


基于 SETNX 的简易分布式锁

最基础的分布式锁思路是:客户端使用 Redis 命令 SETNX key value(SET if Not eXists)尝试创建一个锁标识。当 SETNX 返回 1 时,表示锁成功获取;当返回 0 时,表示锁已被其他客户端持有,需要重试或直接失败。

2.1 SETNX 原理与语义

  • 语法

    SETNX lock_key client_id
    • lock_key:锁对应的 Redis 键;
    • client_id:唯一标识当前客户端或线程(通常使用 UUID 或 IP+线程ID)。
  • 返回值

    • 如果 lock_key 不存在,Redis 会将其设置为 client_id,并返回 1
    • 如果 lock_key 已存在,什么都不做,返回 0
  • 加锁示例

    > SETNX my_lock "client_123"
    1   # 表示加锁成功
    > SETNX my_lock "client_456"
    0   # 表示加锁失败,my_lock 已被 "client_123" 持有

由于 SETNX 具有原子性,多客户端并发执行时只有一个会成功,满足最基本的互斥需求。

但是,光用 SETNX 还不足够。假设客户端 A 成功设置了锁,但在执行业务逻辑前崩溃或网络中断,锁永远不会被删除,导致后续客户端一直阻塞或失败,出现“死锁”问题。为了解决这一点,需要为锁设置过期时间(TTL),在客户端未正常释放时,由 Redis 自动删除锁键。

Redis 2.6.12 之后推荐使用 SET 命令带上参数 NX(只在键不存在时设置)和 PX(设置过期时间,毫秒级),以原子方式完成“加锁+设置过期”两步操作:

SET lock_key client_id NX PX 5000
  • NX:当且仅当 lock_key 不存在时,才执行设置;
  • PX 5000:将 lock_key 的过期时间设为 5000 毫秒(即 5 秒)。

这种写法避免了先 SETNXEXPIRE 可能出现的竞态问题(在 SETNXEXPIRE 之间 Redis 异常导致锁没有过期时间)。

2.2 Java 代码示例(Jedis)

下面用 Jedis 客户端演示基于 SET NX PX 的简易分布式锁:

import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;

import java.util.UUID;

public class SimpleRedisLock {

    private Jedis jedis;
    private String lockKey;
    private String clientId;         // 唯一标识,确保解锁安全
    private int expireTimeMillis;    // 锁超时时间(毫秒)

    public SimpleRedisLock(Jedis jedis, String lockKey, int expireTimeMillis) {
        this.jedis = jedis;
        this.lockKey = lockKey;
        this.clientId = UUID.randomUUID().toString();
        this.expireTimeMillis = expireTimeMillis;
    }

    /**
     * 尝试获取锁
     *
     * @return true 表示加锁成功;false 表示加锁失败
     */
    public boolean tryLock() {
        SetParams params = new SetParams();
        params.nx().px(expireTimeMillis);
        String result = jedis.set(lockKey, clientId, params);
        return "OK".equals(result);
    }

    /**
     * 释放锁(非安全方式:直接 DEL)
     */
    public void unlockUnsafe() {
        jedis.del(lockKey);
    }

    /**
     * 释放锁(安全方式:检查 value 再删除)
     *
     * @return true 表示释放成功;false 表示未释放(可能锁已过期或非自己的锁)
     */
    public boolean unlockSafe() {
        String value = jedis.get(lockKey);
        if (clientId.equals(value)) {
            jedis.del(lockKey);
            return true;
        }
        return false;
    }
}
  • 构造函数中,为当前客户端生成唯一的 clientId,用来在解锁时验证自身持有锁的合法性。
  • tryLock() 方法使用 jedis.set(lockKey, clientId, nx, px) 原子地完成“加锁 + 过期设置”。
  • unlockUnsafe() 直接 DEL,无法防止客户端误删其他客户端的锁。
  • unlockSafe()GET 判断值是否与 clientId 相同,只有相同时才 DEL,避免误删他人锁。但这段逻辑并非原子,存在并发风险:

    • A 客户端执行 GET,发现和自身 clientId 相同;
    • 在 A 调用 DEL 之前,锁意外过期,B 客户端重新获得锁并设置了新的 clientId
    • A 继续执行 DEL,将 B 加的锁错误删除,导致锁失效。

2.3 存在的问题:死锁与误删

基于上面示例,我们可以总结简易锁实现中常见的两个风险:

  1. 死锁风险

    • 如果客户端在持锁期间崩溃或网络抖动,导致无法主动释放锁,但使用了带过期时间的 SET NX PX,锁会在到期后自动释放,从而避免死锁。但如果不设过期,或者业务时间超过过期时间,又没有续期机制,会造成后续客户端加锁失败。
  2. 误删他人锁

    • 在非原子 “检查再删除” 逻辑中,客户端有可能在检查到锁属于自己但在调用 DEL 之前发生超时或运行延迟,造成误删了后来获得锁的其他客户端的锁。
    • 因此,必须用 Lua 脚本将“比对 value + 删除 key”两步操作封装为原子命令。

为保证安全释放,我们需要借助 Lua 脚本。下面详细演示如何在 Redis 端使用 Lua 脚本,确保原子执行。


使用 Lua 脚本保证原子性与安全释放

Redis 内置的 Lua 引擎允许我们将多条命令组合为单个原子操作。借助 Lua 脚本,可以在解锁时进行“判断 value 是否匹配”与“删除 key”两步的原子化,从而完全杜绝误删他人锁的问题。

3.1 Lua 脚本原理解析

3.1.1 加锁脚本

我们使用更通用的 SET 命令带参数实现“加锁 + 过期”,无需额外的 Lua 脚本。示例:

EVAL "return redis.call('SET', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2])" 1 lock_key client_id 5000
  • KEYS[1]:锁键(lock_key
  • ARGV[1]:客户端标识(client_id
  • ARGV[2]:过期时间(5000 毫秒)
  • 返回值:

    • "OK" 表示加锁成功;
    • nil 表示加锁失败。

不过,因为 SET NX PX 本身就是原子命令,没有必要用 Lua 包装。我们直接在客户端用 jedis.set(key, value, nx, px) 即可。

3.1.2 解锁脚本

下面是一段完整的 Lua 脚本 unlock.lua,用于安全释放分布式锁:

-- unlock.lua
-- KEYS[1] = lock_key
-- ARGV[1] = client_id

if redis.call("GET", KEYS[1]) == ARGV[1] then
    -- 只有当锁的持有者与传入 client_id 一致时,才删除锁
    return redis.call("DEL", KEYS[1])
else
    return 0
end
  • 逻辑解析

    1. redis.call("GET", KEYS[1]):获取锁键存储的 client_id
    2. 如果与 ARGV[1] 相同,说明当前客户端确实持有锁,于是执行 redis.call("DEL", KEYS[1]) 删除锁,返回值为 1 (表示删除成功);
    3. 否则返回 0,表示未执行删除(可能锁已过期或锁持有者不是当前客户端)。
  • 原子性保证
    整段脚本在 Redis 端一次性加载并执行,期间不会被其他客户端命令打断,保证“比对+删除”操作的原子性,从根本上避免了在“GET”与“DEL”之间的竞态条件。

3.2 Java 调用 Lua 脚本示例(Spring Data Redis)

假设你在 Spring Boot 项目中使用 Spring Data Redis,可以这样加载并执行 Lua 脚本:

3.2.1 将 unlock.lua 放到 resources/scripts/ 目录下

src
└── main
    └── resources
        └── scripts
            └── unlock.lua

3.2.2 定义 Spring Bean 加载 Lua 脚本

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.script.DefaultRedisScript;

@Configuration
public class RedisScriptConfig {

    /**
     * 将 unlock.lua 脚本加载为 DefaultRedisScript
     */
    @Bean
    public DefaultRedisScript<Long> unlockScript() {
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        // 指定脚本路径 相对于 classpath
        script.setLocation(new ClassPathResource("scripts/unlock.lua"));
        // 返回值类型
        script.setResultType(Long.class);
        return script;
    }
}

3.2.3 在分布式锁工具类中执行脚本

import org.springframework.beans.factory.annotation.Autowired;
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 RedisDistributedLock {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private DefaultRedisScript<Long> unlockScript;

    private static final long DEFAULT_EXPIRE_MILLIS = 5000; // 默认锁过期 5 秒

    /**
     * 获取分布式锁
     *
     * @param lockKey 锁 Key
     * @return clientId 用于之后解锁时比对;如果返回 null 表示获取锁失败
     */
    public String tryLock(String lockKey) {
        String clientId = UUID.randomUUID().toString();
        Boolean success = redisTemplate.opsForValue()
                .setIfAbsent(lockKey, clientId, DEFAULT_EXPIRE_MILLIS, TimeUnit.MILLISECONDS);
        if (Boolean.TRUE.equals(success)) {
            return clientId;
        }
        return null;
    }

    /**
     * 释放锁:使用 Lua 脚本保证原子性
     *
     * @param lockKey  锁 Key
     * @param clientId 获取锁时返回的 clientId
     */
    public boolean unlock(String lockKey, String clientId) {
        // KEYS[1] = lockKey; ARGV[1] = clientId
        Long result = redisTemplate.execute(
                unlockScript,
                Collections.singletonList(lockKey),
                clientId
        );
        return result != null && result > 0;
    }
}
  • tryLock 方法:

    • 通过 setIfAbsent(key, value, timeout, unit) 相当于 SET key value NX PX timeout,如果返回 true,表示加锁成功并设置过期时间。
    • 返回随机 clientId,用于后续安全解锁。若返回 null,表示加锁失败(已被占用)。
  • unlock 方法:

    • 通过 redisTemplate.execute(unlockScript, keys, args)unlock.lua 脚本在 Redis 端执行,原子地完成判断与删除。

3.3 流程图解:加锁与解锁的时序

下面用一个简化的 ASCII 图,帮助理解 Redis 分布式锁在加锁与解锁时的各个步骤:

                          ┌──────────────────────────────────┐
                          │            Redis Server          │
                          └──────────────────────────────────┘
                                     ▲             ▲
                                     │             │
          1. tryLock("my_lock")      │             │ 4. unlock("my_lock", clientId)
             SET my_lock clientId NX PX expireTime │
                                     │             │
                                     ▼             │
   ┌───────────────────────┐    ┌──────────────────────────────────┐
   │   应用 A(客户端)     │    │ 1. Redis 端执行 SETNX + EXPIRE     │
   │                       │    │    原子完成后返回 OK               │
   │ clientId = uuid-A     │    └──────────────────────────────────┘
   │ 加锁成功              │              │
   │ 业务逻辑执行中...     │              ▼
   │                       │    ┌──────────────────────────────────┐
   │                       │    │  /Lock Keys                       │
   │                       │    │  my_lock -> uuid-A (TTL: expire)  │
   └───────────────────────┘    └──────────────────────────────────┘
                                     ▲
                                     │
                 2. 其他客户端 B    │    3. A 调用 unlock 前锁过期?
                    tryLock        │
                 SET my_lock uuid-B?│
                   返回 null       │
                                     │
                                     │
           ┌───────────────────────┐  │            ┌───────────────────────┐
           │ 应用 B(客户端)      │  │            │ 应用 A 调用 unlock   │
           │ 加锁失败,返回 null   │  │            │(执行 Lua 脚本)     │
           └───────────────────────┘  │            └───────────────────────┘
                                     │                   │
                                     │ 4.1 Redis 接收 Lua 脚本  │
                                     │    if GET(key)==clientId │
                                     │      then DEL(key)       │
                                     │      else return 0       │
                                     │
                                     ▼
                           ┌──────────────────────────────────┐
                           │     Lock Key 可能已过期或被 B 获得   │
                           │  - 若 my_lock 值 == uuid-A: DEL 成功  │
                           │  - 否则返回 0,不删除任何数据        │
                           └──────────────────────────────────┘
  • 步骤 1:客户端 A 通过 SET key value NX PX expire 成功加锁;
  • 步骤 2:锁过期前,客户端 B 反复尝试 SET key 均失败;
  • 步骤 3:客户端 A 业务逻辑执行完毕,调用 unlock 方法,在 Redis 端运行 unlock.lua 脚本;
  • 步骤 4:Lua 脚本比对 GET(key)clientId,如果一致则 DEL(key),否则不做任何操作,保证安全释放。

通过上述方式,我们既保证了锁在超时后自动释放,也避免了误删他人锁的风险。


Redisson 生产级分布式锁方案

虽然自己动手实现分布式锁可以帮助理解原理,但在生产环境中有以下挑战:

  • 需要处理锁续期、锁失效、锁可重入、可重试、超时控制等复杂逻辑;
  • 要考虑 Redis 单点故障,需要使用 Redis Sentinel 或 Cluster 模式保证高可用;
  • 如果自己实现的代码不够健壮,在极端并发情况下可能出现竞态或性能瓶颈。

为此,Redisson(基于 Jedis/Lettuce 封装的 Redis 客户端工具包)提供了一套成熟的分布式锁方案,功能丰富、易用且可靠。Redisson 内部会自动完成续期看门狗、超时回退等机制,支持多种锁类型(可重入锁、公平锁、读写锁、信号量等)。

4.1 Redisson 简介

  • 起源:由 Redisson 团队开发,是一个基于 Netty 的 Redis Java 客户端,封装了众多 Redis 功能。
  • 核心特性

    • 可重入锁(Reentrant Lock)
    • 公平锁(Fair Lock)
    • 读写锁(ReadWrite Lock)
    • 信号量(Semaphore)Latch
    • 分布式队列、集合、映射 等。
    • 支持单机、Sentinel、Cluster 模式。
    • 内置看门狗(Watchdog)机制,自动续期锁,防止锁误释放。
  • maven 依赖

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

    也可以只引入 redisson 核心包,根据需要自行配置。

4.2 Java 示例:使用 Redisson 实现公平锁与可重入锁

下面演示如何在 Spring Boot 中,通过 Redisson 快速实现分布式锁。

4.2.1 配置 Redisson Client

application.yml 中配置 Redis 地址(以单机模式为例):

spring:
  redis:
    host: 127.0.0.1
    port: 6379
  redisson:
    # 可以将 Redisson 配置都放在 config 文件中,也可以使用 spring-boot-starter 默认自动配置
    # 这里使用简单模式,指向单个 Redis 节点
    address: redis://127.0.0.1:6379
    lockWatchdogTimeout: 30000 # 看门狗超时时间(ms),Redisson 会自动续期直到 30 秒

如果希望使用 Sentinel 或 Cluster,只需将 addresssentinelAddressesclusterNodes 等配置项配置好即可。

4.2.2 注入 RedissonClient 并获取锁

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 RedissonLockService {

    @Autowired
    private RedissonClient redissonClient;

    /**
     * 获取可重入锁并执行业务
     *
     * @param lockKey  锁名称
     * @param leaseTime 锁过期时间(秒)
     * @return 返回业务执行结果
     */
    public String doBusinessWithReentrantLock(String lockKey, long leaseTime) {
        RLock lock = redissonClient.getLock(lockKey);
        boolean acquired = false;
        try {
            // 尝试加锁:等待时间 3 秒,锁超时时间由 leaseTime 决定
            acquired = lock.tryLock(3, leaseTime, TimeUnit.SECONDS);
            if (!acquired) {
                return "无法获取锁,业务拒绝执行";
            }
            // 模拟业务逻辑
            Thread.sleep(2000);
            return "业务执行完成,锁自动续期或定时释放";
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return "业务执行被打断";
        } finally {
            if (acquired && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    /**
     * 公平锁示例:保证先请求锁的线程先获得锁
     */
    public String doBusinessWithFairLock(String lockKey) {
        RLock fairLock = redissonClient.getFairLock(lockKey + ":fair");
        boolean acquired = false;
        try {
            acquired = fairLock.tryLock(5, 10, TimeUnit.SECONDS);
            if (!acquired) {
                return "无法获取公平锁,业务拒绝执行";
            }
            // 模拟业务
            Thread.sleep(1000);
            return "公平锁业务执行完成";
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return "业务执行被打断";
        } finally {
            if (acquired && fairLock.isHeldByCurrentThread()) {
                fairLock.unlock();
            }
        }
    }
}
  • getLock(lockKey) 返回一个常规的可重入锁(非公平),Redisson 会在内部创建并维护一个有序的临时节点队列,结合看门狗机制自动续期。
  • getFairLock(lockKey) 返回一个公平锁,会严格按照请求顺序分配锁,适用于对公平性要求高的场景。
  • lock.tryLock(waitTime, leaseTime, unit)

    • waitTime:尝试获取锁的最长等待时间,超过则返回 false
    • leaseTime:加锁成功后,锁的自动过期时间;如果 leaseTime 为 0,则会启用看门狗模式,Redisson 会在锁快到过期时自动续期(续期周期为过期时间的 1/3)。

4.2.3 在 Controller 中使用

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class RedissonLockController {

    @Autowired
    private RedissonLockService lockService;

    @GetMapping("/redisson/reentrant")
    public String testReentrantLock() {
        return lockService.doBusinessWithReentrantLock("myReentrantLock", 10);
    }

    @GetMapping("/redisson/fair")
    public String testFairLock() {
        return lockService.doBusinessWithFairLock("myLock");
    }
}
  • 并发访问 /redisson/reentrant/redisson/fair 即可看到锁的排队与互斥执行效果。

分布式锁常见应用场景

分布式锁广泛应用于多实例系统中对共享资源或关键业务的互斥保护,以下列举常见场景:

5.1 限流与排队

  • 流量突发保护:当某个接口或资源承受高并发请求时,可先通过获取锁(或令牌桶、信号量)来限制同时访问人数。
  • 排队处理:对一批请求,串行化顺序执行,例如限购系统中,先获取锁的用户方可继续扣库存、下单,其他用户需排队等待或返回 “系统繁忙”。

5.2 分布式任务调度

  • 定时任务去重:在多台机器上同时部署定时任务,为了避免同一个任务被多次执行,可以在执行前获取一把分布式锁,只有持有锁的实例才执行任务。
  • Leader 选举:多个调度节点中,只有 Leader(获得锁的节点)执行特定任务,其他节点处于候选或 standby 状态。

5.3 资源抢购与秒杀系统

  • 库存扣减:当大批量用户同时抢购某个商品时,需要保证库存只被扣减一次。加锁可让一个用户在扣减库存期间,其他并发请求暂时阻塞或失败。
  • 支付与退款:对于同一订单多次支付或退款操作,需要使用分布式锁保证只能有一个线程对该订单进行状态变更。

分布式锁的性能与注意事项

在生产环境使用 Redis 分布式锁,需要注意以下性能和可靠性细节:

6.1 锁粒度与加锁时长控制

  • 锁粒度:不要为了简单而把全局资源都用同一个锁。应尽可能缩小锁粒度,例如对同一个“用户 ID”加锁,而非对整个“商品库存”加锁。
  • 加锁时长:合理设置过期时间,既要足够长以完成业务,又不能过度冗余,避免长时间持有锁阻塞其他请求。对于无法预估业务耗时场景,推荐使用看门狗模式(Redisson 自动续期),或定时手动续期。
  • 超时退避:当获取锁失败时,可采用指数退避(Exponential Backoff)策略,避免大量客户端瞬间重试造成雪崩。

6.2 避免单点故障:哨兵与集群模式

  • 单机模式:若 Redis 单节点出现故障,锁服务不可用。生产环境应避免使用单机模式。
  • 哨兵模式(Sentinel):可配置多个 Redis 实例组成哨兵集群,实现主从切换与自动故障转移。Redisson 与 Jedis 都支持哨兵模式的连接。
  • 集群模式(Cluster):Redis Cluster 将数据分片到多台节点,可实现更高的可用与可扩展。Redisson 也支持 Cluster 模式下的分布式锁。需注意:在 Cluster 模式下,使用分布式锁时要保证加锁与解锁操作发送到同一主节点,否则由于网络分片机制造成一致性问题。

6.3 看门狗(Watchdog)机制与续期

  • 看门狗概念:一些客户端(如 Redisson)会在加锁时启动一个“看门狗”线程,不断向 Redis 发送 PEXPIRE 延长过期时间,防止锁在持有过程中因过期而被其他客户端误获取。
  • 实现原理:Redisson 在 lock()tryLock() 成功后,会根据锁的 leaseTime 或默认值,启动一个后台定时任务,周期性地续期。例如默认 leaseTime=30 秒时,每隔 10 秒(默认 1/3)向 Redis 发送延时续命令,直到调用 unlock() 或看门狗检测到应用宕机。
  • 注意:如果使用自己手撰的 SET NX PX 方案,需要自行实现续期逻辑,否则锁在超时时间到达后,Redis 会自动删除,可能导致持锁客户端仍在执行业务时锁被误释放。

完整实战示例:秒杀场景下的库存扣减

下面通过一个典型的“秒杀系统”案例,将前文所述技术串联起来,演示如何在高并发场景下,利用 Redis 分布式锁与 Lua 脚本实现原子库存扣减并防止超卖。

7.1 需求描述与设计思路

  • 场景:假设某电商平台对某款热门商品发起秒杀活动,初始库存为 100 件。短时间内可能有上万用户并发请求秒杀。
  • 核心挑战

    1. 防止超卖:在高度并发下,只允许库存 > 0 时才能扣减,扣减后库存减 1,并录入订单信息。
    2. 保证原子性:库存检查与扣减必须在 Redis 端原子执行,防止出现并发竞态造成库存负数(即超卖)。
    3. 分布式锁保护:在订单生成和库存扣减的代码区域,需保证同一件商品只有一个线程能操作库存。
  • 解决方案思路

    1. 使用 Redis Lua 脚本,将“检查库存 + 扣减库存 + 记录订单”三步操作打包为一次原子执行,保证不会中途被其他客户端打断。
    2. 使用分布式锁(Redisson 或原生 SET NX PX + Lua 解锁脚本)保护下单流程,避免在库存扣减与订单写库之间发生并发冲突。
    3. 结合本地缓存或消息队列做削峰,进一步减轻 Redis 压力,此处主要聚焦 Redis 分布式锁与 Lua 脚本实现,不展开队列削峰。

7.2 Lua 脚本实现原子库存扣减

7.2.1 脚本逻辑

将以下 Lua 脚本保存为 seckill.lua,放置在项目资源目录(如 resources/scripts/seckill.lua):

-- seckill.lua
-- KEYS[1] = 库存 key,例如 "seckill:stock:1001"
-- KEYS[2] = 订单 key,例如 "seckill:order:userId"
-- ARGV[1] = 当前用户 ID (用户标识)
-- ARGV[2] = 秒杀订单流水号 (唯一 ID)

-- 查询当前库存
local stock = tonumber(redis.call("GET", KEYS[1]) or "-1")
if stock <= 0 then
    -- 库存不足,直接返回 0 表示秒杀失败
    return 0
else
    -- 库存充足,扣减库存
    redis.call("DECR", KEYS[1])
    -- 生成用户订单,可以把订单流水号存入一个 Set 或者按需存储
    -- 这里示例为将订单记录到 HASH 结构中,key 为 KEYS[2], field 为 用户ID, value 为 订单流水号
    redis.call("HSET", KEYS[2], ARGV[1], ARGV[2])
    -- 返回 1 表示秒杀成功
    return 1
end
  • 参数说明

    • KEYS[1]:当前商品的库存键,初始值为 库存数量
    • KEYS[2]:用于存储所有成功秒杀订单的键(HASH 结构),键名格式可自定义,如 seckill:order:1001 表示商品 ID 为 1001 的订单集合。
    • ARGV[1]:秒杀用户 ID,用于作为 HASH 的 field。
    • ARGV[2]:秒杀订单流水号,用于作为 HASH 的 value。
  • 执行逻辑

    1. 通过 redis.call("GET", KEYS[1]) 获取当前库存数,若 <= 0 返回 0,秒杀失败;
    2. 否则,执行 DECR 扣减库存;
    3. 将该用户的订单流水号记录到 HSET KEYS[2] ARGV[1] ARGV[2],用于后续下游处理(如持久化到数据库)。
    4. 最后返回 1,表示秒杀成功。

7.2.2 优势分析

  • 由于整个脚本在 Redis 端以单次原子操作执行,不会被其他客户端命令插入,因此库存检查与扣减的逻辑绝对不会出现竞态,避免了“超卖”。
  • 通过 HSET 记录订单,仅当扣减库存成功时才执行,保证库存与订单信息一致。
  • Lua 脚本执行速度远快于客户端多次 GET/DECR/HSET 的网络往返,性能更高。

7.3 Java 端集成与高并发测试

下面以 Spring Boot + Spring Data Redis 为例,展示如何加载并执行 seckill.lua 脚本,并模拟高并发进行秒杀测试。

7.3.1 项目依赖(pom.xml

<dependencies>
    <!-- Spring Boot Starter Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Spring Data Redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>

    <!-- Lettuce Client(Redis 客户端) -->
    <dependency>
        <groupId>io.lettuce</groupId>
        <artifactId>lettuce-core</artifactId>
    </dependency>

    <!-- Redisson,用于分布式锁 -->
    <dependency>
        <groupId>org.redisson</groupId>
        <artifactId>redisson-spring-boot-starter</artifactId>
        <version>3.25.0</version>
    </dependency>
</dependencies>

7.3.2 加载 Lua 脚本 Bean

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.script.DefaultRedisScript;

@Configuration
public class SeckillScriptConfig {

    @Bean
    public DefaultRedisScript<Long> seckillScript() {
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setLocation(new ClassPathResource("scripts/seckill.lua"));
        script.setResultType(Long.class);
        return script;
    }
}

7.3.3 秒杀服务实现

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
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 SeckillService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Autowired
    private DefaultRedisScript<Long> seckillScript;

    @Autowired
    private RedissonClient redissonClient;

    // 模拟秒杀接口
    public String seckill(String productId, String userId) {
        String stockKey = "seckill:stock:" + productId;
        String orderKey = "seckill:order:" + productId;
        String orderId = UUID.randomUUID().toString();

        // 1. 获取分布式锁,防止同一用户并发重复购买(可选)
        String userLockKey = "seckill:userLock:" + userId;
        RLock userLock = redissonClient.getLock(userLockKey);
        boolean lockAcquired = false;
        try {
            lockAcquired = userLock.tryLock(3, 5, TimeUnit.SECONDS);
            if (!lockAcquired) {
                return "请勿重复请求";
            }

            // 2. 调用 Lua 脚本执行原子库存扣减 + 记录订单
            Long result = redisTemplate.execute(
                    seckillScript,
                    Collections.singletonList(stockKey),
                    Collections.singletonList(orderKey),
                    userId,
                    orderId
            );
            if (result != null && result == 1) {
                return "秒杀成功,订单ID=" + orderId;
            } else {
                return "秒杀失败,库存不足";
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return "系统异常,请重试";
        } finally {
            if (lockAcquired && userLock.isHeldByCurrentThread()) {
                userLock.unlock();
            }
        }
    }

    /**
     * 初始化库存,用于测试
     */
    public void initStock(String productId, int count) {
        String stockKey = "seckill:stock:" + productId;
        redisTemplate.opsForValue().set(stockKey, count);
    }
}
  • 步骤解析

    1. 分布式锁保护

      • userLockKey = "seckill:userLock:" + userId 为锁的 Key,只允许同一个用户在并发场景下只有一把锁,避免重复请求。
      • Redisson 的 tryLock 会自动续期(看门狗),锁过期后自动解锁,防止死锁。
    2. 调用 Lua 脚本

      • redisTemplate.execute(seckillScript, keys, args...) 会在 Redis 端原子执行 seckill.lua 脚本,实现库存检查与扣减、订单记录。
      • 脚本返回 1 表示扣减成功,返回 0 表示库存不足。
    3. 释放分布式锁

      • 无论秒杀成功或失败,都要在 finally 中释放锁,避免锁泄漏。

7.3.4 Controller 暴露秒杀接口

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/seckill")
public class SeckillController {

    @Autowired
    private SeckillService seckillService;

    /**
     * 初始化库存,非真实场景仅用于测试
     */
    @PostMapping("/init")
    public String init(@RequestParam String productId, @RequestParam int count) {
        seckillService.initStock(productId, count);
        return "初始化库存成功,商品ID=" + productId + ",库存=" + count;
    }

    /**
     * 秒杀接口
     */
    @PostMapping("/buy")
    public String buy(@RequestParam String productId, @RequestParam String userId) {
        return seckillService.seckill(productId, userId);
    }
}

7.3.5 高并发测试演示

  1. 启动 Redis(建议单机模式即可)
  2. 启动 Spring Boot 应用
  3. 初始化库存

    curl -X POST "http://localhost:8080/seckill/init?productId=1001&count=100"
  4. 并发模拟用户抢购

    • 编写一个简单的脚本或使用压测工具(如 ApacheBench、JMeter)发送并发 curl 请求:

      for i in {1..200}; do
        curl -X POST "http://localhost:8080/seckill/buy?productId=1001&userId=user_$i" &
      done
      wait
    • 观察执行结果,大约有 100 条返回 “秒杀成功”,其余“秒杀失败,库存不足”。
    • 可以从 Redis 中查看库存剩余为 0,订单记录存储成功。
  5. Redis 中验证结果

    redis-cli
    > GET seckill:stock:1001
    "0"
    
    > HGETALL seckill:order:1001
    1) "user_1"
    2) "orderId-xxx"
    3) "user_2"
    4) "orderId-yyy"
    ...
    • HGETALL seckill:order:1001 列出了所有成功抢购的用户 ID 及订单流水号,确保没有超卖。

通过上述示例,我们利用 Redis Lua 脚本完成了关键的“检查库存 + 扣减库存 + 记录订单”原子操作,并结合分布式锁(Redisson)防止同一用户重复请求,达到了秒杀场景下的高并发安全保护。


总结与最佳实践

本文从最基础的 SETNX 实现,到使用 Lua 脚本保证原子性,再到 Redisson 生产级分布式锁 的使用,系统地讲解了 Redis 分布式锁的原理与实践。以下几点是实际项目中经常需要注意的最佳实践与总结:

  1. Redis 单点要避免

    • 生产环境请部署 Redis Sentinel 或 Cluster,保证分布式锁服务的高可用。
    • Redisson 能够自动感知主从切换,并维护锁的续期与数据一致性。
  2. 加锁时长需合理

    • 业务执行时间不可预估时,推荐使用 Redisson 的 Watchdog 机制,让锁自动续期,避免锁在业务执行过程中意外过期。
    • 如果选择手动管理过期时间(PX 参数),务必确保过期时间大于业务耗时,并考虑超时续期机制。
  3. 锁粒度需细化

    • 避免使用过于粗糙的全局锁,合理拆分资源维度,按业务对象(如“商品ID+用户ID”或“订单ID”)加锁,减少锁冲突。
    • 可以结合本地缓存、消息队列等方式,减少对 Redis 分布式锁的压力。
  4. Lua 脚本封装关键逻辑

    • 将“检查值 + 修改值”这种需要原子执行的操作都封装到 Lua 脚本中,避免客户端多次网络往返和中途竞态。
    • Lua 脚本性能优异,几乎和普通 Redis 命令一样快,可放心在高并发场景下使用。
  5. 监控与日志

    • 对于分布式锁的获取与释放,需要做好监控与日志记录,尤其是失败场景的告警与追踪,保证系统可观测性。
    • 记录锁获取失败的次数和耗时,帮助调试性能瓶颈和锁等待问题。
  6. 竞态重试与退避策略

    • 并发非常高时,大量客户端同时抢锁,可能造成 Redis 压力陡增。可在客户端实现重试次数与退避机制,避免“热点”锁雪崩。
    • 例如:tryLock 失败后,先 sleep 10ms,再重试;若再次失败,则根据指数退避逐渐延长重试间隔。

通过深入理解分布式锁的原理、常见风险以及成熟的解决方案(如 Redisson),你可以在实际场景中灵活应用 Redis 分布式锁,保证系统在高并发情况下仍能正确、稳定地完成关键业务逻辑。

SpringBoot实战:利用Redis Lua脚本实现分布式多命令原子操作与锁

在分布式系统中,多个客户端同时访问同一份共享资源时,往往需要保证操作的原子性与并发安全。Redis 天然支持高并发场景,但如果仅依赖其单命令原子性,对于多命令组合场景(比如同时修改多个键、检查并更新等)就无法保证原子性。而借助 Lua 脚本,Redis 可以将多条命令包装在同一个脚本里执行,保证**“一组命令”**在 Redis 侧原子执行,从而避免并发冲突。此外,Lua 脚本也常用于实现可靠的分布式锁逻辑。

本文将以 Spring Boot + Spring Data Redis 为基础,全面讲解如何通过 Redis Lua 脚本实现:

  1. 多命令原子操作
  2. 分布式锁(含锁超时续命令与安全释放)

内容包含环境准备、概念介绍、关键代码示例、以及图解说明,帮助你更容易上手并快速应用到项目中。


目录

  1. 环境准备
    1.1. 技术栈与依赖
    1.2. Redis 环境部署
  2. Lua 脚本简介
  3. Spring Boot 集成 Spring Data Redis
    3.1. 引入依赖
    3.2. RedisTemplate 配置
  4. Redis Lua 脚本的原子性与执行流程
    4.1. 为什么要用 Lua 脚本?
    4.2. Redis 调用 Lua 脚本执行流程(图解)
  5. 分布式多命令原子操作示例
    5.1. 场景描述:库存扣减 + 订单状态更新
    5.2. Lua 脚本编写
    5.3. Java 端调用脚本
    5.4. 代码示例详解
    5.5. 执行流程图示
  6. 分布式锁实现示例
    6.1. 分布式锁设计思路
    6.2. 简易版锁:SETNX + TTL
    6.3. 安全释放锁:Lua 脚本检测并删除
    6.4. Java 实现分布式锁类
    6.5. 使用示例与图解
  7. 完整示例项目结构一览
  8. 总结

环境准备

1.1 技术栈与依赖

  • JDK 1.8+
  • Spring Boot 2.5.x 或更高
  • Spring Data Redis 2.5.x
  • Redis 6.x 或更高版本
  • Maven 构建工具

主要依赖示例如下(摘自 pom.xml):

<dependencies>
    <!-- Spring Boot Starter Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Spring Data Redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
    <!-- Lettuce (Redis Client) -->
    <dependency>
        <groupId>io.lettuce</groupId>
        <artifactId>lettuce-core</artifactId>
    </dependency>

    <!-- 可选:用于 Lombok 简化代码 -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    
    <!-- 可选:用于日志 -->
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
    </dependency>
</dependencies>

1.2 Redis 环境部署

本地调试可通过 Docker 快速启动 Redis 实例,命令示例:

docker run -d --name spring-redis -p 6379:6379 redis:6.2.6 redis-server --appendonly yes

如果已经安装 Redis,可直接在本地启动:

redis-server /usr/local/etc/redis/redis.conf

确认 Redis 可用后,可使用 redis-cli 测试连接:

redis-cli ping
# 若返回 PONG 则表示正常

Lua 脚本简介

Lua 是一种轻量级脚本语言,语法简单且灵活。Redis 原生集成了一个 Lua 解释器(基于 Lua 5.1),允许客户端通过 EVAL 命令将“一段” Lua 脚本上传到 Redis 服务器并执行。Lua 脚本执行以下特点:

  1. 原子性
    整段脚本会以单个“调用”原子执行,中间不被其他客户端命令插入。
  2. 效率高
    避免了客户端-服务器之间多次网络往返,直接在服务器端执行多条命令。
  3. 可使用 Redis 原生命令
    在 Lua 脚本里,所有 Redis 命令都可通过 redis.call()redis.pcall() 调用。

常见指令:

  • EVAL script numkeys key1 key2 ... arg1 arg2 ...
  • EVALSHA sha1 numkeys key1 ... arg1 ...

其中:

  • script:Lua 代码
  • numkeys:脚本中要访问的 key 的数量
  • key1/key2...:传入的 key 列表
  • arg1/arg2...:传入的其他参数列表

Spring Boot 集成 Spring Data Redis

3.1 引入依赖

pom.xml 中,确保存在以下依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>io.lettuce</groupId>
    <artifactId>lettuce-core</artifactId>
</dependency>

Spring Boot 自动配置了 Lettuce 作为 Redis 客户端。如果你想使用 Jedis,只需排除 Lettuce 并引入 Jedis 依赖即可。

3.2 RedisTemplate 配置

在 Spring Boot 中,推荐使用 RedisTemplate<String, Object> 来操作 Redis。我们需要在配置类中进行基础配置:

@Configuration
public class RedisConfig {

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        // 默认 LettuceConnectionFactory 会读取 application.properties 中的配置
        return new LettuceConnectionFactory();
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        // 使用 StringRedisSerializer 序列化 key
        StringRedisSerializer stringSerializer = new StringRedisSerializer();
        template.setKeySerializer(stringSerializer);
        template.setHashKeySerializer(stringSerializer);

        // 使用 Jackson2JsonRedisSerializer 序列化 value
        Jackson2JsonRedisSerializer<Object> jacksonSerializer =
                new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jacksonSerializer.setObjectMapper(om);
        template.setValueSerializer(jacksonSerializer);
        template.setHashValueSerializer(jacksonSerializer);

        template.afterPropertiesSet();
        return template;
    }
}

application.properties 中,添加 Redis 连接配置:

spring.redis.host=127.0.0.1
spring.redis.port=6379
# 如果有密码,可加上:
# spring.redis.password=yourpassword

有了上述配置后,我们就能在其它组件或 Service 中注入并使用 RedisTemplate<String, Object> 了。


Redis Lua 脚本的原子性与执行流程

4.1 为什么要用 Lua 脚本?

  • 多命令原子性
    如果你在业务逻辑里需要对多个 Key 进行操作(例如:扣库存后更新订单状态),而只是使用多条 Redis 命令,就无法保证这几步操作“同时”成功或失败,存在中途出错导致数据不一致的风险。
  • 减少网络开销
    如果客户端需要执行多条命令,通常要经历 N 次网络往返(RTT)。而使用 Lua 脚本,只需要一次调用,就能在服务器端执行多条命令,极大提高性能。
  • 实现复杂逻辑
    某些场景下,需要复杂的判断、条件分支,这时可以在 Lua 中完成,而不必在客户端反复查询、再发命令,从而减少延迟和潜在的并发问题。

4.2 Redis 调用 Lua 脚本执行流程(图解)

下面是一次典型的 Lua 脚本调用流程示意图:

┌───────────┐               ┌───────────┐               ┌───────────┐
│ Client    │               │ Redis     │               │  Data     │
│ (Java)    │   EVAL LUA     │ Server    │               │ Storage   │
│           ├──────────────▶│           │               │(Key1,Key2)│
└───────────┘    (script)   │           │               └───────────┘
                            │           │
                            │ 1. 加载/执行│
                            │    Lua 脚本│
                            │ 2. 调用 lua │◀────────────┐
                            │    redis.call(... )          │
                            │    多命令执行               │
                            │ 3. 返回结果                  │
                            └───────────┘
                                      ▲
                                      │
                           响应结果    │
                                      │
                              ┌───────────┐
                              │ Client    │
                              │ (Java)    │
                              └───────────┘
  • Step 1:Java 客户端通过 RedisTemplate.execute() 方法,将 Lua 脚本和参数一起提交给 Redis Server。
  • Step 2:Redis 在服务器端加载并执行 Lua 脚本。脚本内可以直接调用 redis.call("GET", key)redis.call("SET", key, value) 等命令。此时,Redis 会对这整个脚本加锁,保证脚本执行期间,其他客户端命令不会插入。
  • Step 3:脚本执行完后,将返回值(可以是数字、字符串、数组等)返回给客户端。

分布式多命令原子操作示例

5.1 场景描述:库存扣减 + 订单状态更新

假设我们有一个电商场景,需要在用户下单时执行两步操作:

  1. 检查并扣减库存
  2. 更新订单状态为“已创建”

如果拆成两条命令:

IF stock > 0 THEN DECR stockKey
SET orderStatusKey "CREATED"

在高并发情况下,这两条命令无法保证原子性,可能出现以下问题:

  1. 扣减库存后,更新订单状态时程序异常,导致库存减少但订单未创建。
  2. 查询库存时,已被其他线程扣减,但未及时更新,导致库存不足。

此时,借助 Lua 脚本可以将“检查库存 + 扣减库存 + 更新订单状态”三步逻辑,放在一个脚本里执行,保证原子性。

5.2 Lua 脚本编写

创建一个名为 decr_stock_and_create_order.lua 的脚本,内容如下:

-- decr_stock_and_create_order.lua

-- 获取传入的参数
-- KEYS[1] = 库存 KEY (e.g., "product:stock:1001")
-- KEYS[2] = 订单状态 KEY (e.g., "order:status:abcd1234")
-- ARGV[1] = 扣减数量 (一般为 1)
-- ARGV[2] = 订单状态 (e.g., "CREATED")

local stockKey = KEYS[1]
local orderKey = KEYS[2]
local decrCount = tonumber(ARGV[1])
local statusVal = ARGV[2]

-- 查询当前库存
local currentStock = tonumber(redis.call("GET", stockKey) or "-1")

-- 如果库存不足,则返回 -1 代表失败
if currentStock < decrCount then
    return -1
end

-- 否则,扣减库存
local newStock = redis.call("DECRBY", stockKey, decrCount)

-- 将订单状态写入 Redis
redis.call("SET", orderKey, statusVal)

-- 返回剩余库存
return newStock

脚本说明:

  1. local stockKey = KEYS[1]:第一个 Redis Key,表示商品库存
  2. local orderKey = KEYS[2]:第二个 Redis Key,表示订单状态
  3. ARGV[1]:要扣减的库存数量
  4. ARGV[2]:订单状态值
  5. 先做库存检查:若不足,直接返回 -1
  6. 再做库存扣减 + 写入订单状态,最后返回剩余库存

5.3 Java 端调用脚本

在 Spring Boot 项目中,我们可以将上述 Lua 脚本放在 resources/scripts/ 目录下,然后通过 DefaultRedisScript 加载并执行。

1)加载脚本

@Component
public class LuaScriptLoader {

    /**
     * 加载 "decr_stock_and_create_order.lua" 脚本文件
     * 脚本返回值类型是 Long
     */
    @Bean
    public DefaultRedisScript<Long> decrStockAndCreateOrderScript() {
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        // 指定脚本文件路径(classpath 下)
        redisScript.setLocation(new ClassPathResource("scripts/decr_stock_and_create_order.lua"));
        redisScript.setResultType(Long.class);
        return redisScript;
    }
}
注意ClassPathResource("scripts/decr_stock_and_create_order.lua") 要与 src/main/resources/scripts/ 目录对应。

2)Service 层执行脚本

@Service
public class OrderService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate; // 也可用 RedisTemplate<String, Object>

    @Autowired
    private DefaultRedisScript<Long> decrStockAndCreateOrderScript;

    /**
     * 尝试扣减库存并创建订单
     *
     * @param productId   商品ID
     * @param orderId     订单ID
     * @param decrCount   扣减数量,一般为1
     * @return 如果返回 -1 ,表示库存不足;否则返回扣减后的剩余库存
     */
    public long decrStockAndCreateOrder(String productId, String orderId, int decrCount) {
        // 组装 Redis key
        String stockKey = "product:stock:" + productId;
        String orderKey = "order:status:" + orderId;

        // KEYS 列表
        List<String> keys = Arrays.asList(stockKey, orderKey);
        // ARGV 列表
        List<String> args = Arrays.asList(String.valueOf(decrCount), "CREATED");

        // 执行 Lua 脚本
        Long result = stringRedisTemplate.execute(
                decrStockAndCreateOrderScript,
                keys,
                args.toArray()
        );

        if (result == null) {
            throw new RuntimeException("Lua 脚本返回 null");
        }
        return result;
    }
}
  • stringRedisTemplate.execute(...):第一个参数是 DefaultRedisScript,指定脚本和返回类型;
  • 第二个参数是 keys 列表;
  • 剩余可变参数 args 对应脚本中的 ARGV

如果 result == -1,代表库存不足,需在用户侧抛出异常或返回提示;否则返回剩余库存供业务使用。

5.4 代码示例详解

  1. Lua 脚本层面

    • 首先用 redis.call("GET", stockKey) 获取当前库存,这是原子操作。
    • 判断库存是否足够:如果 currentStock < decrCount,直接返回 -1,表示库存不足,并结束脚本。
    • 否则,使用 redis.call("DECRBY", stockKey, decrCount) 进行扣减,返回新的库存数。
    • 接着用 redis.call("SET", orderKey, statusVal) 将订单状态写入 Redis。
    • 最后将 newStock 返回给 Java 客户端。
  2. Java 层面

    • 通过 DefaultRedisScript<Long> 将 Lua 脚本加载到 Spring 容器中,该 Bean 名为 decrStockAndCreateOrderScript
    • OrderService 中注入 StringRedisTemplate(简化版 RedisTemplate<String, String>),同时注入 decrStockAndCreateOrderScript
    • 调用 stringRedisTemplate.execute(...),将脚本、Key 列表与参数列表一并传递给 Redis。
    • 使用脚本返回的 Long 值决定业务逻辑分支。

这样一来,无论在多高并发的场景下,这个“扣库存 + 生成订单”操作,都能在 Redis 侧以原子方式执行,避免并发冲突和数据不一致风险。

5.5 执行流程图示

下面用 ASCII 图解总体执行流程,帮助理解:

┌─────────────────┐      1. 发送 EVAL 脚本请求       ┌─────────────────┐
│  Java 客户端    │ ─────────────────────────────▶ │    Redis Server  │
│ (OrderService)  │    KEYS=[stockKey,orderKey]   │                 │
│                 │    ARGV=[1, "CREATED"]       │                 │
└─────────────────┘                                └─────────────────┘
                                                       │
                                                       │ 2. 在 Redis 端加载脚本
                                                       │   并执行以下 Lua 代码:
                                                       │   if stock<1 then return -1
                                                       │   else decr库存; set 订单状态; return newStock
                                                       │
                                                       ▼
                                                ┌─────────────────┐
                                                │  Redis 数据层    │
                                                │ (Key:product:   │
                                                │  stock:1001)    │
                                                └─────────────────┘
                                                       │
                                                       │ 3. 返回执行结果 = newStock 或 -1
                                                       │
                                                       ▼
┌─────────────────┐                                ┌─────────────────┐
│  Java 客户端    │ ◀──────────────────────────── │    Redis Server  │
│ (OrderService)  │    返回 Long result           │                 │
│                 │    (e.g. 99 或 -1)           │                 │
└─────────────────┘                                └─────────────────┘

分布式锁实现示例

在分布式系统中,很多场景需要通过分布式锁来控制同一资源在某一时刻只能一个客户端访问。例如:秒杀场景、定时任务并发调度、数据迁移等。

下面以 Redis + Lua 脚本方式实现一个安全、可靠的分布式锁。主要思路与步骤如下:

  1. 使用 SET key value NX PX timeout 来尝试获取锁
  2. 如果获取成功,返回 OK
  3. 如果获取失败,返回 null,可重试或直接失败
  4. 释放锁时,需要先判断 value 是否和自己存储的标识一致,以防误删他人锁
注意:判断并删除的逻辑需要通过 Lua 脚本实现,否则会出现“先 GET 再 DEL”期间锁被别的客户端抢走,造成误删。

6.1 分布式锁设计思路

  • 锁 Key:比如 lock:order:1234
  • 值 Value:每个客户端生成一个唯一随机值(UUID),保证释放锁时只删除自己持有的锁
  • 获取锁SET lockKey lockValue NX PX expireTime,NX 表示只有当 key 不存在时才设置,PX 表示设置过期时间
  • 释放锁:通过 Lua 脚本,判断 redis.call("GET", lockKey) == lockValue 时,才执行 DEL lockKey

6.2 简易版锁:SETNX + TTL

在没有 Lua 脚本时,最简单的分布式锁(不推荐):

public boolean tryLockSimple(String lockKey, String lockValue, long expireTimeMillis) {
    // 使用 StringRedisTemplate
    Boolean success = stringRedisTemplate.opsForValue()
        .setIfAbsent(lockKey, lockValue, Duration.ofMillis(expireTimeMillis));
    return Boolean.TRUE.equals(success);
}

public void unlockSimple(String lockKey) {
    stringRedisTemplate.delete(lockKey);
}

缺点:

  1. 释放锁时无法判断当前锁是否属于自己,会误删别人的锁。
  2. 如果业务执行时间超过 expireTimeMillis,锁过期后被别人获取,导致解锁删除了别人的锁。

6.3 安全释放锁:Lua 脚本检测并删除

编写一个 Lua 脚本 redis_unlock.lua,内容如下:

-- redis_unlock.lua
-- KEYS[1] = lockKey
-- ARGV[1] = lockValue

-- 只有当存储的 value 和传入 value 相同时,才删除锁
if redis.call("GET", KEYS[1]) == ARGV[1] then
    return redis.call("DEL", KEYS[1])
else
    return 0
end

运行流程:

  1. client 传入 lockKeylockValue
  2. 脚本先执行 GET lockKey,若值等于 lockValue,则执行 DEL lockKey,并返回删除结果(1)
  3. 否则直接返回 0,不做任何删除

这样就保证了“只删除自己加的锁”,避免误删锁的问题。

6.4 Java 实现分布式锁类

在 Spring Boot 中,我们可以封装一个 RedisDistributedLock 工具类,封装锁的获取与释放逻辑。

1)加载解锁脚本

@Component
public class RedisScriptLoader {

    // 前面已经加载了 decrStock 脚本,下面加载解锁脚本
    @Bean
    public DefaultRedisScript<Long> unlockScript() {
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setLocation(new ClassPathResource("scripts/redis_unlock.lua"));
        redisScript.setResultType(Long.class);
        return redisScript;
    }
}

2)封装分布式锁工具类

@Service
public class RedisDistributedLock {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private DefaultRedisScript<Long> unlockScript;

    /**
     * 尝试获取分布式锁
     *
     * @param lockKey        锁 Key
     * @param lockValue      锁 Value(通常为 UUID)
     * @param expireTimeMillis 过期时间(毫秒)
     * @return 是否获取成功
     */
    public boolean tryLock(String lockKey, String lockValue, long expireTimeMillis) {
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(lockKey, lockValue, Duration.ofMillis(expireTimeMillis));
        return Boolean.TRUE.equals(success);
    }

    /**
     * 释放锁:只有锁的持有者才能释放
     *
     * @param lockKey   锁 Key
     * @param lockValue 锁 Value
     * @return 是否释放成功
     */
    public boolean unlock(String lockKey, String lockValue) {
        List<String> keys = Collections.singletonList(lockKey);
        List<String> args = Collections.singletonList(lockValue);
        // 执行 lua 脚本,返回 1 代表删除了锁,返回 0 代表未删除
        Long result = stringRedisTemplate.execute(unlockScript, keys, args.toArray());
        return result != null && result > 0;
    }
}
方法解析
  • tryLock

    • 使用 stringRedisTemplate.opsForValue().setIfAbsent(key,value,timeout)SETNX + TTL,保证只有当 key 不存在时,才设置成功
    • expireTimeMillis 用于避免死锁,防止业务没有正常释放锁导致锁永远存在
  • unlock

    • 通过先 GET lockKeylockValue 做对比,等于时再 DEL lockKey,否则不删除
    • 这部分通过 redis_unlock.lua Lua 脚本实现原子“校验并删除”

6.5 使用示例与图解

1)使用示例

@RestController
@RequestMapping("/api/lock")
public class LockController {

    @Autowired
    private RedisDistributedLock redisDistributedLock;

    @GetMapping("/process")
    public ResponseEntity<String> processTask() {
        String lockKey = "lock:task:123";
        String lockValue = UUID.randomUUID().toString();
        long expireTime = 5000; // 5秒过期

        boolean acquired = redisDistributedLock.tryLock(lockKey, lockValue, expireTime);
        if (!acquired) {
            return ResponseEntity.status(HttpStatus.CONFLICT).body("获取锁失败,请稍后重试");
        }

        try {
            // 业务处理逻辑
            Thread.sleep(3000); // 模拟执行 3 秒
            return ResponseEntity.ok("任务执行成功");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("任务执行异常");
        } finally {
            // 释放锁(安全释放)
            boolean released = redisDistributedLock.unlock(lockKey, lockValue);
            if (!released) {
                // 日志记录:释放锁失败(可能锁已过期被其他人持有)
                System.err.println("释放锁失败,lockKey=" + lockKey + ", lockValue=" + lockValue);
            }
        }
    }
}

2)解锁 Lua 脚本流程图(图解)

┌────────────────┐         1. EVAL redis_unlock.lua         ┌─────────────────┐
│ Java 客户端    │ ─────────────────────────────────────────▶ │  Redis Server    │
│ (unlock 方法) │    KEYS=[lockKey], ARGV=[lockValue]      │                  │
└────────────────┘                                         └─────────────────┘
                                                              │
                                                              │ 2. 执行 Lua:
                                                              │    if GET(key)==value 
                                                              │       then DEL(key)
                                                              │       else return 0
                                                              │
                                                              ▼
                                                    ┌──────────────────────────┐
                                                    │   Redis Key-Value 存储     │
                                                    │   lockKey -> lockValue     │
                                                    └──────────────────────────┘
                                                              │
                                                              │ 3. 返回结果 1 或 0
                                                              ▼
┌────────────────┐                                         ┌─────────────────┐
│ Java 客户端    │ ◀───────────────────────────────────────── │  Redis Server    │
│ (unlock 方法) │   返回 1(删除成功)或 0(未删除)         │                  │
└────────────────┘                                         └─────────────────┘

这样,分布式锁的获取与释放就得到了很好的保障,在高并发分布式场景中能避免竞态条件与误删锁带来的风险。


完整示例项目结构一览

以下是本文示例代码对应的典型项目目录结构:

springboot-redis-lua-demo/
├── pom.xml
├── src
│   ├── main
│   │   ├── java
│   │   │   └── com.example.redisluademo
│   │   │       ├── RedisConfig.java
│   │   │       ├── LuaScriptLoader.java
│   │   │       ├── OrderService.java
│   │   │       ├── RedisDistributedLock.java
│   │   │       └── controller
│   │   │            ├── OrderController.java
│   │   │            └── LockController.java
│   │   └── resources
│   │       ├── application.properties
│   │       └── scripts
│   │           ├── decr_stock_and_create_order.lua
│   │           └── redis_unlock.lua
│   └── test
│       └── java
│           └── com.example.redisluademo
│               └── RedisLuaDemoApplicationTests.java
└── README.md

简要说明:

  • RedisConfig.java:配置 RedisTemplate
  • LuaScriptLoader.java:加载 Lua 脚本
  • OrderService.java:演示多命令原子操作脚本调用
  • RedisDistributedLock.java:分布式锁工具类
  • OrderController.java:演示下单调用示例(可选,适当演示接口)
  • LockController.java:演示分布式锁场景
  • decr_stock_and_create_order.luaredis_unlock.lua:两个核心 Lua 脚本

总结

本文详细介绍了在 Spring Boot 项目中,如何借助 Redis Lua 脚本,实现:

  1. 分布式多命令原子操作

    • 通过 Lua 脚本将 “检查库存、扣库存、写订单状态” 三步逻辑打包在一起,保证在 Redis 端以原子方式执行,避免中途失败导致数据不一致。
    • 在 Java 侧,通过 DefaultRedisScript 加载脚本并配合 RedisTemplate.execute() 调用脚本。
  2. 分布式锁

    • 结合 SETNX + TTL 实现基本的加锁操作;
    • 利用 Lua 脚本保证“先校验 Value 再删除”这一操作的原子性,避免误删除锁的问题。
    • 在 Java 侧封装加锁与解锁逻辑,确保业务执行期间获取到合适的并发控制。

通过“代码示例 + 图解”,本文帮助你较为清晰地理解 Redis Lua 脚本在高并发场景下的威力,以及如何在 Spring Boot 中优雅地集成使用。你可以将上述示例直接复制到项目中,根据业务需求进行扩展和优化。

Tip

  • 如果业务中有更复杂的并发控制需求,也可以借助像 Redisson 这样的 Redis 客户端,直接使用它封装好的分布式锁和信号量功能。
  • 发布时间和配置请根据线上的 Redis 版本进行测试,注意 Redis 集群模式下 Lua 脚本涉及到多节点 key 存取时,需要将所有 key 定位到同一个 slot,否则脚本会报错。
2024-09-09

这是一个关于如何使用Scrapy-Redis来管理分布式爬虫的文章,它解释了源代码中的相关组件,包括队列管理和去重策略。




# 假设我们有一个Scrapy项目,我们想要使用Scrapy-Redis来管理分布式爬虫。
 
# 首先,我们需要安装Scrapy-Redis:
pip install scrapy-redis
 
# 然后,在Scrapy的settings.py文件中,我们需要配置调度器(scheduler)和队列类(queue class):
 
# settings.py
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
SCHEDULER_PERSIST = True
 
# 如果你想使用PriorityQueue来保证高优先级的请求先被爬取,可以这样设置:
SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.PriorityQueue'
# 或者使用FifoQueue按照先进先出顺序排列请求:
SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.FifoQueue'
# 或者使用LifoQueue按照后进先出顺序排列请求:
SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.LifoQueue'
 
# 最后,指定使用Redis作为数据库和调度器:
ITEM_PIPELINES = {
    'scrapy_redis.pipelines.RedisPipeline': 300,
}
 
# 这里使用了RedisPipeline来存储项目,而不是默认的Scrapy的PicklePipeline。
 
REDIS_HOST = 'localhost'
REDIS_PORT = 6379
 
# 如果你的Redis服务器需要密码,可以设置REDIS_PASSWORD。
# REDIS_PASSWORD = 'your_password'
 
# 现在,当你运行你的Scrapy爬虫时,Scrapy-Redis会使用Redis作为调度和去重的后端,
# 使得爬虫可以跨多个节点分布式地运行。

这段代码展示了如何配置Scrapy项目以使用Scrapy-Redis提供的调度器和去重功能,并且如何设置优先级队列和Last-In-First-Out队列。最后,它指定了使用Redis作为数据库和调度器,以便爬虫可以跨多个节点分布式运行。

2024-09-09

在Redis中,可以使用发布/订阅模式(pub/sub)来实现类似消息队列的消息发布和订阅功能。以下是一个使用Python和redis-py库的简单示例:

首先,确保你已经安装了redis-py库:




pip install redis

然后,你可以使用以下代码来实现发布者(Publisher)和订阅者(Subscriber):




import redis
 
# 连接到Redis
r = redis.Redis(host='localhost', port=6379, db=0)
 
# 发布者将消息发送到特定的频道
def publish_message(channel, message):
    r.publish(channel, message)
 
# 订阅者订阅特定的频道并接收消息
def subscribe_to_channel(channel):
    pubsub = r.pubsub()
    pubsub.subscribe(channel)
    for message in pubsub.listen():
        if message['type'] == 'message':
            print(f"Received message: {message['data'].decode()}")
 
# 使用示例
publish_channel = "my-channel"
subscribe_channel = "my-channel"
 
# 启动订阅者线程
import threading
subscriber_thread = threading.Thread(target=subscribe_to_channel, args=(subscribe_channel,))
subscriber_thread.start()
 
# 发布一条消息
publish_message(publish_channel, b"Hello, Redis!")
 
# 等待订阅者接收消息
subscriber_thread.join()

在这个例子中,publish_message函数负责发布消息到指定的频道,而subscribe_to_channel函数则用于订阅该频道并打印接收到的消息。这两个函数可以在不同的程序中或者在同一个程序的不同部分调用。注意,为了避免发送和接收线程相互冲突,这里使用了线程来运行订阅者。

2024-09-09

以下是一个使用Redis实现简易滑动窗口的Python示例代码:




import redis
import time
 
# 连接Redis
r = redis.Redis(host='localhost', port=6379, db=0)
 
# 滑动窗口限流的实现
def is_rate_limited(user_id, max_requests, window_size):
    key = f'rate_limit:{user_id}'
    timestamp = time.time()
 
    # 请求数加一
    r.incr(key)
 
    # 定义窗口内最大请求数
    r.expire(key, window_size)
 
    # 获取窗口内的请求数
    requests = r.get(key)
 
    # 如果请求数超过限制,则返回True表示被限流
    if requests and int(requests) > max_requests:
        return True
    else:
        return False
 
# 用户ID和最大请求数、窗口大小(秒)
user_id = 'user123'
max_requests = 5
window_size = 60
 
# 检查是否被限流
if is_rate_limited(user_id, max_requests, window_size):
    print("被限流了")
else:
    print("没有被限流")

这段代码定义了一个is_rate_limited函数,它使用Redis来跟踪给定用户ID的请求数,并且如果窗口内的请求数超过最大请求限制,则返回True表示被限流。每次调用该函数,请求数都会增加,并且窗口会被重置。这个简易的实现没有考虑多线程/进程的竞争条件,但它可以作为一个基本的滑动窗口限流策略的示范。

2024-09-09



#include <stdio.h>
#include <stdlib.com
 
// 假设的Redis集群模式API
 
// 初始化集群模式的客户端
void init_cluster_mode_client(const char* nodes) {
    // 实现集群模式客户端的初始化逻辑
    printf("初始化Redis集群模式客户端,节点列表:%s\n", nodes);
}
 
// 执行集群模式的命令
void execute_cluster_mode_command(const char* command) {
    // 实现集群模式下命令的执行逻辑
    printf("执行Redis集群模式命令:%s\n", command);
}
 
// 示例代码
int main() {
    // 假设的Redis集群节点列表
    const char* nodes = "127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002";
    
    // 初始化集群模式客户端
    init_cluster_mode_client(nodes);
    
    // 执行集群模式命令
    execute_cluster_mode_command("SET key value");
    execute_cluster_mode_command("GET key");
    
    return 0;
}

这个示例代码展示了如何初始化Redis集群模式客户端和执行集群模式命令。在实际应用中,你需要根据你的Redis库来实现这些函数的具体逻辑。

2024-09-09

问题解释:

  1. Redis 并发:指的是多个客户端同时访问 Redis 服务器时可能出现的性能瓶颈或不可预期的行为。
  2. 穿透:指的是客户端请求查询不存在的 key 时,Redis 不会返回错误,而是返回 nil。可能导致缓存穿透,请求直接打到后端存储。
  3. 雪崩:指的是当 Redis 服务器宕机或网络问题导致无法提供服务时,大量的并发请求会直接打到后端存储,造成其压力过大,甚至导致服务崩溃。

解决方法:

  1. 并发处理:

    • 使用连接池管理并发连接。
    • 对于高并发操作,可以使用 Redis 的事务或者 Lua 脚本来保证操作的原子性。
    • 限制 Redis 的最大连接数,避免由于过多的连接而导致的服务不稳定。
  2. 防止穿透:

    • 使用布隆过滤器:在客户端查询 key 之前,先在布隆过滤器中检查 key 是否存在。如果不存在,就不会请求 Redis。
    • 缓存空值:当 key 不存在时,可以将一个特殊值(如空字符串或特殊对象)缓存起来,避免后端存储压力。
  3. 雪崩预防:

    • 保证 Redis 高可用性:使用 Redis 集群或者 Sentinel 来保证服务的高可用性。
    • 限流与降级:当服务压力过大时,可以采用限流措施,如果 Redis 服务器宕机,可以快速切换到备用服务器或者关闭 Redis 服务,避免对后端存储的进一步压力。
2024-09-09



#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
// 假设这是Redis中set命令的一个简化实现
void setCommand(char *key, char *value) {
    // 这里可以实现将键值对存储到Redis的数据结构中
    printf("设置键 %s 的值为 %s\n", key, value);
}
 
// 假设这是Redis中get命令的一个简化实现
char *getCommand(char *key) {
    // 这里可以实现从Redis的数据结构中获取键对应的值
    printf("获取键 %s 的值\n", key);
    return "返回的值";
}
 
int main() {
    // 模拟用户执行set命令
    setCommand("name", "Redis");
    
    // 模拟用户执行get命令
    char *value = getCommand("name");
    printf("获取到的值是: %s\n", value);
    
    // 假设这是Redis中del命令的一个简化实现
    void delCommand(char *key) {
        // 这里可以实现从Redis的数据结构中删除键及其对应的值
        printf("删除键 %s 及其值\n", key);
    }
    
    // 模拟用户执行del命令
    delCommand("name");
    
    return 0;
}

这个代码示例展示了如何实现Redis中的SET, GET, 和 DEL命令的简化版本。它演示了如何使用C语言字符串来模拟键和值,并展示了如何打印出相关的命令日志。这个例子不是完整的Redis实现,而是为了展示如何实现简单的Redis命令,并且教会开发者如何在实际的应用程序中使用C语言与Redis交互。

2024-09-09

在Windows上安装并设置Redis密码、修改端口,并提供外部访问的步骤如下:

  1. 下载并安装Redis。
  2. 打开Redis配置文件redis.conf
  3. 找到并修改以下配置项:

    • 设置密码:找到# requirepass foobared,去掉#注释,并将foobared替换为你的密码。
    • 修改端口:找到port 6379,将6379改为你想要的端口号。
  4. 保存并关闭配置文件。
  5. 重启Redis服务。
  6. 确保Windows防火墙允许外部访问所修改的端口。
  7. 测试连接。

以下是修改后的配置文件的相关部分示例:




# requirepass yourStrongPassword
requirepass yourSecurePassword
 
# If port is set to 0, Redis does not listen on a TCP socket.
port 12345

重启Redis服务的命令可能如下:




redis-server --service-stop
redis-server --service-start

确保防火墙规则允许外部访问12345端口的命令可能如下:




netsh advfirewall firewall add rule name="Allow Redis Port" dir=in action=allow protocol=TCP localport=12345

使用redis-cli测试连接:




redis-cli -p 12345 -a yourSecurePassword

如果一切设置正确,你应该能够连接到Redis服务器,并执行命令。

2024-09-09

Redis是一个开源的使用C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。

以下是一些常见的Redis知识点和使用示例:

  1. 使用Redis存储会话信息



import redis
 
r = redis.Redis(host='localhost', port=6379, db=0)
r.set('session_id', 'session_data')
session_data = r.get('session_id')
  1. 实现缓存



import redis
 
r = redis.Redis(host='localhost', port=6379, db=0)
r.set('my_key', 'my_value')
value = r.get('my_key')
  1. 排行榜/计数



import redis
 
r = redis.Redis(host='localhost', port=6379, db=0)
r.zadd('leaderboard', {'username': 1})
leaderboard = r.zrange('leaderboard', 0, -1, withscores=True)
  1. 发布/订阅



import redis
 
r = redis.Redis(host='localhost', port=6379, db=0)
p = r.pubsub()
p.subscribe('my_channel')
r.publish('my_channel', 'my_message')
  1. 分布式锁



import redis
import time
import uuid
 
r = redis.Redis(host='localhost', port=6379, db=0)
lock_name = 'my_lock'
uid = str(uuid.uuid4())
end = time.time() + 10
 
while time.time() < end:
    if r.set(lock_name, uid, ex=5, nx=True):
        try:
            # Do some work here
            pass
        finally:
            if r.get(lock_name) == uid:
                r.delete(lock_name)
        break
    time.sleep(0.1)
  1. 使用Redis进行缓存失效



import redis
 
r = redis.Redis(host='localhost', port=6379, db=0)
r.set('key', 'value')
r.expire('key', 5)  # 设置键的过期时间为5秒
  1. 使用Redis进行分页



import redis
 
r = redis.Redis(host='localhost', port=6379, db=0)
cursor = '0'
 
while cursor != 0:
    cursor, data = r.scan(cursor=cursor, match='pattern*', count=10)
    for key in data:
        # Do something with key
        pass
  1. 使用Redis进行地理位置查询



import redis
 
r = redis.Redis(host='localhost', port=6379, db=0)
r.geoadd('city', {'New York': [40.7128, -74.0060], 'Venice': [12.3456, -123.4567}})
distance = r.geodist('city', 'New York', 'Venice', 'm')

以上示例代码展示了Redis的基本使用,包括存储会话信息、实现缓存、排行榜/计数、发布/订阅、分布式锁、缓存失效、分页、地理位置查询等功能。开发者可以根据自己的需求选择合适的Redis功能和数据类型来应用。