第9篇:Redis分布式锁与分布式ID

📚 文章概述

分布式锁和分布式ID是分布式系统中的两个核心问题。Redis提供了强大的原子操作能力,可以很好地解决这两个问题。本文将深入讲解基于Redis的分布式锁实现原理、Redlock算法、分布式ID生成策略,帮助读者在实际项目中正确使用分布式锁和生成分布式ID。


一、理论部分

1.1 分布式锁概述

1.1.1 为什么需要分布式锁?

分布式环境下的并发问题:
Server1 Server2 Redis Database 读取库存: 10 读取库存: 10 计算: 10-1=9 计算: 10-1=9 更新库存: 9 更新库存: 9 最终库存: 9 应该是8! Server1 Server2 Redis Database

分布式锁的作用:

  • 保证同一时刻只有一个进程可以执行临界区代码
  • 防止并发问题
  • 保证数据一致性
1.1.2 分布式锁特性

锁的基本特性:
分布式锁 互斥性 可重入性 锁超时 高可用 高性能 同一时刻只有一个客户端持有锁 同一客户端可以多次获取锁 防止死锁 锁服务高可用 获取锁的性能高

1.2 基于SET NX EX实现

1.2.1 基本实现

SET NX EX命令:

bash 复制代码
SET key value NX EX timeout

参数说明:

  • NX:只有当键不存在时才设置
  • EX:设置过期时间(秒)

实现流程:
Client1 Client2 Redis SET lock:resource "client1" NX EX 10 OK (获取成功) SET lock:resource "client2" NX EX 10 (nil) (获取失败) 执行业务逻辑 DEL lock:resource OK (释放锁) Client1 Client2 Redis

1.2.2 实现代码

Python实现:

python 复制代码
import redis
import time
import uuid

class DistributedLock:
    def __init__(self, redis_client, key, timeout=10):
        self.redis = redis_client
        self.key = key
        self.timeout = timeout
        self.identifier = str(uuid.uuid4())
    
    def acquire(self):
        """获取锁"""
        end = time.time() + self.timeout
        while time.time() < end:
            # 尝试获取锁
            if self.redis.set(self.key, self.identifier, nx=True, ex=self.timeout):
                return True
            time.sleep(0.001)  # 短暂等待后重试
        return False
    
    def release(self):
        """释放锁"""
        # Lua脚本保证原子性
        script = """
        if redis.call('get', KEYS[1]) == ARGV[1] then
            return redis.call('del', KEYS[1])
        else
            return 0
        end
        """
        self.redis.eval(script, 1, self.key, self.identifier)
    
    def __enter__(self):
        if self.acquire():
            return self
        raise Exception("Failed to acquire lock")
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        self.release()
1.2.3 锁超时问题

问题场景:
Client1 Redis Client2 获取锁 (超时10秒) 成功 业务执行15秒 超过锁超时时间 锁自动过期 获取锁 成功 释放锁 成功 (但锁已被Client2持有) 锁被误释放! Client1 Redis Client2

解决方案:锁续期(Watch Dog)

python 复制代码
import threading

class DistributedLockWithRenew:
    def __init__(self, redis_client, key, timeout=10):
        self.redis = redis_client
        self.key = key
        self.timeout = timeout
        self.identifier = str(uuid.uuid4())
        self.renew_thread = None
        self.locked = False
    
    def acquire(self):
        if self.redis.set(self.key, self.identifier, nx=True, ex=self.timeout):
            self.locked = True
            # 启动续期线程
            self.renew_thread = threading.Thread(target=self._renew)
            self.renew_thread.daemon = True
            self.renew_thread.start()
            return True
        return False
    
    def _renew(self):
        """续期线程"""
        while self.locked:
            time.sleep(self.timeout / 3)  # 在过期前1/3时间续期
            if self.locked:
                script = """
                if redis.call('get', KEYS[1]) == ARGV[1] then
                    return redis.call('expire', KEYS[1], ARGV[2])
                else
                    return 0
                end
                """
                self.redis.eval(script, 1, self.key, self.identifier, self.timeout)
    
    def release(self):
        self.locked = False
        if self.renew_thread:
            self.renew_thread.join()
        # 释放锁
        script = """
        if redis.call('get', KEYS[1]) == ARGV[1] then
            return redis.call('del', KEYS[1])
        else
            return 0
        end
        """
        self.redis.eval(script, 1, self.key, self.identifier)

1.3 Redlock算法

1.3.1 Redlock原理

Redlock算法:

graph TD A[客户端] --> B[尝试从N个Redis实例获取锁] B --> C{获取锁的实例数 >= N/2+1?} C -->|是| D[计算锁的有效时间] C -->|否| E[获取失败] D --> F[锁的有效时间 = 锁超时时间 - 获取锁耗时] F --> G{有效时间 > 0?} G -->|是| H[获取成功] G -->|否| I[获取失败,释放所有锁] style H fill:#ccffcc style E fill:#ffcccc style I fill:#ffcccc

算法步骤:

  1. 获取当前时间T1
  2. 依次尝试从N个Redis实例获取锁
  3. 计算获取锁的总耗时T2
  4. 如果获取锁的实例数 >= N/2+1,且有效时间 > 0,则获取成功
  5. 否则释放所有已获取的锁
1.3.2 Redlock实现

Python实现:

python 复制代码
import time
import random

class Redlock:
    def __init__(self, redis_clients, retry_count=3, retry_delay=0.2):
        self.redis_clients = redis_clients
        self.quorum = len(redis_clients) // 2 + 1
        self.retry_count = retry_count
        self.retry_delay = retry_delay
    
    def acquire(self, resource, ttl):
        """获取锁"""
        identifier = str(uuid.uuid4())
        retry = 0
        
        while retry < self.retry_count:
            start_time = time.time()
            n = 0
            
            # 尝试从所有实例获取锁
            for redis_client in self.redis_clients:
                try:
                    if redis_client.set(resource, identifier, nx=True, ex=ttl):
                        n += 1
                except:
                    pass
            
            # 计算有效时间
            elapsed = time.time() - start_time
            drift = ttl * 0.01 + 0.002  # 时钟漂移
            validity = ttl - elapsed - drift
            
            # 检查是否获取成功
            if n >= self.quorum and validity > 0:
                return {
                    'validity': validity,
                    'resource': resource,
                    'identifier': identifier
                }
            
            # 释放已获取的锁
            for redis_client in self.redis_clients:
                try:
                    redis_client.delete(resource)
                except:
                    pass
            
            # 随机延迟后重试
            time.sleep(random.uniform(0, self.retry_delay))
            retry += 1
        
        return None
    
    def release(self, lock):
        """释放锁"""
        script = """
        if redis.call('get', KEYS[1]) == ARGV[1] then
            return redis.call('del', KEYS[1])
        else
            return 0
        end
        """
        for redis_client in self.redis_clients:
            try:
                redis_client.eval(script, 1, lock['resource'], lock['identifier'])
            except:
                pass

1.4 分布式ID生成

1.4.1 为什么需要分布式ID?

分布式ID需求:

  • 全局唯一
  • 趋势递增
  • 高性能
  • 高可用
1.4.2 Redis自增ID

基本实现:
客户端 INCR id:key 返回自增ID

实现:

python 复制代码
class RedisIdGenerator:
    def __init__(self, redis_client, key='id:generator'):
        self.redis = redis_client
        self.key = key
    
    def next_id(self):
        """生成下一个ID"""
        return self.redis.incr(self.key)
    
    def get_current_id(self):
        """获取当前ID"""
        return self.redis.get(self.key) or 0

问题:

  • 单点故障
  • 性能瓶颈
  • ID不连续(重启后)
1.4.3 号段模式

号段模式:
应用 从Redis获取号段 号段: 1-1000 本地自增 号段用完

实现:

python 复制代码
class SegmentIdGenerator:
    def __init__(self, redis_client, key='id:segment', step=1000):
        self.redis = redis_client
        self.key = key
        self.step = step
        self.current = 0
        self.max_id = 0
    
    def next_id(self):
        if self.current >= self.max_id:
            # 获取新号段
            self._fetch_segment()
        self.current += 1
        return self.current
    
    def _fetch_segment(self):
        """获取新号段"""
        max_id = self.redis.incrby(self.key, self.step)
        self.current = max_id - self.step
        self.max_id = max_id
1.4.4 Snowflake算法

Snowflake ID结构:
64位ID 1位: 符号位 0 41位: 时间戳 10位: 机器ID 12位: 序列号

实现:

python 复制代码
import time

class SnowflakeIdGenerator:
    def __init__(self, datacenter_id, machine_id):
        # 时间戳起始点:2020-01-01 00:00:00
        self.start_timestamp = 1577836800000
        self.datacenter_id = datacenter_id  # 5位
        self.machine_id = machine_id  # 5位
        self.sequence = 0
        self.last_timestamp = -1
    
    def next_id(self):
        timestamp = self._current_timestamp()
        
        if timestamp < self.last_timestamp:
            raise Exception("时钟回退")
        
        if timestamp == self.last_timestamp:
            # 同一毫秒内,序列号自增
            self.sequence = (self.sequence + 1) & 0xFFF
            if self.sequence == 0:
                # 序列号溢出,等待下一毫秒
                timestamp = self._wait_next_millis(self.last_timestamp)
        else:
            self.sequence = 0
        
        self.last_timestamp = timestamp
        
        # 组装ID
        return ((timestamp - self.start_timestamp) << 22) | \
               (self.datacenter_id << 17) | \
               (self.machine_id << 12) | \
               self.sequence
    
    def _current_timestamp(self):
        return int(time.time() * 1000)
    
    def _wait_next_millis(self, last_timestamp):
        timestamp = self._current_timestamp()
        while timestamp <= last_timestamp:
            timestamp = self._current_timestamp()
        return timestamp

1.5 分布式锁最佳实践

1.5.1 锁超时设置

超时时间设置:

  • 根据业务执行时间设置
  • 考虑网络延迟
  • 设置合理的超时时间
1.5.2 锁释放

安全释放锁:

  • 使用Lua脚本保证原子性
  • 验证锁的标识符
  • 防止误释放其他客户端的锁
1.5.3 锁重试

重试策略:

  • 指数退避
  • 最大重试次数
  • 避免活锁

二、实践指南

2.1 分布式锁使用示例

python 复制代码
# 使用分布式锁
lock = DistributedLock(r, 'resource:lock', timeout=10)

if lock.acquire():
    try:
        # 临界区代码
        do_something()
    finally:
        lock.release()

# 使用上下文管理器
with DistributedLock(r, 'resource:lock', timeout=10):
    do_something()

2.2 分布式ID使用示例

python 复制代码
# Redis自增ID
id_gen = RedisIdGenerator(r)
id = id_gen.next_id()

# 号段模式
segment_gen = SegmentIdGenerator(r, step=1000)
id = segment_gen.next_id()

# Snowflake
snowflake = SnowflakeIdGenerator(datacenter_id=1, machine_id=1)
id = snowflake.next_id()

三、总结

3.1 关键知识点回顾

  1. 分布式锁

    • SET NX EX基本实现
    • 锁超时和续期
    • Redlock算法
  2. 分布式ID

    • Redis自增ID
    • 号段模式
    • Snowflake算法

3.2 最佳实践

  1. 分布式锁

    • 合理设置超时时间
    • 使用Lua脚本释放锁
    • 考虑使用Redlock提高可靠性
  2. 分布式ID

    • 根据场景选择方案
    • 考虑性能和可用性
    • 注意ID的唯一性

下一篇预告: 第10篇将深入讲解Redis监控、运维与故障排查,包括监控指标、日志分析、性能监控和故障排查方法。


相关推荐
赵得C3 小时前
软件设计师进阶知识点解析:分布式与数据应用考点精讲
java·开发语言·分布式·设计模式
云和数据.ChenGuang3 小时前
openEuler 上安装与部署 Redis 运维教程
运维·数据库·redis·运维工程师·运维技术
妮妮喔妮3 小时前
Redis Cluster故障处理机制
java·数据库·redis
500843 小时前
鸿蒙 Flutter 分布式数据同步:DistributedData 实时协同实战
分布式·flutter·华为·electron·开源·wpf·音视频
欧克小奥3 小时前
Redis单节点分片集群实现
数据库·redis·缓存
不穿格子的程序员3 小时前
Redis篇1——Redis深度剖析:从 5 种对象到 6 大底层结构
数据库·redis·缓存·redis五大数据类型·redis六大数据结构
没有腰的嘟嘟嘟3 小时前
从 0 到 1:我如何用 Spring Boot 3 + Redis 打造一个生产级通用幂等与防重中间件(含图解 + 代码 + 案例)
spring boot·redis·中间件·lua
小满、3 小时前
Redis:安装、主从复制、Sentinel 哨兵、Cluster 集群
数据库·redis·redis cluster·redis sentinel·redis 主从复制
song5013 小时前
鸿蒙 Flutter 图像编辑:原生图像处理与滤镜开发
图像处理·人工智能·分布式·flutter·华为·交互