半夜三点,报警短信突然炸响,服务器监控显示数据库连接全部中断。你迷迷糊糊爬起来,发现服务已经卡死,用户请求堆积如山。这种场景,不少运维和开发都经历过。问题的根源,往往不是数据库崩了,而是连接断开了,程序没处理好重连。
连接为什么会断?
数据库连接不是永远在线的。网络抖动、防火墙超时、数据库重启、负载过高,都会导致连接突然失效。比如,公司用的云数据库默认8小时清理空闲连接,如果应用长时间没操作,再发起查询,就会收到“MySQL has gone away”这类错误。
更麻烦的是,有些断连不会立刻暴露。连接看似正常,但实际已失效。程序发个请求,等十几秒才报错,用户体验直接崩盘。
重连不是加个 try 就完事
很多人觉得,不就是断了再连吗?try 一下,catch 到异常就重新 connect。可真这么干,很容易踩坑。
比如,短时间内频繁重连,可能触发数据库的连接数限制或IP封禁策略。特别是高并发场景,成百上千的线程同时重连,数据库瞬间被压垮,形成雪崩。
还有,事务状态怎么办?如果断连前正在执行转账操作,重连后事务已经丢失,不加判断直接继续,可能导致数据错乱。
靠谱的重连策略长什么样
真正稳定的方案,得有退避机制。第一次失败,等1秒重试;再失败,等2秒;接着4秒、8秒,指数级递增,避免集中冲击。同时设置最大重试次数,超过就彻底报错,防止无限循环。
代码层面,可以用装饰器或中间件封装重连逻辑。以 Python 的 pymysql 为例:
import time
import random
import pymysql
def retry_query(max_retries=3, backoff_factor=1):
def decorator(func):
def wrapper(*args, **kwargs):
for i in range(max_retries + 1):
try:
return func(*args, **kwargs)
except (pymysql.err.OperationalError, pymysql.err.InterfaceError) as e:
if i == max_retries:
raise e
sleep_time = backoff_factor * (2 ** i) + random.uniform(0, 1)
time.sleep(sleep_time)
return None
return wrapper
return decorator
@retry_query(max_retries=3)
def get_user_data(user_id):
conn = pymysql.connect(host="localhost", user="root", password="123", db="test")
cursor = conn.cursor()
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
result = cursor.fetchone()
cursor.close()
conn.close()
return result
这段代码对查询函数做了包装,遇到连接类异常会自动重试,还加入了随机延迟防抖。
连接池才是日常主力
在真实项目里,手动管理连接太危险。主流做法是用连接池,比如 HikariCP、DBCP 或 SQLAlchemy 的引擎池。它们会自动检测连接健康状态,失效的连接会被丢弃,新请求分配到可用连接。
关键是要配对参数。比如设置 validationQuery 定期探活,开启 testOnBorrow 在取出连接时检查。否则池子里一堆僵尸连接,照样出问题。
别忘了监控和告警
再好的重连机制也只是补救。应该早做预警。比如监控连接池的活跃数、等待数,突然飙升可能意味着连接泄漏。记录重连日志,分析频率和时机,能提前发现网络或配置隐患。
某次线上事故复盘发现,每天上午9点准时出现批量重连。排查后才发现是公司防火墙每两小时清理一次TCP长连接,而应用恰好在整点前后进入高频访问期。调整防火墙策略后,问题消失。
连接断开不可怕,可怕的是毫无准备。一个健壮的系统,得把断连当成常态来设计,而不是意外来应对。