慢查询优化、Pipeline 事务与发布订阅实战 一、Redis 慢查询1.1 什么是慢查询我们配置一个时间阈值如果某条命令的执行时间超过这个阈值就认为它是慢查询。Redis 把慢命令记录到一个先进先出FIFO的队列中拿到一个固定长度、保存在内存中的队列。通过设置慢查询阈值以后超过时间的命令就会被放入这个队列。后期查询这个队列过滤出慢命令针对性优化。1.2 配置慢查询# 阈值设为 0记录所有命令单位微秒-1 表示不记录configsetslowlog-log-slower-than0# 最多记录 100 条configsetslowlog-max-len100# 把设置持久化到本地配置文件config rewrite1.3 查看与清理慢查询队列slowlog get[n]# 获取慢查询队列n 为可选的条数slowlog len# 获取慢查询队列长度slowlog reset# 清空慢查询队列slowlog get返回的日志由几个属性组成日志的标识 ID。发生的时间戳。命令耗时微秒。执行的命令及参数。1.4 常见慢命令与优化建议KEYS、SMEMBERS、HGETALL等全量命令是O(n)key 多时会阻塞。用SCAN、HSCAN、SSCAN游标式遍历代替全量命令。用MGET、MSET、Pipeline 减少网络往返。避免大 key如超大 List/Hash必要时做数据拆分。对比 MySQLMySQL 有slow_query_log慢查询日志思路类似Redis 通过slowlog在内存里维护慢命令队列。二、Pipeline 管道与事务2.1 什么是 PipelinePipeline管道把多条命令一次性发到 Redis减少网络往返RTT提升吞吐。redis-cli命令行本身没有 pipeline但 Redis 支持此功能且各语言客户端都有对应实现。特点Pipeline 期间会独占当前连接。多个命令放到一个管道中要么都执行要么都不执行——利用 Pipeline 可实现事务效果。它把“多条命令的网络开销”合并为一次极大提升批量效率。2.2 Pipeline 与事务的关系管道本身是“批量发送”默认不保证原子性。若要体现事务/原子性可开启transactionTrue它在底层会包装成MULTI ... EXEC。importredis poolredis.ConnectionPool(host10.0.0.101,port6379,password654321)connredis.Redis(connection_poolpool)# 创建 pipeline默认 transactionTruepipeconn.pipeline(transactionTrue)pipe.multi()# 开启事务pipe.set(name,lqz)# 中间代码可能出异常pipe.set(role,nb)pipe.execute()# 一次性执行需要单条执行、不开启事务时r.pipeline(transactionFalse)。2.3 事务四大特性ACID事务有四个基本特性原子性Atomicity事务内操作要么全部成功、要么全部失败。持久性Durability事务一旦提交结果持久保存。一致性Consistency事务前后数据完整、一致。隔离性Isolation并发事务互不干扰。Redis 有没有事务能否满足四大特性Redis 通过 PipelineMULTI/EXEC实现事务命令被一次性地、按顺序执行中间不会被其他客户端插入因此可认为支持事务。但 Redis 事务与关系型数据库不同若某条命令在执行期报错如对字符串做INCR之前的命令不会回滚只有入队期错误才会中断整个事务。因此常说的结论是Redis 事务具备“原子性、隔离性、一致性”的部分特性持久性取决于持久化配置RDB/AOF并非传统意义完整 ACID。重要限制Pipeline 只支持单实例在集群环境下跨槽位的事务不支持可通过 hash tag 让同一事务的 key 落在同一槽位或用 Lua 脚本。2.4 原生操作MULTI / EXEC / DISCARD / WATCHmulti# 开启事务之后命令先入队setname lqzsetage18exec# 执行入队命令discard# 放弃事务清空命令队列2.5 原生操作乐观锁WATCH在开启事务之前先watch某个 key。若在事务执行前该 key 被其他客户端修改则exec会返回失败事务不执行。watchage# 监视 agemulti# 开启事务decr ageexec# 若 age 在 watch 后已被修改则事务不执行返回 nil另一台机器若在decr前抢先修改了age则 watch 的事务会失败。这正是利用 Redis 实现的乐观锁。2.6 使用 Redis 乐观锁实现秒杀同步基于 WATCH用户 1importredis connredis.Redis(host127.0.0.1,port6379)withconn.pipeline()aspipe:conn.watch(count)# 先监视自己的值有没有被修改pipe.multi()# 事务开始old_countconn.get(count)countint(old_count)input(我考虑一下)ifcount0:# 有库存pipe.set(count,count-1)retpipe.execute()# 一次性推送执行print(type(ret),ret)用户 2与用户 1 几乎相同只是不input直接执行。注意Windows 下如果 watched 的 key 已被修改execute()不会抛异常而是返回一个空列表macOS 和 Linux 下会直接抛异常。因此根据len(ret) 0判断是否抢购成功更稳妥。并发秒杀测试100 个线程importredisfromthreadingimportThreaddefchoose(name,conn):withconn.pipeline()aspipe:conn.watch(count)pipe.multi()old_countconn.get(count)countint(old_count)ifcount0:pipe.set(count,count-1)retpipe.execute()iflen(ret)0:print(第 %s 个人抢购成功%name)else:print(第 %s 个人抢购失败%name)if__name____main__:connredis.Redis(host127.0.0.1,port6379)foriinrange(100):tThread(targetchoose,args(i,conn))t.start()三、发布订阅Pub/Sub3.1 概念发布订阅即观察者模式发布者发布消息所有订阅者都能收到属于生产者-消费者模型。区别于“生产者-消费者 队列”发布订阅模型一条消息广播给所有订阅者。队列模型一条消息只被其中一个消费者取走如BLPOP。3.2 原生命令publish channel message# 发布消息返回收到消息的订阅者数量subscribe channel# 订阅频道# 只要发布者一发布所有订阅者都会收到3.3 Python 实现发布者importredis rredis.Redis(host10.0.0.101,port6379,password654321)r.publish(channel,bye)订阅者importredis rredis.Redis(host10.0.0.101,port6379,password654321)subr.pubsub()sub.subscribe(channel)formessageinsub.listen():# 持续监听print(message)3.4 与消息队列的区别对比项发布订阅 Pub/Sub消息队列List BLPOP消费方式广播给所有订阅者一条消息被一个消费者取走消息持久化不持久化离线订阅者收不到消息可持久化在列表中典型场景聊天室、通知、实时播报任务队列、削峰填谷若订阅者离线发布者发布的消息会丢失fire-and-forget需要可靠投递时可使用 List BLPOP 或专业消息队列RabbitMQ、Kafka。