好的,没问题!直接开始我们的Redis分布式锁之旅!
各位观众,晚上好!我是今晚的锁匠——锁住并发Bug,打开性能之门的锁匠! 今天咱们要聊聊Redis分布式锁,这玩意儿就像咱们生活中的锁,目的是为了保护共享资源,防止多个进程(或者线程)同时修改导致数据混乱。
第一幕:锁,锁,锁,锁住我的心(和资源)
想象一下,电商秒杀活动,100件商品,成千上万的人同时抢购,如果没锁住库存,101个人都显示抢购成功,那就尴尬了! 这时候,就需要分布式锁来保证只有一个人能成功扣减库存。
为什么要用Redis?
- 高性能: Redis是基于内存的,速度快,能抗住高并发。
- 简单易用: Redis命令简单直观,容易上手。
- 天然分布式: Redis集群模式本身就支持分布式环境。
第二幕:Setnx的独角戏:简单但有缺陷
最简单的实现方式就是SETNX
(SET if Not eXists)命令。 意思是,如果Key不存在,就设置这个Key的值,如果存在,就什么也不做。
代码示例 (Python):
import redis
import time
import uuid
class SimpleRedisLock:
def __init__(self, redis_client, lock_name, expire_time=10):
self.redis_client = redis_client
self.lock_name = lock_name
self.expire_time = expire_time # 锁的过期时间,秒
def acquire_lock(self):
lock_value = str(uuid.uuid4()) #生成唯一的锁value,防止误删
lock_key = "lock:" + self.lock_name #加上前缀,方便管理
acquired = self.redis_client.setnx(lock_key, lock_value)
if acquired:
self.redis_client.expire(lock_key, self.expire_time) #设置过期时间
return lock_value # 返回锁value,用于解锁验证
else:
return None # 获取锁失败
def release_lock(self, lock_value):
lock_key = "lock:" + self.lock_name
# 使用lua脚本保证原子性
script = """
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
"""
result = self.redis_client.eval(script, 1, lock_key, lock_value)
return result == 1
if __name__ == '__main__':
redis_client = redis.Redis(host='localhost', port=6379, db=0)
lock = SimpleRedisLock(redis_client, "my_resource")
lock_value = lock.acquire_lock()
if lock_value:
try:
print("获取到锁,开始处理资源...")
time.sleep(5) # 模拟处理资源
print("处理资源完成,释放锁...")
finally:
if lock.release_lock(lock_value):
print("锁释放成功")
else:
print("锁释放失败")
else:
print("未能获取到锁,请稍后重试")
原理:
- 加锁: 使用
SETNX
尝试设置锁的Key,如果设置成功,说明获取到锁。 - 设置过期时间: 为了防止程序崩溃导致锁无法释放,需要设置一个过期时间。
- 解锁: 删除锁的Key。
存在的问题:
- 原子性问题:
SETNX
和EXPIRE
不是原子操作。 如果SETNX
成功后,程序崩溃,EXPIRE
就无法执行,导致死锁。 - 误删问题: 如果锁的过期时间设置太短,任务还没执行完,锁就被自动释放了,后面的请求可能会误删别人的锁。
- 重入性问题: 同一个线程/进程无法重复获取锁。
改进:
- 使用
SET
命令的NX
和EX
参数: Redis 2.6.12以后,SET
命令支持NX
(Not eXists) 和EX
(EXpire time in seconds) 参数,可以将SETNX
和EXPIRE
合并成一个原子操作。
def acquire_lock(self):
lock_value = str(uuid.uuid4())
lock_key = "lock:" + self.lock_name
acquired = self.redis_client.set(lock_key, lock_value, nx=True, ex=self.expire_time)
if acquired:
return lock_value
else:
return None
- 解锁时校验Value: 为了防止误删别人的锁,解锁时需要判断锁的Value是否是自己的Value。 可以使用Lua脚本保证原子性。 上面的代码已经包含了这个改进。
第三幕:Redlock:终极解决方案?
Redlock是Redis官方推荐的分布式锁算法,旨在解决单Redis节点故障导致锁失效的问题。 它通过在多个独立的Redis节点上加锁,只有超过半数的节点加锁成功,才认为获取锁成功。
原理:
- 获取当前时间: 获取当前时间戳。
- 尝试在N个Redis节点上加锁: 依次尝试在N个Redis节点上使用相同的Key和Value加锁。 加锁时设置一个超时时间,如果在超时时间内未能加锁成功,则跳过该节点。
- 判断是否加锁成功: 如果超过半数的节点加锁成功,且加锁总耗时小于锁的有效时间,则认为获取锁成功。
- 释放锁: 如果加锁成功,则需要释放所有节点上的锁。 如果加锁失败,也要释放所有节点上的锁。
代码示例 (Python):
import redis
import time
import uuid
class Redlock:
def __init__(self, redis_nodes, lock_name, lock_timeout=10, retry_delay=0.2, retry_count=3):
self.redis_nodes = redis_nodes # Redis节点列表,例如:[{'host': '127.0.0.1', 'port': 6379}, {'host': '127.0.0.1', 'port': 6380}, ...]
self.lock_name = lock_name
self.lock_timeout = lock_timeout # 锁的超时时间,秒
self.retry_delay = retry_delay # 重试延迟,秒
self.retry_count = retry_count # 重试次数
self.quorum = len(redis_nodes) // 2 + 1 # 大多数节点的数量
def acquire_lock(self):
lock_value = str(uuid.uuid4())
lock_key = "redlock:" + self.lock_name
retry = 0
while retry < self.retry_count:
acquired_count = 0
start_time = time.time()
for node in self.redis_nodes:
try:
redis_client = redis.Redis(**node)
acquired = redis_client.set(lock_key, lock_value, nx=True, ex=self.lock_timeout)
if acquired:
acquired_count += 1
except Exception as e:
print(f"Error acquiring lock on node {node}: {e}")
elapsed_time = time.time() - start_time
is_valid = acquired_count >= self.quorum and elapsed_time < self.lock_timeout
if is_valid:
return lock_value
else:
# 释放所有节点上的锁
self._release_lock(lock_value)
time.sleep(self.retry_delay)
retry += 1
return None
def _release_lock(self, lock_value):
lock_key = "redlock:" + self.lock_name
script = """
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
"""
for node in self.redis_nodes:
try:
redis_client = redis.Redis(**node)
redis_client.eval(script, 1, lock_key, lock_value)
except Exception as e:
print(f"Error releasing lock on node {node}: {e}")
def release_lock(self, lock_value):
self._release_lock(lock_value)
if __name__ == '__main__':
redis_nodes = [
{'host': 'localhost', 'port': 6379, 'db': 0},
{'host': 'localhost', 'port': 6380, 'db': 0},
{'host': 'localhost', 'port': 6381, 'db': 0}
]
lock = Redlock(redis_nodes, "my_resource")
lock_value = lock.acquire_lock()
if lock_value:
try:
print("获取到Redlock锁,开始处理资源...")
time.sleep(5) # 模拟处理资源
print("处理资源完成,释放Redlock锁...")
finally:
lock.release_lock(lock_value)
print("Redlock锁释放成功")
else:
print("未能获取到Redlock锁,请稍后重试")
注意事项:
- 节点数量: Redlock需要奇数个Redis节点,通常是3或5个。
- 故障转移: Redlock依赖于所有Redis节点的时钟同步,如果时钟不同步,可能会导致锁失效。
- 性能: Redlock需要在多个节点上加锁,性能比单节点锁要差。
Redlock的争议:
Redlock虽然是官方推荐的算法,但也存在一些争议。 有些人认为Redlock并不能完全解决分布式锁的问题,比如网络分区问题。 具体可以参考Martin Kleppmann的文章: "How to do distributed locking correctly"。
第四幕:总结与选择
特性 | Setnx (改进版) | Redlock |
---|---|---|
实现难度 | 简单 | 复杂 |
性能 | 高 | 较低 |
可靠性 | 较低 | 较高 (理论上) |
适用场景 | 并发不高 | 对可靠性要求高,允许一定性能损耗 |
抗单点故障 | 否 | 是 |
复杂度 | 低 | 高 |
是否需要时钟同步 | 否 | 是,节点之间需要时钟同步,否则可能出现问题 |
如何选择?
- 并发不高,对可靠性要求不高: 使用
SET
命令的NX
和EX
参数即可。 - 并发高,对可靠性要求高: 可以考虑Redlock,但需要充分了解其原理和风险。 同时,需要对Redis节点进行监控,确保时钟同步。
- 不差钱: 直接用Zookeeper,虽然性能不如Redis,但可靠性更高。
最佳实践:
- 设置合理的过期时间: 过期时间要足够长,保证任务能执行完成,但又不能太长,防止死锁。
- 解锁时校验Value: 防止误删别人的锁。
- 使用Lua脚本保证原子性: 防止并发操作导致数据不一致。
- 监控Redis节点: 及时发现并处理故障。
第五幕:未来的锁:更智能,更可靠
分布式锁是一个不断发展的领域。 未来,可能会出现更智能,更可靠的分布式锁解决方案。 比如,基于RAFT或者Paxos算法的分布式锁,可以提供更强的容错性和一致性。
最后,记住一句真理:没有银弹!
选择合适的分布式锁方案,需要根据具体的业务场景和需求进行权衡。 不要盲目追求高大上的技术,适合自己的才是最好的。
感谢各位的观看,希望今天的分享对大家有所帮助! 散会!