📚 文章概述
分布式锁和分布式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算法:
算法步骤:
- 获取当前时间T1
- 依次尝试从N个Redis实例获取锁
- 计算获取锁的总耗时T2
- 如果获取锁的实例数 >= N/2+1,且有效时间 > 0,则获取成功
- 否则释放所有已获取的锁
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 关键知识点回顾
-
分布式锁
- SET NX EX基本实现
- 锁超时和续期
- Redlock算法
-
分布式ID
- Redis自增ID
- 号段模式
- Snowflake算法
3.2 最佳实践
-
分布式锁
- 合理设置超时时间
- 使用Lua脚本释放锁
- 考虑使用Redlock提高可靠性
-
分布式ID
- 根据场景选择方案
- 考虑性能和可用性
- 注意ID的唯一性
下一篇预告: 第10篇将深入讲解Redis监控、运维与故障排查,包括监控指标、日志分析、性能监控和故障排查方法。