"小王,咱们下周二要搞个iPhone新品限时抢购,预计瞬时流量会突破10万QPS,你可要把库存超卖的问题解决好啊!"产品经理老张拍着小王的肩膀说。
小王盯着电脑屏幕上的秒杀系统架构图,额头渗出细密的汗珠,去年双十一就因为分布式锁没处理好,导致同一部手机被卖了两次,公司赔了钱不说,技术团队还被老板点名批评,这次他下定决心要彻底搞懂Redis分布式锁的颗粒度控制问题。
当多个服务实例同时操作共享资源时(比如库存扣减),传统的单机锁就失效了,想象一下,你有10台服务器都在处理"剩余最后1件"的商品订单,如果没有分布式锁协调,很可能这件商品会被卖出10次。
Redis因为其单线程特性、高性能和丰富的原子操作,成为实现分布式锁的热门选择,但要用好它,必须理解锁的"颗粒度"这个关键概念。
# 伪代码示例:全局锁实现 def deduct_stock(): lock_key = "global_inventory_lock" # 获取全局锁 if redis.setnx(lock_key, 1): redis.expire(lock_key, 10) try: # 业务处理 update_inventory() finally: redis.delete(lock_key) else: raise Exception("系统繁忙,请重试")
这种锁把整个库存系统当成一个整体来锁定,虽然能防止超卖,但所有商品操作都要排队,相当于把并发的优势完全抛弃了,在抢购场景下,99%的请求都在等待那1%正在处理的请求。
def deduct_stock(product_id): lock_key = f"product_lock_{product_id}" # 获取商品级别锁 if redis.set(lock_key, 1, nx=True, ex=10): try: # 只锁定这个商品的操作 update_single_product_inventory(product_id) finally: redis.delete(lock_key) else: raise Exception("当前商品购买人数过多,请稍候")
这是最常见的做法,每个商品ID一个独立的锁,不同商品之间互不影响,iPhone14和iPhone15的抢购可以并行处理,但如果有10万人同时抢购iPhone14,这些请求还是得一个个排队。
def deduct_stock(product_id, user_id): # 将库存分成10个分片 slot = hash(user_id) % 10 lock_key = f"inventory_slot_{product_id}_{slot}" # 获取库存分片锁 if redis.set(lock_key, 1, nx=True, ex=5): try: # 操作对应分片的库存 update_inventory_slot(product_id, slot) finally: redis.delete(lock_key) else: raise Exception("抢购太火爆,请重试")
这是真正的高并发解决方案,我们把100件库存分成10个分片(每个分片10件),每个分片独立加锁,基于用户ID哈希决定操作哪个分片,这样10个用户理论上可以同时购买而不会冲突,实际场景中,可以将分片数设置得更高。
set key value NX EX 10
这条命令必须是原子操作,分开执行setnx和expire可能会导致死锁,NX表示"不存在时才设置",EX设置过期时间(单位秒)。
lock_value = str(uuid.uuid4()) # 生成唯一标识 if redis.set(lock_key, lock_value, nx=True, ex=10): try: # 业务处理 finally: # 只有锁的持有者才能删除 if redis.get(lock_key) == lock_value: redis.delete(lock_key)
防止误删其他客户端的锁:A客户端获取锁后处理时间过长导致锁自动释放,B客户端获得锁,这时A处理完直接删除就会误删B的锁。
对于可能长时间执行的任务,需要单独起线程定期检查并延长锁的过期时间:
def renew_lock(): while running: if redis.ttl(lock_key) < 3: # 剩余时间不足3秒 redis.expire(lock_key, 10) # 续期10秒 time.sleep(1) # 每秒检查一次
全局锁:适用于必须严格串行的场景,如整个系统的维护模式开关
业务实体锁(商品/订单级别):大多数业务场景的最佳选择,平衡了安全性和性能
分片锁:极端高并发场景的终极武器,但实现复杂度最高,需要处理分片间的平衡问题
混合模式:可以组合使用,比如商品维度锁+关键操作使用更细粒度的锁
某电商平台在2024年大促时的数据对比:
锁类型 | QPS上限 | 超卖发生次数 | 平均响应时间 |
---|---|---|---|
全局锁 | 500 | 0 | 1200ms |
商品维度锁 | 15000 | 2(网络超时) | 150ms |
库存分片锁 | 85000 | 0 | 50ms |
这个案例显示,通过将100个热门商品的库存分成1000个虚拟分片(每个分片10%库存),他们成功将峰值处理能力提升了170倍。
死锁预防:一定要设置过期时间,并且确保业务代码不会无限期阻塞
锁等待:不要用死循环抢锁,建议指数退避重试(如第一次等100ms,第二次等200ms...)
时钟漂移:多台服务器时间不同步可能导致锁提前释放,建议使用Redis的自身时间
锁冲突监控:记录锁等待时间和冲突次数,这些是系统瓶颈的重要指标
# 监控示例 start_time = time.time() if not acquire_lock(): statsd.increment("lock.conflict") # 锁冲突计数 wait_time = time.time() - start_time statsd.timing("lock.wait_time", wait_time) # 记录等待时间
随着Redis 7.0及以上版本对脚本和事务的增强,分布式锁的实现可以更加可靠,比如使用新出的PXAT
选项可以设置精确的毫秒级过期时间,而Lua脚本的优化让原子操作更加高效。
不过无论技术如何发展,理解业务需求、合理选择锁颗粒度的基本原则不会改变。锁的粒度越细,并发度越高,但系统复杂度也越高,好的架构师知道如何在二者间找到最佳平衡点。
下次当你设计秒杀系统时,不妨问问自己:这个锁的颗粒度是否还能更精细?是否有不必要的全局等待?通过Redis分布式锁的精准控制,你的系统将能在高并发洪流中稳如磐石。
本文由 夕智伟 于2025-08-03发表在【云服务器提供商】,文中图片由(夕智伟)上传,本平台仅提供信息存储服务;作者观点、意见不代表本站立场,如有侵权,请联系我们删除;若有图片侵权,请您准备原始证明材料和公证书后联系我方删除!
本文链接:https://vps.7tqx.com/wenda/527670.html
发表评论