Django 中间件全家桶背诵笔记

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 8 caching_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()

⚠️ 两个必知差异(实测):

  1. cache 层 key 被加了前缀 :逻辑 demo:key → 实际 ':1:demo:key';值是 pickle 二进制 。用原生客户端读必须 r.get(cache.make_key('demo:key')),直接 r.get('demo:key') 返回 None
  2. 参数名不同 :cache.set(timeout=) ✅ vs r.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 延迟升高),而非单纯慢。


十一、缓存三大坑(实测方案有效)

  1. 穿透 (查不存在的 key → 全打到 DB) ✅ 实测方案:查库为空时缓存空值 r.set('miss:999', '', ex=60) 更优:布隆过滤器
  2. 击穿 (热点 key 过期瞬间) ✅ 实测方案:SET NX EX 互斥锁抢锁 → 抢到才重建 → finally 释放锁 进阶:逻辑过期(值里带过期时间,永不过期)
  3. 雪崩 (大量 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 重要变化(实测)

  1. 禁止「非持久化(non-durable) + 非排他(non-exclusive)」队列 :直接返回 541 INTERNAL_ERROR - Feature 'transient_nonexcl_queues' is deprecated。以前 auto_delete=True 的临时队列玩法在 4.x 受限(要么 durable、要么 exclusive)
  2. vhost 默认 / 在 URL 里要写成 %2F
  3. 管理用:rabbitmqctl list_queues name messages consumers、list_exchanges ✅实测
  4. 测试结束清理: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

根因(逐层排查确认):

  1. Celery 默认 event_queue_durable=False ------ 事件队列是「非持久化+非排他」,4.x 拒收
  2. 更深一层: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 容器。

相关推荐
vipxieliang8 小时前
Go 泛型结构体与约束进阶:从自定义约束到标准库泛型 API 实战
后端·go
isfox8 小时前
Python 文件操作:读写文件这件事,记住 with open 就够了
后端
武子康8 小时前
虚拟线程不是无限并发:Java Agent 后端真正该限制的是这些资源
人工智能·后端·agent
未秃头的程序猿8 小时前
Spring Cloud Gateway 做统一加解密,我在请求体上栽了三天
java·后端·架构
Rain的Java大神之路8 小时前
别再上来就堆 Redis!点赞系统从打挂数据库到 18w 写峰值,我把这套方案讲透了
java·后端·面试
SimonKing8 小时前
开源文档解析天花板 MinerU 4.0:PDF / 图片 / Office 等任意文档都支持
java·后端·程序员
咖啡八杯8 小时前
Spring MessageSource 国际化消息解析流程
java·后端·spring·架构·框架·若依
念何架构之路9 小时前
go-grpc逻辑架构图
开发语言·后端·golang
博.闻广见9 小时前
第10篇:Agent系统的性能优化 —— 缓存、压缩与动态模型路由
java·人工智能·后端·spring·agent