Django 中间件全家桶背诵笔记(实测版):MySQL · Redis · MongoDB · RabbitMQ · Celery
实测环境 :Ubuntu 24.04 · Python 3.x(实测 3.12)· Django 5.2.18 · django-redis 7.0.0 · PyMySQL 1.2.3 · redis-py 8.1.0 · Redis 7.0.15 · MySQL 8.0.46 · MongoDB 8.0.32(pymongo 4.19.0)· RabbitMQ 4.3.6(pika 1.4.4)· Celery 5.6.3 + kombu 5.6.2 版本口径 :全文 Python 代码与命令均按 Python 3.x 编写,不使用 Python 2 语法;
python3命令在 Windows 本地对应python。 全部代码在真实服务器跑通,共 200+ 个知识点。✅=实测通过,⚠️=实测发现的坑/与常见写法不同
零、必记的大坑(最容易面试翻车)
| # | 坑 | 实测现象 |
|---|---|---|
| 1 | Django 6.x 要求 MySQL 8.4+ | Django 6.1 + MySQL 8.0.46 → NotSupportedError: MySQL 8.4 or later is required。MySQL 8.0 必须用 Django 5.2 LTS |
| 2 | PyMySQL 连 MySQL 8 要 cryptography | MySQL 8 默认 caching_sha2_password,PyMySQL 缺 cryptography 报认证错 |
| 3 | Django 会把会话隔离级别改成 READ-COMMITTED | 实测 MySQL 全局是 REPEATABLE-READ,但 Django 连接后 @@SESSION.transaction_isolation = READ-COMMITTED(Django 主动 SET 的) |
| 4 | django-redis 7.0 的 Sentinel 配置全变了 | 网上旧写法(LOCATION 传列表 + SENTINEL_KWARGS.service_name)在 7.0 直接报 AttributeError: 'tuple' object has no attribute 'startswith' |
| 5 | MongoDB 单节点不支持事务 | start_transaction 抛 OperationFailure: Transaction numbers are only allowed on a replica set member or mongos,多文档事务必须副本集;单文档 $inc 原子操作不受影响 |
| 6 | pymongo 4 把 wtimeout 改名 wtimeoutms | MongoClient(..., wtimeout=5000) → ConfigurationError: Unknown option: wtimeout;单条写指定写关注要 coll.with_options(write_concern=...),insert_one(write_concern=) 已移除 |
| 7 | pika 1.x 不能直接传 URL 字符串 | BlockingConnection('amqp://...') → AttributeError: 'str' object has no attribute 'connection_attempts',必须传 pika.URLParameters(url) 对象 |
| 8 | RabbitMQ 4.x 拒绝「非持久化+非排他」队列 | 声明 durable=False, exclusive=False 的队列直接被 broker 拒:Queue.declare: (541) INTERNAL_ERROR - Feature 'transient_nonexcl_queues' is deprecated。Celery 5.6 默认就声明这种队列 → worker 根本起不来(详见第十四章) |
一、包引入
bash
python3 -m pip install django django-redis PyMySQL cryptography redis
django-redis:Django 缓存层(自动 pickle + key 前缀)PyMySQL:MySQL 驱动(纯 Python,无需编译)cryptography:MySQL 8caching_sha2_password认证必需 ✅redis:redis-py 8.x,django-redis 底层依赖
驱动注册 (项目包 __init__.py,必须在 settings 加载前执行):
python
import pymysql
pymysql.install_as_MySQLdb()
二、Django 连 MySQL 完整步骤
Step 1 装包 → Step 2 配 settings → Step 3 manage.py check → Step 4 makemigrations/migrate
python
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'djtest',
'USER': 'dj', # ⚠️ 不要用 root:实测 root 是 auth_socket(1698),Django 走 TCP 登录不了
'PASSWORD': 'djpass123',
'HOST': '127.0.0.1',
'PORT': '3306',
'OPTIONS': {'charset': 'utf8mb4', 'connect_timeout': 5},
'CONN_MAX_AGE': 60, # ✅ 实测生效:两次查询 connection_id 相同(23 vs 23),连接被复用
}
}
Step 4 建表:
bash
python3 manage.py makemigrations app
python3 manage.py migrate
授权(root 执行):
sql
CREATE DATABASE djtest DEFAULT CHARSET utf8mb4;
CREATE USER 'dj'@'%' IDENTIFIED BY 'djpass123';
GRANT ALL ON djtest.* TO 'dj'@'%';
三、MySQL 增删改查(全部实测通过)
python
# ── 增 ──
User.objects.create(name='tom', email='t@x.com', age=20)
User(name='bob').save() # save 后 pk 才回填
User.objects.get_or_create(email='a@x.com', defaults={}) # 幂等,返回 (obj, created)
User.objects.bulk_create([...], batch_size=2) # 批量插入
# ── 查 ──
User.objects.get(pk=1) # 不存在抛 DoesNotExist
User.objects.filter(age__gte=18, name__startswith='a')
User.objects.exclude(age=30)
User.objects.order_by('-age')[:10]
User.objects.values('age').annotate(cnt=Count('id')) # 分组统计
User.objects.only('name').first() # 只取部分字段
User.objects.defer('email').first() # 排除字段(访问时会补发查询)
User.objects.select_related('user') # FK/O2O 预加载,防 N+1
User.objects.prefetch_related('orders') # M2M/反向FK 预加载
User.objects.exists() / .count() / .in_bulk(field_name='email')
User.objects.iterator(chunk_size=2000) # 大表流式,不进内存
User.objects.aggregate(total=Sum('age'), avg=Avg('age'), mx=Max('age'))
User.objects.raw('SELECT * FROM t_user WHERE age > %s', [18])
from django.db.models import Q
User.objects.filter(Q(name='tom') | Q(name='bob'))
# ── 改 ──
u = User.objects.get(pk=1); u.age += 1; u.save() # 单对象
User.objects.filter(pk=1).update(age=30) # 批量,不走 save
User.objects.filter(pk=1).update(balance=F('balance') - 50) # F() 数据库端运算,防读改写竞态
User.objects.update_or_create(email='a@x.com', defaults={'name': 'x'})
# ⚠️ bulk_update 要求对象已有 pk:新建对象直接 bulk_update 报
# ValueError: All bulk_update() objects must have a primary key set.
# 必须先从库里查出来再改 ✅实测
# ── 删 ──
User.objects.get(pk=1).delete() # 返回 (条数, {表: 条数})
User.objects.filter(age__lt=18).delete()
Order.objects.first().delete() # FK CASCADE 级联删 ✅实测
⚠️ aggregate 用 Sum/Avg/Max/Min/Count,不是 F('age').sum() ------ 后者报 AttributeError: 'F' object has no attribute 'sum' ✅实测
四、MySQL 事务与异常处理(全部实测通过)
python
from django.db import transaction, IntegrityError, OperationalError, DatabaseError
from django.db.models import F
@transaction.atomic
def transfer(fid, tid, money):
try:
with transaction.atomic(): # 嵌套 = savepoint
User.objects.filter(pk=fid).update(balance=F('balance') - money)
User.objects.filter(pk=tid).update(balance=F('balance') + money)
except IntegrityError:
return '唯一键/外键/非空冲突' # 1062 Duplicate entry
except OperationalError:
raise # 1205 锁等待超时 / 1213 死锁,必须退出 atomic 后重试
except DatabaseError:
raise
异常层级 :DatabaseError → IntegrityError(1062) / OperationalError(1205,1213,2003,2013) / ProgrammingError(表不存在) / DataError
实测行为表:
| 场景 | 实测结果 |
|---|---|
| atomic 内抛异常 | 数据回滚 ✅ |
| 嵌套 atomic 内层失败 | 内层 savepoint 回滚,外层-5 保留 ✅ |
| 唯一键冲突 | IntegrityError: (1062, "Duplicate entry ...") ✅ |
| 表不存在 | ProgrammingError ✅ |
| 锁等待超时 | OperationalError code=1205(设 innodb_lock_wait_timeout=1 后 1.0s 触发)✅ |
| 真实死锁(两线程交叉 update) | OperationalError code=1213 ✅ |
连接被 connection.close() 破坏 |
下次查询 Django 自动重开连接,业务无感 ✅ |
死锁重试标准写法(实测有效):
python
from django.db import connections
for attempt in range(3):
try:
with transaction.atomic():
do_something()
break
except OperationalError:
connections['default'].close() # 丢弃坏连接
time.sleep(0.1 * (2 ** attempt)) # 指数退避
- 悲观锁:
select_for_update();乐观锁:version字段 +F()或select_for_update
五、MySQL 索引知识点(EXPLAIN 实测)
sql
SHOW INDEX FROM t_user;
EXPLAIN SELECT * FROM t_user WHERE name='x' AND age=1;
EXPLAIN 重点看 type(好→坏: const>eq_ref>ref>range>index>ALL)、key、rows、Extra
索引失效实测 (表上有 name 单列索引 + (name,age) 复合索引):
| 条件 | 实测 type | 实测 key | 结论 |
|---|---|---|---|
name='x' |
ref | 单列 name 索引 | 命中 |
age=1 |
ALL | NULL | 复合索引最左前缀失效 |
name LIKE '%x' |
ALL | NULL | 前导 % 失效 |
CAST(name AS CHAR)='x' |
ALL | NULL | 字段用函数失效 |
name+''='x' |
ALL | NULL | 表达式失效 |
name != 'x' |
ALL | NULL | 否定条件失效 |
name LIKE 'x%' |
range | name 索引 | 命中 + Using index condition |
varchar列 code=100(数字) |
ALL | NULL | 隐式类型转换失效 |
varchar列 code='100' |
ref | idx_code | 命中 |
覆盖索引:
ini
SELECT name,age WHERE name='x' AND age=1 FORCE INDEX(复合索引)
-> type=ref key=复合索引 Extra=Using index ✅ 不回表
不 FORCE 时优化器选了单列 name 索引 -> Extra=Using where 需回表
⚠️ 重要实测发现 :同时有单列索引 name 和复合索引 (name, age) 时,优化器优先选单列索引 ,复合索引白白浪费。必要时用 FORCE INDEX 或删掉冗余单列索引。
其他:
- 慢查询:
SET GLOBAL slow_query_log=ON需要SYSTEM_VARIABLES_ADMIN或SUPER权限 ,普通应用账号会报1227 Access denied✅实测(root 用sudo mysql执行成功) log_bin=ON、binlog_format=ROW、server_id=1、gtid_mode=OFF、innodb_flush_log_at_trx_commit=1、innodb_buffer_pool_size=256M✅实测- 字符集
utf8mb4存 emoji ✅实测(张三😀正常) - MVCC:RR 下事务内两次读快照一致(888→888);RC 下两次读不一致(1888→2888) ✅实测
六、Django 连 Redis 完整步骤
Step 1 装包 → Step 2 配 settings → Step 3 manage.py check → Step 4 代码里用
python
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/0',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
'SOCKET_CONNECT_TIMEOUT': 5,
'SOCKET_TIMEOUT': 5,
'CONNECTION_POOL_KWARGS': {'max_connections': 50, 'retry_on_timeout': True},
},
}
}
python
from django.core.cache import cache
from django_redis import get_redis_connection
cache.set('k', {'a': 1}, timeout=60) # ✅ 自动 pickle + 加前缀
cache.get('k') # -> {'a': 1}
cache.add / touch / get_many / incr / get_or_set / delete # ✅ 全部实测通过
r = get_redis_connection('default') # 拿原生客户端
r.set('u:1', 'x', ex=60) # ⚠️ 原生用 ex=,不是 timeout=
r.hset('h:1', mapping={'a': 1})
pipe = r.pipeline(transaction=True) # MULTI/EXEC
pipe.set('a', 1); pipe.incr('b'); pipe.execute()
⚠️ 两个必知差异(实测):
- cache 层 key 被加了前缀 :逻辑
demo:key→ 实际':1:demo:key';值是 pickle 二进制 。用原生客户端读必须r.get(cache.make_key('demo:key')),直接r.get('demo:key')返回None - 参数名不同 :
cache.set(timeout=)✅ vsr.set(timeout=)❌TypeError,原生必须r.set(ex=)
version 也在 key 里:cache.set('vkey','v1',version=1) → :1:vkey;version=2 → :2:vkey(可用于灰度发布)
七、Redis 异常处理与重连(实测通过)
python
from redis.exceptions import ConnectionError as RedisConnErr, TimeoutError as RedisTimeout
def safe_get(key):
try:
r = get_redis_connection('default')
return r.get(key)
except (RedisConnErr, RedisTimeout) as e:
logger.error('redis down: %s', e) # 降级:回源 DB / 返回默认值
try:
r.ping() # 主动探活,下一次调用即重连
except Exception:
pass
return None
实测结论:
| 项 | 结果 |
|---|---|
Redis 宕机时 cache.get/set |
抛 redis.exceptions.ConnectionError(原样透传,不包装)✅ |
| 业务层捕获后 | 可降级回源 MySQL,主流程不崩 ✅ |
| Redis 恢复后 | 连接池自动重建连接,进程无需重启,写入立刻成功 ✅ |
| 重试循环 + 指数退避 | 有效 ✅ |
retry_on_timeout=True 只重试超时,连接断开不自动重试,靠业务层兜底。
静默降级(可选用):
python
'OPTIONS': {'IGNORE_EXCEPTIONS': True} # ✅ 实测:Redis 挂了 set/get/delete 全返回 None 不抛异常
八、Redis 1主2从 + Sentinel(全部实测通过)
yaml
写 ┌─────────┐
│ Master │ 7001
└────┬────┘
replicaof ┌─┴─┐ ┌─┴─┐
┌──────┘ └───┐ │
┌───▼───┐ ┌───▼───┐
│Slave 1│ │Slave 2│ 7002 / 7003
└───▲───┘ └───▲───┘
读 └─ replicaof ─┘ 读
Sentinel ×3 (27001/27002/27003) quorum=2,down-after-milliseconds=3000
启动:
bash
redis-server --port 7001 --daemonize yes --save '' \
--repl-diskless-sync yes --repl-diskless-sync-delay 0
redis-cli -p 7002 replicaof 127.0.0.1 7001
redis-cli -p 7003 replicaof 127.0.0.1 7001
⚠️ 实测坑 :不设 --repl-diskless-sync-delay 0 时,从库会卡在 state=wait_bgsave,日志刷 Delay next BGSAVE for diskless SYNC,同步迟迟不完成。设 0 后 1 秒内同步完成。
Sentinel 配置:
yaml
port 26379
sentinel monitor mymaster 127.0.0.1 7001 2
sentinel down-after-milliseconds mymaster 3000
sentinel failover-timeout mymaster 10000
sentinel parallel-syncs mymaster 1
实测结果:
| 项 | 实测 |
|---|---|
| 主从同步 | 1 秒内 master_link_status:up,offset 三方对齐(132/132/132)✅ |
| 写主读从 | 主写 3 条,2 个从库都能读到 ✅ |
| 从库写 | READONLY You can't write against a read only replica. ✅ |
| 手动 failover | replicaof no one 提主 + 另一从重指向,正常 ✅ |
| Sentinel 自动 failover | 杀主库 6 秒完成切换,新主 7003,数据完好 ✅ |
| failover 日志 | +sdown → +vote-for-leader → +odown master ... #quorum 3/2 → +switch-master mymaster 127.0.0.1 7001 127.0.0.1 7003 ✅ |
| Django 客户端 | 无需改配置,自动跟随新主继续读写 ✅ |
⚠️ Sentinel 配置残留坑 :sentinel 会把当前主写回配置文件,重启后仍认旧主。实验重来必须删掉 sentinel-*.conf 重新生成,否则 sentinel replicas 显示 master-link-status:err ✅实测踩过
⚠️ Windows 注意事项 :Windows 版 Redis 5.0.14 不支持 fork,主从全量同步会永远卡在 wait_bgsave。1主2从/Sentinel 必须在 Linux 上验证 ✅实测
九、django-redis 7.0 Sentinel 正确配置(⚠️ 与网上旧写法不同)
python
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': 'redis://mymaster/0', # ⚠️ hostname 就是 service_name,单个字符串
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.SentinelClient',
'CONNECTION_FACTORY': 'django_redis.pool.SentinelConnectionFactory', # ⚠️ 必须
'SENTINELS': [ # ⚠️ 新选项名,不是把地址塞进 LOCATION
('127.0.0.1', 27001),
('127.0.0.1', 27002),
('127.0.0.1', 27003),
],
'SENTINEL_KWARGS': {'socket_timeout': 1}, # 只剩 socket 相关参数
'SOCKET_CONNECT_TIMEOUT': 2,
'SOCKET_TIMEOUT': 2,
},
}
}
新旧对比:
| 旧写法(5.x) | 新写法(7.0) | |
|---|---|---|
| LOCATION | [('ip',26379), ...] |
'redis://mymaster/0' |
| service_name | SENTINEL_KWARGS.service_name |
LOCATION 的 hostname |
| sentinel 地址 | LOCATION 里 | OPTIONS['SENTINELS'] |
| CONNECTION_FACTORY | 不需要 | 必须指定 SentinelConnectionFactory |
旧写法实测报错:AttributeError: 'tuple' object has no attribute 'startswith' 漏 CONNECTION_FACTORY 实测报错:ImproperlyConfigured: ... CONNECTION_POOL_CLASS is not configured correctly
redis-py 8.x API(实测):
python
from redis.sentinel import Sentinel
s = Sentinel([('127.0.0.1', 27001), ...], socket_timeout=1)
s.discover_master('mymaster') # ('127.0.0.1', 7001)
s.discover_replicas('mymaster') # ⚠️ 不是 discover_slaves 之外的 discover_sentinels
s.sentinel_sentinels('mymaster') # ⚠️ 没有 discover_sentinels 方法
m = s.master_for('mymaster') # 拿到主库 Redis 客户端
r = s.replica_for('mymaster') # 拿到从库客户端
m.get_master_address() # ('127.0.0.1', 7001)
十、Redis 常用命令(全部实测)
| 类别 | 命令 | 实测 |
|---|---|---|
| 字符串 | SET key val EX 60 / GET / INCR / DECR |
✅ 计数器原子累加 |
| 哈希 | HSET mapping= / HGETALL / HINCRBY |
✅ |
| 列表 | LPUSH/RPUSH / LRANGE / LPOP / BLPOP key timeout |
✅ 阻塞队列 |
| 集合 | SADD / SISMEMBER / SMEMBERS |
✅ |
| 有序集合 | ZADD / ZINCRBY / ZREVRANGE withscores |
✅ 排行榜 |
| TTL | EXPIRE / PTTL / PERSIST / TTL |
✅ -1永久 -2不存在 |
| 遍历 | SCAN cursor MATCH count / scan_iter |
✅ |
| 类型 | TYPE / EXISTS / DEL |
✅ |
| 事务 | MULTI/EXEC/DISCARD + WATCH |
✅ WatchError 乐观锁 |
| 批处理 | PIPELINE |
✅ 减少 RTT |
| 发布订阅 | PUBLISH/SUBSCRIBE |
✅ |
| 分布式锁 | SET key val NX EX 30 |
✅ 第二个抢锁返回 None,立即失败不阻塞 |
| Lua 解锁 | if get(KEYS[1])==ARGV[1] then del end |
✅ 非持有者释放返回 0 |
KEYS * vs SCAN:实测 5 万 key 时 KEYS 52.8ms、SCAN 65.7ms,耗时差别不大 。禁用 KEYS 的真正原因是它阻塞 Redis 单线程(实测期间其他客户端 ping 延迟升高),而非单纯慢。
十一、缓存三大坑(实测方案有效)
- 穿透 (查不存在的 key → 全打到 DB) ✅ 实测方案:查库为空时缓存空值
r.set('miss:999', '', ex=60)更优:布隆过滤器 - 击穿 (热点 key 过期瞬间) ✅ 实测方案:
SET NX EX互斥锁抢锁 → 抢到才重建 → finally 释放锁 进阶:逻辑过期(值里带过期时间,永不过期) - 雪崩 (大量 key 同时过期 / Redis 宕机) ✅ 实测方案:TTL 加随机抖动(实测 TTL 329~331 分散,不再同时过期) 兜底:多级缓存 + 熔断限流 +
IGNORE_EXCEPTIONS
Cache-Aside 一致性:先更新 DB,再删缓存(不是先删缓存)
python
update_db()
cache.delete(key) # ✅ 实测:删除后 get 返回 None,下次回源 DB
更强一致:延迟双删 / Canal 订阅 binlog 失效缓存
十二、MongoDB(PyMongo 4 实测 49/49)
装包 :python3 -m pip install pymongo(4.19.0) ⚠️ Django ORM 接 MongoDB 的 djongo 不可用 ------装它会强迫 Django 降级到 3.1.12(老到跑不动)。生产要么纯 pymongo 手写 DAL,要么用 mongoengine(0.29.3 可正常搭配),要么保持 MySQL 做主库、Mongo 只当文档型从存储。
连接
python
from pymongo import MongoClient
client = MongoClient(
'mongodb://djtest:djtest123@172.30.0.2:27017/djtest?authSource=admin',
serverSelectionTimeoutMS=3000, # 选不出节点多久后抛异常(默认30s,测试调小)
)
db = client['djtest']
users = db['users'] # 集合(=关系库的表),不存在时首次写入自动创建
⚠️ authSource 必须写认证库 :账号建在 admin 库就必须 ?authSource=admin,否则 AuthenticationFailed。✅实测
- 无模式:同一集合的文档字段可以完全不同,不需要迁移
MongoClient自带拓扑感知 + 自动重连,断线恢复后无需重建客户端 ✅实测
增删改查
python
# ── 增 ──
users.insert_one({'name': 'tom', 'tags': ['a','b'], 'profile': {'score': 90}})
users.insert_many([{...}, {...}]) # 返回 inserted_ids
r = users.insert_one(doc); r.inserted_id # ObjectId 回填
# ── 查 ──
users.find_one({'name': 'tom'})
users.find({'age': {'$gte': 15}}) # 游标,list() 才取回
users.count_documents({}) # 精确计数
users.estimated_document_count() # 元数据估算(快)
users.distinct('city') # 去重值列表
users.find().sort('age', DESCENDING).skip(1).limit(3) # 分页
users.find({'name': 'tom'}, {'_id': 0, 'name': 1}) # 投影
# ── 改 ──
users.update_one({'name': 'tom'}, {'$set': {'age': 21}}) # modified_count
users.update_many({'city': 'bj'}, {'$inc': {'age': 1}})
users.replace_one({'name': 'u2'}, {'name': 'u2', 'age': 99}) # 整体替换
users.find_one_and_update({'name': 'u3'}, {'$set': {'age': 77}}) # 默认返回更新前
users.bulk_write([UpdateOne(...), UpdateOne(...)]) # 一次 RTT 批量写
# ── 删 ──
users.delete_one({'name': 'arr'}) # deleted_count
users.delete_many({'name': {'$in': ['u6']}})
查询/更新操作符(全部实测)
| 类别 | 操作符 | 说明 |
|---|---|---|
| 比较 | $gt $lt $gte $lte $ne |
{'age': {'$gt': 5, '$lte': 20}} 可组合 |
| 集合 | $in $nin |
{'name': {'$in': ['u1','u2']}} |
| 存在 | $exists |
{'tags': {'$exists': True}} |
| 正则 | $regex |
{'name': {'$regex': '^u[12]$'}} |
| 逻辑 | $or $and $not $nor |
{'$or': [{...}, {...}]} |
| 边更新 | $set $inc $unset |
{'$set': {'level': 3}, '$inc': {'age': 1}, '$unset': {'tags': ''}} |
| 数组 | $push $addToSet $pop $pull |
$addToSet 去重追加;$pop: {'tags': 1} 弹尾部 |
嵌套与数组查询(MySQL 做不到、面试常问)✅实测:
- 嵌套字段:
{'profile.score': 90}(点号路径直达深层) - 数组包含:
{'tags': 'a'}------ 数组里有 a 就命中 - 数组范围:
{'scores': {'$gt': 75}}------ 数组里有任一元素 >75 就命中
ObjectId :_id 默认是 ObjectId 类型;非法字符串 ObjectId('badid') 抛 InvalidId;从 URL 拿到字符串 id 必须 ObjectId(s) 转换后再查 ✅实测
索引与聚合
python
users.create_index([('name', ASCENDING)], unique=True, name='uniq_name')
users.create_index([('city', ASCENDING), ('age', DESCENDING)]) # 复合索引(可混合升降序)
users.list_indexes()
users.find({'name': 'n5'}).explain() # winningPlan + executionStats.totalDocsExamined
- explain 实测 :走索引
totalDocsExamined=1;无索引查询totalDocsExamined=1000(=全集合扫描) - 唯一索引冲突 →
DuplicateKeyError✅实测(同 MySQL 的 1062 位置)
python
# 聚合管道(相当于 SQL 的 GROUP BY / JOIN / CASE WHEN)
users.aggregate([
{'$match': {'city': 'bj'}}, # WHERE
{'$group': {'_id': '$city', 'n': {'$sum': 1}, # GROUP BY + 聚合函数
'avg_age': {'$avg': '$age'}, 'max_age': {'$max': '$age'}}},
{'$sort': {'n': -1}}, {'$limit': 5}, # ORDER BY + LIMIT
])
# $project + $switch = SELECT CASE WHEN 分桶
# $lookup = LEFT JOIN:{'$lookup': {'from': 'users', 'localField': 'uid',
# 'foreignField': 'uid', 'as': 'user'}}
# 之后常接 {'$unwind': '$user'} 把数组拆开再 $group
⚠️ $lookup 的 localField(本表字段)与 foreignField(对方字段)字段名必须是实际存在的键,拼错不报错只是关联为空(类似 MySQL JOIN 忘 ON)✅实测
事务、原子性、异常
| 项 | 实测结果 |
|---|---|
start_transaction(单节点) |
❌ OperationFailure: Transaction numbers are only allowed on a replica set member or mongos------单机版没有事务,需副本集(≥3 节点或单节点副本集) |
| 单文档原子性 | ✅ $inc/更新符在单文档上天然原子,计数器场景不需要事务 |
| 连不上 | ServerSelectionTimeoutError(由 serverSelectionTimeoutMS 控制,默认 30s 才有响应) |
| 唯一键冲突 | DuplicateKeyError |
_id 非法 |
InvalidId(bson.errors) |
| 认证/权限错误 | OperationFailure(code 13 Unauthorized 等) |
写关注 WriteConcern(对标 MySQL 的 sync_binlog/半同步)✅实测:
python
# ⚠️ pymongo 4:wtimeout 改名为 wtimeoutms!3.x 写法直接 ConfigurationError: Unknown option: wtimeout
client = MongoClient(uri, w='majority', wtimeoutms=5000) # w:1 主库确认 / majority 多数派确认
# ⚠️ pymongo 4 移除了 insert_one(write_concern=...):TypeError: unexpected keyword argument
from pymongo.write_concern import WriteConcern
coll = users.with_options(write_concern=WriteConcern(w=1)) # 正确做法:派生 collection
coll.insert_one({...})
十三、RabbitMQ 4.x + pika(实测 24/24)
装包 :python3 -m pip install pika(1.4.4),broker 用 rabbitmq:4-management 容器(4.3.6)
python
# ⚠️ pika 1.x:BlockingConnection 不接受 URL 字符串!
# BlockingConnection('amqp://guest:guest@127.0.0.1:5672/%2F')
# -> AttributeError: 'str' object has no attribute 'connection_attempts'
import pika
params = pika.URLParameters('amqp://guest:guest@127.0.0.1:5672/%2F') # %2F = vhost "/"
params.connection_attempts = 3 # 初始化失败重试次数
params.retry_delay = 1 # 重试间隔秒
params.heartbeat = 60 # 心跳(默认60,0禁用)
params.blocked_connection_timeout = 30
conn = pika.BlockingConnection(params)
ch = conn.channel()
四种 Exchange(实测)
| 类型 | 路由规则 | 实测 |
|---|---|---|
direct |
routing_key 精确匹配 | key.a 的队列才收到 ✅ |
fanout |
广播给所有绑定队列,忽略 routing_key | 两个队列都收到 ✅ |
topic |
通配符:* 一个词、# 零或多词 |
绑定 order.*.* 收到 order.created.vip;不匹配 log.info.app(三段但首段不对)、order.created(只有两段)✅ |
headers |
按消息头匹配,x-match: all/any |
⚠️ headers 必须放 pika.BasicProperties(headers={...}),basic_publish 没有 headers 参数 |
python
ch.exchange_declare(exchange='ex1', exchange_type='direct', durable=True, auto_delete=False)
ch.queue_declare(queue='q1', durable=True, auto_delete=False,
arguments={'x-message-ttl': 5000, 'x-max-length': 100})
ch.queue_bind(queue='q1', exchange='ex1', routing_key='key.a')
ch.basic_publish(exchange='ex1', routing_key='key.a', body='msg',
properties=pika.BasicProperties(delivery_mode=2)) # 2=消息持久化
消费与确认(可靠性核心)
python
# 轮询模式
m = ch.basic_get(queue='q1', auto_ack=True) # (method, properties, body)
# ⚠️ 队列空时返回 (None, None, None) ------ 它是 truthy!不能写 if not m,要写 if m[0] is None ✅实测
# 订阅模式
ch.basic_qos(prefetch_count=2) # QoS:最多同时未确认 2 条
ch.basic_consume(queue='q1',
on_message_callback=lambda c, method, props, body: ..., # 4个参数!
auto_ack=False)
ch.start_consuming()
ch.basic_ack(method.delivery_tag) # 手动确认
ch.basic_nack(method.delivery_tag, requeue=True) # 拒收但重回队列(稍后重投)
ch.basic_reject(method.delivery_tag, requeue=False) # 拒收且丢弃 → 进死信(若有DLX)
手动 ack 实测 :取后未确认 message_count 不变(unacked 状态),ack 后才消失;进程崩溃未 ack 的消息会自动重回队列(这就是 RabbitMQ 不丢消息的核心)。
TTL 与死信队列(DLX)
python
# 队列级 TTL:整个队列所有消息
ch.queue_declare(queue='q_ttl', arguments={'x-message-ttl': 1000}) # 毫秒
# 消息级 TTL:单条消息
ch.basic_publish(..., properties=pika.BasicProperties(expiration='1000'))
# 死信队列:主流 q_main 上的消息 被 reject(requeue=False) / TTL 过期 / 队列满 时自动转到 DLX
ch.queue_declare(queue='q_main', durable=True,
arguments={'x-dead-letter-exchange': 'ex_dlx',
'x-dead-letter-routing-key': 'dead'})
ch.queue_bind(queue='dlq', exchange='ex_dlx', routing_key='dead')
✅实测:reject(requeue=False) 后消息进 DLQ,且消息头带上 x-death 死信记录。TTL 实测 1.6s 后消息消失/进 DLQ。
发布确认(publisher confirm)
python
ch.confirm_delivery() # 开启后 basic_publish 同步等待 broker ack,投递失败会抛异常
配合队列 durable + 消息 delivery_mode=2 + 手动 ack,构成 RabbitMQ「不丢消息」三件套。
RPC 模式与异常(实测)
python
# RPC:客户端建 exclusive 临时回调队列(queue='' 让 broker 生成名字,断连即删)
cb_q = ch.queue_declare(queue='', exclusive=True).method.queue
ch.basic_publish(..., properties=pika.BasicProperties(reply_to=cb_q, correlation_id=str(uuid4())))
| 异常 | 触发场景 |
|---|---|
pika.exceptions.ProbableAuthenticationError |
账号密码错 ✅实测 |
AMQPConnectionError |
broker 连不上(端口不通)✅实测 |
ChannelClosed(code 406 PRECONDITION_FAILED) |
同名队列参数不一致重新声明(如 durable 或 x-message-ttl 不同)✅实测,忠实复现:声明参数必须前后完全一致 |
⚠️ RabbitMQ 4.x 重要变化(实测)
- 禁止「非持久化(non-durable) + 非排他(non-exclusive)」队列 :直接返回
541 INTERNAL_ERROR - Feature 'transient_nonexcl_queues' is deprecated。以前 auto_delete=True 的临时队列玩法在 4.x 受限(要么 durable、要么 exclusive) - vhost 默认
/在 URL 里要写成%2F - 管理用:
rabbitmqctl list_queues name messages consumers、list_exchanges✅实测 - 测试结束清理:RabbitMQ 4 下临时队列不会自己消失,要
rabbitmqctl delete_queue <name>手动删 ✅实测
十四、Celery + Django(双 broker 实测:Redis 15/15、RabbitMQ 7/7)
装包 :python3 -m pip install celery(5.6.3,依赖 kombu 5.6.2) 定位:Celery = 分布式任务队列(生产者投任务 → broker 排队 → worker 执行 → result backend 存结果)
scss
Django 视图 --delay()--> [broker: Redis 或 RabbitMQ] --> Celery Worker --> [result backend: Redis]
^ 定时: Celery Beat
配置(celery_app.py,与 Django 集成)
python
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tproj.settings') # 必须在 import celery 前
from celery import Celery
from celery.schedules import crontab
app = Celery('proj')
app.conf.update(
broker_url='redis://127.0.0.1:6379/1', # 或 'amqp://guest:guest@127.0.0.1:5672//'
result_backend='redis://127.0.0.1:6379/2', # 结果存储,可与 broker 分开
task_serializer='json', result_serializer='json', accept_content=['json'],
timezone='Asia/Shanghai', enable_utc=True,
task_default_queue='djtest_default',
task_routes={'tasks.add': {'queue': 'djtest_math'}, # 任务路由到不同队列
'tasks.slow': {'queue': 'djtest_slow'}},
task_acks_late=True, # 执行完才 ack(worker 崩溃任务重回)
worker_prefetch_multiplier=1, # 一次只预取1条,长任务公平分发
task_reject_on_worker_lost=True,
broker_connection_retry_on_startup=True,
beat_schedule={ # ⚠️ 定时配置必须写在这里!beat 是独立进程读这个文件
'every-3-seconds': {'task': 'tasks.echo', 'schedule': 3.0, 'args': ['beat-fired']},
'every-night-2am': {'task': 'tasks.echo', 'schedule': crontab(hour=2, minute=0)},
},
)
app.autodiscover_tasks(['app'], force=True) # 自动发现 app/tasks.py
任务定义 (app/tasks.py):
python
from celery import shared_task
@shared_task(name='tasks.add')
def add(x, y): return x + y
@shared_task(name='tasks.touch_db')
def touch_db():
from django.db import connection, close_old_connections
close_old_connections() # ⚠️ worker 是 fork 出来的,Django 连接要重建
... # 之后正常用 ORM
@shared_task(name='tasks.flaky', bind=True, max_retries=3)
def flaky(self, fail_times=2):
if self.request.retries < fail_times:
raise self.retry(countdown=1) # 自动重试(retries 次数可用)
return 'ok'
启动与调用(实测命令)
bash
celery -A celery_app worker --loglevel=info --pool=solo --concurrency=1 \
-Q djtest_default,djtest_math,djtest_slow -n testw@%h
# ⚠️ -Q 必须显式列出所有路由目标队列!否则 task_routes 路由出去的队列没人消费(任务永远 PENDING)✅实测
celery -A celery_app beat --loglevel=info --schedule=/tmp/celerybeat-schedule
python
tasks.add.delay(3, 4) # 投递,返回 AsyncResult
tasks.add.apply_async(args=[7, 8], queue='djtest_math') # 指定队列
tasks.add.apply_async(args=[1, 1], countdown=2) # eta/countdown 延时执行
app.send_task('tasks.echo', args=['hi'], queue='djtest_default') # 按名字调任务
r = tasks.add.delay(10, 20)
r.get(timeout=15) # 阻塞取结果(实测 1+1=2 正常返回);r.state / r.successful()
tasks.flaky.delay(fail_times=2).get(timeout=30) # 前2次失败后第3次成功 ✅实测
监控:
python
app.control.inspect(timeout=5).stats() # worker 状态、并发数、pool 实现
app.control.inspect().active() / .registered()
app.control.ping(timeout=5) # ⚠️ celery 5.x 返回 list[dict](不是 dict):[{'celery@host': {'ok': 'pong'}}]
beat 实测 :schedule=3.0s,10 秒内定时投递 3 次 ✅。⚠️ --schedule 文件会持久化 last_run_at,不删掉它重启 beat 不会立刻触发 (测试要 rm celerybeat-schedule)✅踩过。
任务失败与状态(实测)
| 场景 | 实测 |
|---|---|
| 任务抛异常 | r.get() 重新抛出原异常;r.state='FAILURE',traceback 存 result backend ✅ |
self.retry(countdown=1) |
worker 重新入队,self.request.retries 递增 ✅ |
| 队列没有 worker 监听 | 任务滞留 broker,get() TimeoutError ✅(排查第一反应看 -Q) |
⚠️⚠️ Celery 5.6 + RabbitMQ 4.x 兼容大坑(本次实测最大发现)
现象 :amqp:// broker 下 worker 启动即崩,无限重启,日志刷:
makefile
amqp.exceptions.InternalError: Queue.declare: (541) INTERNAL_ERROR - Feature `transient_nonexcl_queues` is deprecated.
billiard.exceptions.RestartFreqExceeded: 5 in 1s
根因(逐层排查确认):
- Celery 默认
event_queue_durable=False------ 事件队列是「非持久化+非排他」,4.x 拒收 - 更深一层:kombu 5.6 的 pidbox(远程控制 inspect 的回信机制)
Mailbox.get_reply_queue()硬编码durable=False, auto_delete=True,response 队列*.reply.celery.pidbox同样被 541 拒绝
修复(实测有效,worker 恢复正常,7/7 通过):
python
# celery_app.py 顶部
import kombu.pidbox
_orig = kombu.pidbox.Mailbox.get_reply_queue
def _durable_get_reply_queue(self):
q = _orig(self)
q.durable = True # 关键:pidbox 回复队列强制持久化
q.auto_delete = False
return q
kombu.pidbox.Mailbox.get_reply_queue = _durable_get_reply_queue
app.conf.event_queue_durable = True # 事件队列也持久化(Celery 默认 False,4.x 拒收)
简化方案(二选一):
- 不用远程控制:
worker_enable_remote_control=False(但inspect ping就没了) - 直接换 Redis broker(blogdemo 生产就是这个选择,完全无此问题)
Redis broker vs RabbitMQ broker(实测对比)
| Redis broker | RabbitMQ broker | |
|---|---|---|
| 队列本质 | List(lpush/brpop)+ zset 做延迟 | 真 AMQP 队列 |
| 持久化/可靠性 | 弱(可配 AOF 但整体不如 AMQP) | durable + confirm + ack 三件套原生 |
| 死信/TTL/优先级 | 无原生(要自己实现) | 原生支持 |
| 依赖 | 已有 Redis 就能用 | 需额外常驻服务 |
| 延迟任务 | 靠 beat/zset 搬运 | 原生 TTL+DLX,但仍有调度轮询 |
| 实测选择 | ✅ t16 全通过 | ⚠️ 需给 Celery 打补丁才跑通(t17) |
十五、速记口诀
- Django 6 配 MySQL 8.0 → 必炸,用 5.2 LTS
- PyMySQL 上 MySQL 8 → 必须装 cryptography
- Django 会话隔离级别 = READ-COMMITTED(不是 MySQL 全局的 RR)
- cache 层
timeout=,原生层ex=;cache 层 key 带:版本:前缀且值是 pickle - Redis 断了不自动重试,但连接池在服务恢复后会自动重建连接
- django-redis 7 Sentinel = LOCATION(单URL) + SENTINELS + CONNECTION_FACTORY
- 原子用 atomic + F();唯一键 IntegrityError;死锁 1213 / 锁等待 1205 要出事务再重试
- 慢查询开关要 SYSTEM_VARIABLES_ADMIN 权限
- varchar 列和数字比较会全表扫
- 1主2从读写分离,牺牲一致性;Sentinel 6 秒切换;Windows Redis 做不了主从
- Mongo 单机无事务(要副本集);单文档 $inc 天然原子;pymongo4 是 wtimeoutms 不是 wtimeout
- Mongo 嵌套用点号、数组直接等值就命中;$lookup 拼错字段不报错只空关联
- pika1.x 必须 URLParameters;basic_get 空返回 (None,None,None) 是 truthy;同名队列参数必须一致
- RabbitMQ4 禁「非持久化+非排他」队列;Celery5.6 默认就声明这种 → worker 起不来的锅在这
- Celery worker 必须 -Q 显式列出路由队列;beat_schedule 写在 celery_app.py;beat 的 schedule 文件删了才会立即触发
附录:实测覆盖情况
| 模块 | 结果 |
|---|---|
| Redis 缓存层 CRUD | 9/9 ✅ |
| Redis 常用命令 | 13/13 ✅ |
| 缓存三大坑 + Cache-Aside | 4/4 ✅ |
| Redis 异常处理与重连 | 9/9 ✅ |
| 1主2从 + Sentinel failover | 11/11 ✅ |
| django-redis Sentinel 配置 | 4/4 ✅ |
| MySQL CRUD | 20/20 ✅ |
| MySQL 事务与异常 | 9/9 ✅ |
| 索引 / EXPLAIN / 覆盖索引 | 7/7 ✅ |
| 分布式锁 / 连接池 / 慢查询权限 | 7/7 ✅ |
| MongoDB 连接/CRUD/操作符/嵌套数组 | 22/22 ✅ |
| MongoDB 索引 / explain / 聚合管道 | 8/8 ✅ |
| MongoDB 事务限制 / 异常 / 写关注 | 10/10 ✅ |
| MongoDB 连接管理与清理 | 9/9 ✅ |
| RabbitMQ 四种 Exchange + 队列参数 | 10/10 ✅ |
| RabbitMQ ack/nack/QoS | 4/4 ✅ |
| RabbitMQ TTL / 死信 / confirm | 4/4 ✅ |
| RabbitMQ RPC / 异常 / 状态查询 / 清理 | 6/6 ✅ |
| Celery + Redis broker(含 beat) | 15/15 ✅ |
| Celery + RabbitMQ broker(含 4.x 兼容补丁) | 7/7 ✅ |
测试代码位于服务器 /tmp/djtest/proj2/(t1~t17 系列脚本,可直接 python3 tX_xxx.py 复跑);本机备份在「实测代码」目录。 runner 环境:/tmp/djtest/venv(Django 5.2.18 / pymongo 4.19 / pika 1.4.4 / celery 5.6.3);MongoDB、RabbitMQ 均为 Docker 容器。