Python数据库操作与异常处理:一个让我半夜爬起来修Bug的故事

简介: 本文以凌晨报警的注册接口故障为引,深入剖析Python数据库异常处理:区分OperationalError(可重试)、IntegrityError(需业务处理)和ProgrammingError(须修复代码);倡导使用with管理资源、自定义业务异常、重试机制与分层捕获,全面提升系统健壮性。(239字)

免费编程软件「python+pycharm」

链接:https://pan.quark.cn/s/48a86be2fdc0

凌晨两点的报警电话

去年冬天的一个凌晨,我被手机吵醒了。监控报警:用户注册接口成功率跌到了50%。

我迷迷糊糊打开电脑,翻到错误日志,发现全是同一个异常:

IntegrityError: (1062, "Duplicate entry '13800138000' for key 'phone'")

事情是这样的:我们在做一个用户注册功能,用户填完手机号提交,系统往数据库里插一条记录。正常情况下,手机号是唯一的,不会重复。但那天因为网络抖动,用户点了两次提交按钮,后端接口收到了两个一模一样的注册请求。

第一个请求成功了,插入了用户数据。第二个请求来了,发现手机号已经存在,INSERT语句直接报错。然后我们的代码做了什么?**它把异常打印到日志,然后给前端返回了一个"服务器错误"**。

用户第二次提交看到的是报错页面,但他不知道的是——第一次提交其实已经注册成功了。他以为没成功,可能又试了第三次、第四次……

这个Bug暴露了两个问题:一是接口没做幂等处理,二是数据库操作的异常处理写得太粗糙了

今天我就用这个场景出发,把Python操作数据库时应该怎么处理异常,掰开揉碎了讲清楚。

代理 IP 使用小技巧 让你的数据抓取效率翻倍 (33).png


数据库操作到底有哪些异常?

在Python里操作数据库,不管是SQLite、MySQL还是PostgreSQL,常见的异常可以归成几类。

连接层面的异常

import sqlite3

try:
   conn = sqlite3.connect('production.db')
except sqlite3.OperationalError as e:
   print(f"数据库连接失败: {e}")
   # 可能的原因:文件打不开、权限不足、网络不通

OperationalError是操作层面的错误,跟SQL语句本身没关系,是执行环境出了问题。比如:

  • 数据库服务器挂了
  • 连接超时
  • 文件被锁
  • 磁盘满了

这类错误通常需要重试,或者走降级方案。

SQL层面的异常(编程错误)

# 表不存在
cursor.execute("SELECT * FROM users")  
# sqlite3.OperationalError: no such table: users

# 列不存在
cursor.execute("SELECT phone FROM user")  
# sqlite3.OperationalError: no such column: phone

# SQL语法错误
cursor.execute("CREATE TABLE users id INTEGER")  
# sqlite3.OperationalError: near "id": syntax error

ProgrammingError在SQLite里是OperationalError的子集,在MySQL的pymysql里有独立的ProgrammingError。它表示你的代码写错了:

  • 表名或列名拼写错误
  • SQL语法有问题
  • 参数绑定的数量不匹配

这类错误不应该用重试来解决,正确的做法是修代码。

数据完整性层面的异常

# 违反了唯一约束
cursor.execute("INSERT INTO users (phone) VALUES ('13800138000')")
# sqlite3.IntegrityError: UNIQUE constraint failed: users.phone

IntegrityError是数据完整性错误。常见于:

  • 主键冲突(重复插入同一主键)
  • 唯一键冲突(重复的手机号、邮箱)
  • 外键约束失败(引用了不存在的父表记录)
  • NOT NULL约束失败

这类异常往往需要业务逻辑来处理——不是简单报错就完事了,要判断该怎么做。


业务场景里的正确姿势

回到注册接口的案例。当捕获到IntegrityError时,正确的做法不是直接返回错误,而是区分情况

import sqlite3

def register_user(phone, name):
   try:
       conn.execute(
           "INSERT INTO users (phone, name) VALUES (?, ?)",
           (phone, name)
       )
       conn.commit()
       return {"status": "success", "msg": "注册成功"}
   except sqlite3.IntegrityError as e:
       if "UNIQUE constraint failed: users.phone" in str(e):
           # 手机号已注册,应该返回"用户已存在",而不是"服务器错误"
           return {"status": "exists", "msg": "该手机号已注册"}
       else:
           # 其他完整性错误,记录详细日志
           logger.error(f"IntegrityError: {e}")
           raise
   except sqlite3.OperationalError as e:
       # 连接问题,可以重试
       logger.warning(f"数据库操作失败,准备重试: {e}")
       time.sleep(1)
       return register_user(phone, name)  # 递归重试(要有上限)

关键点是:不要用一个笼统的except Exception把所有异常都吞掉,那样你既不知道出了什么问题,也没法针对性地处理。


上下文管理器:让你的数据库代码自动"善后"

很多人的数据库代码长这样:

def get_user(user_id):
   conn = sqlite3.connect('app.db')
   cursor = conn.cursor()
   cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
   result = cursor.fetchone()
   cursor.close()
   conn.close()
   return result

万一execute报错了怎么办?cursor.close()conn.close()都不会被执行,连接就泄漏了。

with(上下文管理器)来解决:

def get_user(user_id):
   with sqlite3.connect('app.db') as conn:
       with conn.cursor() as cursor:
           cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
           return cursor.fetchone()
   # 退出with块时,cursor和conn会自动关闭,即使发生了异常

with块保证资源被正确释放。如果你用的是ORM(如SQLAlchemy),它也有类似的上下文管理机制。


自定义异常:让错误信息带上业务上下文

当异常发生时,原始的数据库错误信息(比如"Table doesn't exist")对用户来说是毫无意义的。更好的做法是把异常包装一下。

class UserRegistrationError(Exception):
   """用户注册相关的所有异常的基类"""
   pass

class UserAlreadyExistsError(UserRegistrationError):
   pass

class DatabaseConnectionError(UserRegistrationError):
   pass

def register_user(phone, name):
   try:
       # 数据库操作...
   except sqlite3.IntegrityError as e:
       if "UNIQUE constraint" in str(e):
           raise UserAlreadyExistsError(f"手机号{phone}已注册") from e
   except sqlite3.OperationalError as e:
       raise DatabaseConnectionError("数据库暂时不可用") from e

这样在业务层捕获异常时,可以直接按类型处理:

try:
   result = register_user(phone, name)
except UserAlreadyExistsError:
   # 前端显示"用户已存在"
except DatabaseConnectionError:
   # 前端显示"系统繁忙,请稍后重试"

raise ... from e的写法保留了原始异常信息,方便调试。


一个完整的实战模板

把上面所有内容整合成一个可复用的模板:

import sqlite3
import time
import logging

logger = logging.getLogger(__name__)

class DatabaseError(Exception):
   pass

class DuplicateRecordError(DatabaseError):
   pass

def with_retry(max_retries=3, delay=1):
   """重试装饰器,用于处理临时性数据库错误"""
   def decorator(func):
       def wrapper(*args, **kwargs):
           for attempt in range(max_retries):
               try:
                   return func(*args, **kwargs)
               except sqlite3.OperationalError as e:
                   if attempt == max_retries - 1:
                       raise DatabaseError(f"操作失败,已重试{max_retries}次") from e
                   logger.warning(f"第{attempt+1}次重试,错误: {e}")
                   time.sleep(delay * (attempt + 1))  # 指数退避
       return wrapper
   return decorator

@with_retry(max_retries=3)
def create_user(phone, name):
   try:
       with sqlite3.connect('app.db') as conn:
           conn.execute(
               "INSERT INTO users (phone, name) VALUES (?, ?)",
               (phone, name)
           )
           conn.commit()
           return {"id": conn.lastrowid, "phone": phone}
   except sqlite3.IntegrityError as e:
       if "UNIQUE constraint" in str(e):
           raise DuplicateRecordError(f"手机号{phone}已存在") from e
       raise DatabaseError(f"数据完整性错误: {e}") from e


总结:记住三条原则

  1. 区分异常类型OperationalError重试,IntegrityError走业务逻辑,ProgrammingError改代码。
  2. 用上下文管理器:数据库连接和游标用with管理,避免资源泄漏。
  3. 把异常包装成业务异常:让上层代码按业务逻辑处理,而不是处理数据库底层错误。

那次凌晨的报警之后,我把所有数据库操作都按照这个模板重构了。后来再遇到网络抖动、重复提交这类问题,代码都能正确处理——要么重试成功,要么返回明确的业务提示,再也没有半夜的报警电话了。

目录
相关文章
|
5天前
|
人工智能 定位技术 SEO
我学 GEO 第 15 天:终于知道AI GEO该如何做?
我是暴走的莉莉酱,边旅行边研究AI GEO的数字游民。专注普通人如何提升“AI可见度”——让AI在回答用户问题时准确识别、理解并推荐你。不讲玄学,只做可测、可调、可持续的GEO实践。
409 125
|
7天前
|
机器学习/深度学习 人工智能 调度
🐴 HappyHorse 1.1 现已上线阿里云百炼!快来查收模型使用指南,现在调用享 6 折~
HappyHorse 1.1 是新一代视频生成大模型,全面升级动态表现力、角色一致性、指令遵循、视觉质感与音画协同能力。支持I2V/T2V/R2V三类生成,适配短剧、电商广告、品牌营销等场景,提供高质、流畅、可控的AI视频生产力。
696 5
🐴 HappyHorse 1.1 现已上线阿里云百炼!快来查收模型使用指南,现在调用享 6 折~
|
5天前
|
缓存 人工智能 运维
阿里云618百炼大模型Qwen3.7-Max功能、免费试用、订阅计费、配置接入详解
Qwen3.7-MAX是阿里云百炼平台推出的通义千问3.7系列旗舰大语言模型,专为智能体时代复杂任务打造,依托阿里云全域算力与自研技术,在逻辑推理、长文本处理、代码工程、长周期自主执行等领域达到行业顶尖水平。2026年618期间,该模型推出多重免费试用权益、按量计费5折、订阅套餐优惠等专属福利,覆盖个人开发者、团队与企业全场景需求,以下从核心功能、免费试用、订阅计费、配置接入四方面展开详细解析。
406 123
|
3天前
|
人工智能 自然语言处理 API
阿里云Token Plan团队版解析:功能、三档套餐与省钱订阅指南
阿里云百炼平台推出的Token Plan团队版,是面向企业与团队的AI大模型订阅服务,以Credits为统一计量单位,整合文本与图像生成模型,提供团队管理、数据安全、多工具兼容等核心能力,解决团队零散订阅AI服务的管理混乱、成本失控、数据安全等痛点。本文将从核心定位、套餐详情、计费规则、团队管理、工具兼容、便宜订阅技巧等方面,全面解析Token Plan团队版,帮助企业与团队高效、低成本地使用AI服务。
302 108
|
4天前
|
存储 人工智能 数据可视化
别再手动复制 Skill 了:多 Agent 时代的 Skill 管理方案
多 Agent 场景下 Skill 的统一管理与同步。
245 126
|
18天前
|
缓存 测试技术 API
Qwen 3.7 Plus 与 Max 实测:性价比与多模态能力差异解析(2026)
2026 年 6 月 1 日,阿里悄无声息地发布了 Qwen 3.7 Plus,距 Qwen 3.7 Max 上线刚好 11 天。同样的 1M 上下文,同样的 35 小时自治上限。但价格才是头条:Plus 是 0.40/M输入,Max是 2.50/M——便宜约 6 倍——并且还能看图、看视频。Vision Arena 上 Plus 已经排到 #16。所以这周真正值得讨论的问题不是”要不要为视觉能力买单”,而是”Max 凭什么用 6 倍价格换来 2 个百分点的 benchmark 领先”。
|
11天前
|
缓存 人工智能 运维
GLM 5.2自托管全流程实战:硬件选型、vLLM/SGLang部署与成本盈亏测算
2026年智谱发布GLM 5.2超大混合专家模型,区别于以往仅开放API的闭源大模型,该模型权重以MIT开源协议对外发布,企业与开发者可完整下载、本地审计、私有化部署,实现数据不出环境、自定义微调、自主调度推理资源。GLM 5.2拥有753B总参数,原生支持百万级上下文窗口,在代码生成、长文档推理、数学逻辑等多项基准测试中对标国际顶尖商用模型,是首款可完整自托管的前沿代码向大模型。
912 0
|
13天前
|
Linux 程序员 数据格式
【2026最新】Notepad++下载、安装和使用一篇搞定(附中文版安装包)
Notepad++ 是一款免费开源、轻量高效的 Windows 文本编辑器,支持 C/Python/HTML 等 80+ 语言语法高亮、代码折叠、正则替换、编码转换及插件扩展,专为程序员与文本处理用户打造,完美替代系统记事本。(239字)