游戏后端分布式学习——分布式锁与选主

游戏后端分布式学习——分布式锁与选主 概念为何引入分布式锁分布式锁是最直接、最成熟的方案尤其适合短时间、高频次的互斥操作对一致性要求极高不能出现重复奖励需要跨服务器协调分布式锁在mmo游戏中常见场景简易分布式锁实现用于后续场景中分布式锁的使用importtimeimportuuidimportredisclassDistributedLock:def__init__(self,redis_client,lock_key,expire10):self.redisredis_client self.lock_keylock_key self.expireexpire self.owner_idstr(uuid.uuid4())# 唯一标识持有者defacquire(self):尝试获取锁成功返回 True失败返回 False# SET key value NX EX seconds —— 原子操作resultself.redis.set(self.lock_key,self.owner_id,nxTrue,exself.expire)returnresultisnotNone# Redis 返回 OK 表示成功defrelease(self):释放锁只释放属于自己的锁# 使用 Lua 脚本保证原子性检查 owner 再删除lua_script if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end self.redis.eval(lua_script,1,self.lock_key,self.owner_id)def__enter__(self):self.acquire()returnselfdef__exit__(self,exc_type,exc_val,exc_tb):self.release()限量活动发放全服只发 100 份限量皮肤10 万玩家同时抢——需要锁保证发到第 100 份时立即停止。场景全服限量 100 份绝版坐骑10 万玩家同时抢 并发极高瞬间万级 QPS 幂等重复发放意味着超发资损必须用分布式锁 Lua 原子扣减-- 限量活动发放的原子操作locallua_script[[ local remain_key KEYS[1] local lock_key KEYS[2] local token ARGV[1] local player_id ARGV[2] local item_id ARGV[3] -- 尝试获取锁 if redis.call(SET, lock_key, token, NX, EX, 5) then local remain redis.call(GET, remain_key) if remain and tonumber(remain) 0 then redis.call(DECR, remain_key) -- 发放物品这里简化实际需要调用游戏服 API redis.call(LPUSH, item_grant_queue, player_id .. : .. item_id) redis.call(DEL, lock_key) return 1 -- 成功 else redis.call(DEL, lock_key) return 0 -- 已发完 end else return -1 -- 获取锁失败 end ]]世界 Boss 击杀判定假设游戏里有 3 台战斗服务器分别承载不同区域的玩家。一只世界 Boss 血条归零的那一刻三个服务器都可能检测到自己区域内的某个玩家打出了“最后一击”。如果没有锁服务器 A 判定玩家 X 击杀 → 发放奖励服务器 B 也判定玩家 Y 击杀 → 同样发放奖励服务器 C 也可能凑热闹……因为“发放奖励”这个操作必须是互斥的——同一时刻只允许一台服务器执行。分布式锁正是用来在多个独立进程之间协调对共享资源的独占访问的完美工具。设计要点锁的粒度按 Boss 实例 ID 加锁比如 lock:boss:worldboss_001不同 Boss 互不影响。锁的超时设置合理超时比如 10 秒防止拿到锁的服务器突然宕机导致死锁。锁的持有者标识每个服务器生成唯一 ID如 server_A释放锁时检查是不是自己的锁避免误删别人的锁。原子性操作加锁和设超时要一起完成Redis 的 SET key value NX EX seconds。为什么不直接用数据库事务数据库事务比如 MySQL 行锁也可以做到互斥但性能开销大高并发下数据库会成为瓶颈。数据库连接可能不稳定事务超时回滚复杂。分布式环境下多个服务器可能连不同的数据库实例分库分表事务无法跨库。而 Redis 等内存型锁性能极高适合高频短时间的互斥操作。defworld_boss_kill_handler(boss_id,server_name): 模拟世界 Boss 击杀处理 boss_id: 世界 Boss 的唯一标识 server_name: 当前服务器的名字比如 Server-A rredis.Redis(hostlocalhost,port6379,decode_responsesTrue)lockDistributedLock(r,flock:boss:{boss_id},expire10)iflock.acquire():try:print(f[{server_name}] 抢到了 Boss{boss_id}的锁开始发放奖励...)# 这里执行真正的奖励发放逻辑检查击杀者、发邮件等time.sleep(1)# 模拟耗时操作print(f[{server_name}] 奖励发放完毕释放锁。)finally:lock.release()else:print(f[{server_name}] 没抢到锁放弃处理。)# 模拟三个服务器同时尝试处理importthreading threads[]fornamein[Server-A,Server-B,Server-C]:tthreading.Thread(targetworld_boss_kill_handler,args(worldboss_001,name))threads.append(t)t.start()fortinthreads:t.join()跨服匹配跨服匹配池是一个全局队列里面排着来自不同服务器的玩家。多个匹配服务器Match Server同时从这个队列里取人组队。如果没有锁匹配服务器 A 取了玩家 P1、P2准备组队。匹配服务器 B 也取了玩家 P1、P3准备组队。结果 P1 同时出现在两个队伍里游戏逻辑错乱。从匹配池取玩家这个操作必须是原子的——要么整个取走要么不取。分布式锁可以保证同一时刻只有一个匹配服务器在执行“取出并标记”的操作。设计要点锁的粒度按匹配池 ID 加锁比如 lock:matching:pool_1也可以更细粒度到“批次”。锁的持有时间匹配操作很快毫秒级锁超时可设为 1~2 秒。配合队列原子操作实际项目中常用 Redis 的 LPOP/RPOP 或 BLPOP 结合 Lua 脚本但分布式锁提供了额外的安全边界。防重复匹配拿到锁后还要二次校验玩家是否已被匹配双重检查模式。为什么不直接用 Redis 事务或 Lua 脚本Lua 脚本确实可以实现原子性但如果脚本执行时间较长比如涉及多个键操作会阻塞 Redis 主线程影响其他请求。而分布式锁可以让业务逻辑在应用层完成锁只保证互斥不阻塞 Redis。更常见的做法是用 Lua 脚本原子性地 pop 玩家再配合分布式锁防止多个匹配服务器同时执行该脚本。defmatch_players(match_pool_key,server_name): 从匹配池中取出一对玩家进行匹配 match_pool_key: Redis 列表的 key存放等待匹配的玩家 ID server_name: 当前匹配服务器名称 rredis.Redis(hostlocalhost,port6379,decode_responsesTrue)lockDistributedLock(r,flock:matching:{match_pool_key},expire2)iflock.acquire():try:# 拿到锁后从队列中取出两个玩家如果有player1r.lpop(match_pool_key)ifplayer1isNone:print(f[{server_name}] 匹配池为空无事可做。)returnplayer2r.lpop(match_pool_key)ifplayer2isNone:# 如果只取到一个放回去保持公平r.rpush(match_pool_key,player1)print(f[{server_name}] 匹配池人数不足将{player1}放回。)return# 执行匹配逻辑创建对局等print(f[{server_name}] 成功匹配{player1}和{player2})# 实际项目还会把匹配结果写入数据库finally:lock.release()else:print(f[{server_name}] 没抢到锁稍后重试。)# 模拟先向匹配池插入一些玩家rredis.Redis(hostlocalhost,port6379,decode_responsesTrue)r.delete(matching:pool_1)r.rpush(matching:pool_1,Player1,Player2,Player3,Player4)# 三个匹配服务器并发工作threads[]fornamein[Match-Server-1,Match-Server-2,Match-Server-3]:tthreading.Thread(targetmatch_players,args(matching:pool_1,name))threads.append(t)t.start()fortinthreads:t.join()选主在游戏中的常见场景