Redis实战:缓存、队列、限流的Python实现

4 阅读4分钟

摘要:Redis不只是缓存,它是一把瑞士军刀。本文用Python实现Redis在缓存、消息队列、分布式锁、限流器等场景的最佳实践,每个场景都有生产级代码。

连接Redis

import redis
import json
from functools import wraps

# 连接池(推荐)
pool = redis.ConnectionPool(
    host='localhost',
    port=6379,
    db=0,
    decode_responses=True,  # 自动解码为字符串
    max_connections=20,
)
r = redis.Redis(connection_pool=pool)

# 测试连接
r.ping()  # True

场景1:缓存装饰器

def redis_cache(prefix='cache', ttl=300):
    """Redis缓存装饰器"""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            # 生成缓存key
            key_parts = [prefix, func.__name__] + [str(a) for a in args]
            if kwargs:
                key_parts += [f'{k}={v}' for k, v in sorted(kwargs.items())]
            cache_key = ':'.join(key_parts)
            
            # 尝试读缓存
            cached = r.get(cache_key)
            if cached is not None:
                return json.loads(cached)
            
            # 执行函数
            result = func(*args, **kwargs)
            
            # 写入缓存
            r.setex(cache_key, ttl, json.dumps(result, ensure_ascii=False))
            return result
        
        # 手动清除缓存
        def invalidate(*args, **kwargs):
            key_parts = [prefix, func.__name__] + [str(a) for a in args]
            cache_key = ':'.join(key_parts)
            r.delete(cache_key)
        
        wrapper.invalidate = invalidate
        return wrapper
    return decorator

# 使用
@redis_cache(prefix='user', ttl=600)
def get_user_info(user_id):
    """模拟数据库查询"""
    import time
    time.sleep(0.5)  # 模拟慢查询
    return {'id': user_id, 'name': f'用户{user_id}'}

# 第一次调用:0.5s(查数据库)
info = get_user_info(123)

# 第二次调用:<1ms(读缓存)
info = get_user_info(123)

# 清除缓存
get_user_info.invalidate(123)

场景2:简易消息队列

class RedisQueue:
    """基于Redis List的消息队列"""
    
    def __init__(self, name, redis_client=None):
        self.r = redis_client or r
        self.key = f'queue:{name}'
        self.processing_key = f'queue:{name}:processing'
    
    def push(self, message):
        """发送消息"""
        self.r.lpush(self.key, json.dumps(message, ensure_ascii=False))
    
    def pop(self, timeout=0):
        """接收消息(阻塞)"""
        result = self.r.brpoplpush(self.key, self.processing_key, timeout)
        if result:
            return json.loads(result)
        return None
    
    def ack(self, message):
        """确认消息已处理"""
        self.r.lrem(self.processing_key, 1, json.dumps(message, ensure_ascii=False))
    
    def size(self):
        return self.r.llen(self.key)
    
    def processing_count(self):
        return self.r.llen(self.processing_key)

# 生产者
queue = RedisQueue('tasks')
queue.push({'type': 'send_email', 'to': 'user@test.com', 'subject': '通知'})
queue.push({'type': 'generate_report', 'report_id': 42})

# 消费者
import signal
import sys

running = True
def shutdown(sig, frame):
    global running
    running = False

signal.signal(signal.SIGINT, shutdown)

while running:
    msg = queue.pop(timeout=5)
    if msg:
        try:
            print(f'处理消息: {msg}')
            # process(msg)
            queue.ack(msg)
        except Exception as e:
            print(f'处理失败: {e}')

场景3:分布式锁

import uuid
import time

class RedisLock:
    """Redis分布式锁"""
    
    def __init__(self, name, timeout=10, redis_client=None):
        self.r = redis_client or r
        self.key = f'lock:{name}'
        self.timeout = timeout
        self.token = str(uuid.uuid4())
    
    def acquire(self, blocking=True, retry_interval=0.1):
        """获取锁"""
        while True:
            if self.r.set(self.key, self.token, nx=True, ex=self.timeout):
                return True
            if not blocking:
                return False
            time.sleep(retry_interval)
    
    def release(self):
        """释放锁(Lua脚本保证原子性)"""
        lua_script = """
        if redis.call('get', KEYS[1]) == ARGV[1] then
            return redis.call('del', KEYS[1])
        else
            return 0
        end
        """
        self.r.eval(lua_script, 1, self.key, self.token)
    
    def __enter__(self):
        self.acquire()
        return self
    
    def __exit__(self, *args):
        self.release()

# 使用
with RedisLock('update_inventory', timeout=30):
    # 这段代码同一时刻只有一个进程能执行
    current = int(r.get('inventory:item_1') or 0)
    if current > 0:
        r.set('inventory:item_1', current - 1)

场景4:滑动窗口限流器

class RateLimiter:
    """滑动窗口限流器"""
    
    def __init__(self, name, max_requests, window_seconds, redis_client=None):
        self.r = redis_client or r
        self.key = f'ratelimit:{name}'
        self.max_requests = max_requests
        self.window = window_seconds
    
    def is_allowed(self, identifier):
        """检查是否允许请求"""
        key = f'{self.key}:{identifier}'
        now = time.time()
        pipe = self.r.pipeline()
        
        # 移除窗口外的记录
        pipe.zremrangebyscore(key, 0, now - self.window)
        # 统计窗口内的请求数
        pipe.zcard(key)
        # 添加当前请求
        pipe.zadd(key, {f'{now}:{uuid.uuid4().hex[:8]}': now})
        # 设置过期时间
        pipe.expire(key, self.window)
        
        results = pipe.execute()
        current_count = results[1]
        
        return current_count < self.max_requests

# 使用:每个IP每分钟最多30次请求
limiter = RateLimiter('api', max_requests=30, window_seconds=60)

def handle_request(ip):
    if not limiter.is_allowed(ip):
        return {'error': '请求过于频繁,请稍后再试'}, 429
    return {'data': 'ok'}, 200

场景5:排行榜

class Leaderboard:
    """Redis Sorted Set实现排行榜"""
    
    def __init__(self, name, redis_client=None):
        self.r = redis_client or r
        self.key = f'leaderboard:{name}'
    
    def update_score(self, user_id, score):
        """更新分数"""
        self.r.zadd(self.key, {user_id: score})
    
    def increment_score(self, user_id, delta=1):
        """增加分数"""
        return self.r.zincrby(self.key, delta, user_id)
    
    def get_rank(self, user_id):
        """获取排名(从0开始)"""
        rank = self.r.zrevrank(self.key, user_id)
        return rank + 1 if rank is not None else None
    
    def get_score(self, user_id):
        return self.r.zscore(self.key, user_id)
    
    def top_n(self, n=10):
        """获取前N名"""
        results = self.r.zrevrange(self.key, 0, n - 1, withscores=True)
        return [{'user_id': uid, 'score': int(score), 'rank': i + 1}
                for i, (uid, score) in enumerate(results)]
    
    def around_me(self, user_id, n=5):
        """获取某用户附近的排名"""
        rank = self.r.zrevrank(self.key, user_id)
        if rank is None:
            return []
        start = max(0, rank - n)
        end = rank + n
        results = self.r.zrevrange(self.key, start, end, withscores=True)
        return [{'user_id': uid, 'score': int(score), 'rank': start + i + 1}
                for i, (uid, score) in enumerate(results)]

# 使用
lb = Leaderboard('weekly_score')
lb.update_score('user_1', 1500)
lb.update_score('user_2', 2300)
lb.increment_score('user_1', 100)

print(lb.top_n(10))
print(lb.get_rank('user_1'))

场景6:发布订阅

import threading

# 发布者
def publish_event(channel, message):
    r.publish(channel, json.dumps(message, ensure_ascii=False))

# 订阅者
def subscriber(channels):
    pubsub = r.pubsub()
    pubsub.subscribe(channels)
    
    for message in pubsub.listen():
        if message['type'] == 'message':
            data = json.loads(message['data'])
            print(f"[{message['channel']}] {data}")

# 后台运行订阅者
thread = threading.Thread(target=subscriber, args=(['notifications', 'alerts'],))
thread.daemon = True
thread.start()

# 发布消息
publish_event('notifications', {'type': 'new_order', 'order_id': 12345})

总结

Redis的核心应用场景:

  • 缓存:减少数据库压力,提升响应速度
  • 队列:异步任务处理,削峰填谷
  • 分布式锁:多进程/多机器的互斥操作
  • 限流:保护后端服务不被打垮
  • 排行榜:实时排名,Sorted Set天然适合
  • 发布订阅:简单的事件通知

选择Redis数据结构的原则:String做缓存,List做队列,Set做去重,Sorted Set做排行,Hash做对象存储。