会话存储与排行榜模式
使用 ElastiCache 将 HTTP 会话状态从应用服务器卸载出来,并利用 Redis 有序集合实现实时排行榜
会话存储与排行榜模式 是 CoddyKit 上的免费 Cloud & IT Cert Prep 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Cloud & IT Cert Prep 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Cloud & IT Cert Prep 课程共包含 4 节课。
服务器端会话的问题
传统 Web 应用程序将会话数据存储在服务器内存中。单服务器环境可以正常工作,但进行水平扩展后就会出现问题——如果用户后续的请求被路由到另一台 EC2 实例,该实例并不知道用户的会话信息,用户也就会被注销。粘性会话(负载均衡器中的会话亲和性)可以部分解决此问题,但会降低负载均衡的效果。可扩展的解决方案是将会话状态移至一个所有实例都能访问的共享低延迟存储中,这正是 ElastiCache Redis 所提供的能力。
使用 Redis 存储会话
将会话存储在Redis中可以获得以下能力:所有应用程序服务器都能以亚毫秒级延迟读取会话;使用内置 TTL 自动使会话过期;通过原子会话更新避免竞态条件;以及通过删除 Key 立即使会话失效。应用程序将会话 ID 存储在 Cookie 中;每次请求到达时,应用程序都会使用会话 ID 在 Redis 中查找并获取会话数据。所有应用程序服务器共享同一个 Redis,因此任何服务器都可以处理任何用户的请求。
# Session storage with Redis (Python Flask example)
import redis, json, uuid
from datetime import timedelta
redis_client = redis.Redis(host='prod-redis-primary', port=6379)
SESSION_TTL = int(timedelta(hours=8).total_seconds())
def create_session(user_id):
session_id = str(uuid.uuid4())
session_data = {'user_id': user_id, 'logged_in': True}
redis_client.setex(f'session:{session_id}', SESSION_TTL, json.dumps(session_data))
return session_id
def get_session(session_id):
data = redis_client.get(f'session:{session_id}')
return json.loads(data) if data else None会话 TTL 与滑动过期
固定 TTL 表示会话在创建 N 秒后过期,无论期间是否有活动。滑动 TTL(每次访问时延长过期时间)更符合用户习惯——会话会在最后一次访问后的 N 秒过期。在 Redis 中,可以在每次成功读取会话时,对会话 Key 调用 EXPIRE(或 EXPIREAT)来实现滑动 TTL,从而重置过期倒计时。这样可以确保活跃用户不会意外退出,同时让不活跃的会话自动过期并释放内存。
# Sliding TTL session implementation
def get_session_with_sliding_ttl(session_id, redis_client, ttl_seconds=1800):
session_key = f'session:{session_id}'
# Pipeline: GET + EXPIRE in one round trip
pipe = redis_client.pipeline()
pipe.get(session_key)
pipe.expire(session_key, ttl_seconds) # Reset TTL on access
results = pipe.execute()
data = results[0]
if data:
return json.loads(data)
return None # Session expired or not found在 Redis 中实现购物车
电子商务购物车非常适合使用 Redis。每个购物车都存储为一个 Redis Hash,字段是产品 SKU,值是数量。使用 HINCRBY 和 HDEL 等 Hash 操作,可以在不读取和重写整个购物车的情况下执行原子更新。再结合 TTL(使放弃的购物车在 24 小时后过期),Redis 就能提供快速、持久的购物车存储,而无需为每次添加到购物车的操作都使用关系数据库。
# Shopping cart operations using Redis Hash
cart_key = f'cart:{user_id}'
# Add item (or increase quantity)
# HINCRBY cart:user42 SKU-001 2
redis_client.hincrby(cart_key, 'SKU-001', 2)
# Remove item
# HDEL cart:user42 SKU-001
redis_client.hdel(cart_key, 'SKU-001')
# Get all items in cart
# HGETALL cart:user42
cart = redis_client.hgetall(cart_key) # {b'SKU-001': b'2', b'SKU-002': b'1'}
# Set TTL for cart abandonment (24 hours)
redis_client.expire(cart_key, 86400)排行榜架构
实时排行榜是 Redis 的经典应用场景之一,由有序集合(ZSET)提供支持。每个玩家条目都有一个分数;有序集合始终按照分数升序维护成员。排行榜查询(前 N 名玩家、玩家排名、指定分数范围内的玩家)的复杂度为 O(log n) 或 O(log n + m),即使面对数百万玩家也极其迅速。Redis 有序集合为许多游戏、健身和社交排名功能提供了基础,无需编写复杂的数据库查询,也不必在每次查看页面时重新计算排名。
# Real-time leaderboard with Redis Sorted Set
# Add or update a player's score
# ZADD game:weekly:leaderboard 15750 'player:alice'
redis_client.zadd('game:weekly:leaderboard', {'player:alice': 15750})
# Increment score (atomic)
# ZINCRBY game:weekly:leaderboard 500 'player:alice'
redis_client.zincrby('game:weekly:leaderboard', 500, 'player:alice')
# Get top 10 players (highest scores first)
# ZREVRANGE game:weekly:leaderboard 0 9 WITHSCORES
top_10 = redis_client.zrevrange('game:weekly:leaderboard', 0, 9, withscores=True)玩家排名和附近玩家
除了“显示前 10 名”之外,排行榜还常见的两个功能是显示玩家排名和显示某个玩家附近的玩家。使用 Redis 有序集合可以轻松实现这两个功能。ZREVRANK会按照分数降序返回玩家的从 0 开始的排名。要显示某个玩家上方和下方各 5 名玩家,可以先获取该玩家的排名,然后使用ZREVRANGE查询从排名减 5 到排名加 5 的范围。这样只需执行两条 Redis 命令,就能提供个性化的排行榜视图,无需复杂的 SQL 窗口函数。
# Get Alice's rank (0-indexed, so add 1 for display)
# ZREVRANK game:weekly:leaderboard 'player:alice'
rank = redis_client.zrevrank('game:weekly:leaderboard', 'player:alice')
print(f'Alice is rank #{rank + 1}')
# Get 5 players above and below Alice
start = max(0, rank - 5)
end = rank + 5
nearby = redis_client.zrevrange(
'game:weekly:leaderboard', start, end, withscores=True
)
print('Players near Alice:', nearby)使用 Redis 进行速率限制
速率限制(限制客户端在某个时间窗口内可以发出的请求数量)是 Redis 的另一个高价值应用场景。滑动窗口算法使用一个有序集合,其中每个成员都是一个请求时间戳。每次收到请求时:移除早于窗口起点的成员,统计剩余成员数量;如果数量超过限制则拒绝请求,然后添加新的时间戳。该方法可以实现精确到毫秒的滑动窗口速率限制,比固定窗口计数器准确得多,同时不会产生数据库开销。
# Sliding window rate limiter (100 requests per 60 seconds)
import time
def is_rate_limited(user_id, redis_client, limit=100, window_seconds=60):
key = f'ratelimit:{user_id}'
now = time.time()
window_start = now - window_seconds
pipe = redis_client.pipeline()
pipe.zremrangebyscore(key, '-inf', window_start) # Remove old
pipe.zcard(key) # Count current
pipe.zadd(key, {str(now): now}) # Add this request
pipe.expire(key, window_seconds)
results = pipe.execute()
request_count = results[1]
return request_count >= limit # True = rate limited使用 Redis 实现分布式锁
分布式锁用于协调多个应用服务器对共享资源的独占访问。Redis 的 SET key value NX EX ttl命令提供原子的锁获取操作:仅当键不存在时才设置该键(NX = Not eXists),并设置 TTL,以便在持有锁的进程崩溃时避免死锁。操作完成后,持有者会删除该键。Redlock 算法使用多个 Redis 节点形成法定人数机制,可以提供更健壮的分布式锁,但也会增加复杂性。对于大多数应用场景,使用单个 Redis 节点实现的锁已经足够。
# Distributed lock with Redis SET NX EX
import uuid
def acquire_lock(redis_client, resource, ttl_seconds=30):
lock_id = str(uuid.uuid4()) # Unique ID to identify this lock holder
key = f'lock:{resource}'
acquired = redis_client.set(key, lock_id, nx=True, ex=ttl_seconds)
return lock_id if acquired else None
def release_lock(redis_client, resource, lock_id):
key = f'lock:{resource}'
# Only delete if we still own the lock (Lua script for atomicity)
lua = 'if redis.call("get",KEYS[1])==ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end'
redis_client.eval(lua, 1, key, lock_id)会话存储:ElastiCache 与 DynamoDB
ElastiCache Redis 和 DynamoDB 都可以存储会话数据,但二者的权衡不同。ElastiCache Redis:微秒级延迟、基于内存(除非启用持久化,否则数据易失)、数据模型更简单,但需要 VPC。DynamoDB:个位数毫秒级延迟(DAX 可以达到与 Redis 相当的性能)、完全托管,无需维护集群,默认具备持久性,可通过全局表在全球访问,并支持按需容量的无服务器模式。对于 SAA-C03 考试:如果题目强调微秒级延迟或复杂的内存中操作,请选择 Redis;如果强调持久性、无服务器或全球规模,请考虑 DynamoDB。
使用 Redis 进行地理空间索引
Redis 内置地理空间数据类型(GEO命令),可以存储纬度和经度坐标并支持邻近查询。使用 GEOADD、GEODIST 和 GEORADIUS(在 Redis 6.2 中现为 GEOSEARCH),可以在 O(n + log n) 时间内查找指定点一定半径范围内的所有位置。应用场景包括:查找附近的司机(网约车)、查找 5 公里以内的餐厅,以及按距离对搜索结果排序。这样无需额外的地理空间数据库,同时可以保持内存级别的位置查询速度。
# Store driver locations
# GEOADD drivers 13.361389 38.115556 'driver:001'
# GEOADD drivers 15.087269 37.502669 'driver:002'
# Find all drivers within 10 km of a point
# GEOSEARCH drivers FROMLONLAT 13.5 38.1 BYRADIUS 10 km ASC COUNT 5 WITHCOORD
# Result: sorted list of driver IDs within 10 km with coordinates使用 HyperLogLog 统计独立访客
HyperLogLog 是一种概率型数据结构,可以使用固定的内存量估算集合中独立元素的数量;无论添加多少个独立项目,Redis 中都只需 12 KB 内存。它提供约 0.81% 的标准误差。使用 PFADD 添加元素,使用 PFCOUNT 获取估算值。当不要求精确计数且重视内存效率时,这非常适合统计每日独立活跃用户、独立页面浏览量或独立 IP 地址。若将数百万个独立用户 ID 存储为 Redis Set,会占用数 GB;而 HyperLogLog 只需 12 KB。
# Count unique daily visitors using HyperLogLog
date = '2024-01-15'
hll_key = f'unique_visitors:{date}'
# Track a visitor (PFADD is idempotent for the same user)
# PFADD unique_visitors:2024-01-15 'user:12345'
redis_client.pfadd(hll_key, 'user:12345')
redis_client.pfadd(hll_key, 'user:67890')
redis_client.pfadd(hll_key, 'user:12345') # Duplicate — not counted again
# Get estimated unique visitor count
# PFCOUNT unique_visitors:2024-01-15
count = redis_client.pfcount(hll_key)
print(f'Unique visitors today (estimate): {count}')快速检查
请测试您对本课 AWS Solutions Architect(SAA-C03)相关概念的理解。
课程回顾
本课您学到了:Redis 会话存储通过让所有实例以亚毫秒级延迟访问共享会话状态,支持无状态水平扩展;Redis 有序集合通过 O(log n) 的排名查询支持实时排行榜;专用 Redis 数据类型(用于统计独立数量的 HyperLogLog、用于邻近查询的 GEO、分布式锁)可以高效解决常见的架构问题。至此,ElastiCache 缓存课程已经完成;接下来我们将学习高可用性和容错架构。
常见问题解答
「会话存储与排行榜模式」课时是免费的吗?
是的 — 「会话存储与排行榜模式」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Cloud & IT Cert Prep 课程的其余内容,请升级到 CoddyKit PRO。 Cloud & IT Cert Prep 课程共包含 4 节课。
「会话存储与排行榜模式」这节课中我会学到什么?
使用 ElastiCache 将 HTTP 会话状态从应用服务器卸载出来,并利用 Redis 有序集合实现实时排行榜 你通过在浏览器中直接运行的动手代码来练习 Cloud & IT Cert Prep,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Cloud & IT Cert Prep 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Cloud & IT Cert Prep 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。
「会话存储与排行榜模式」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Cloud & IT Cert Prep 课中编写并运行代码吗?
能。每节 Cloud & IT Cert Prep 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。