拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Docker容器内连接数据库删除数据:从docker exec到SQL实操

Docker容器内连接数据库删除数据:从docker exec到SQL实操

先说个很常见的场景:某个用 Docker 部署的项目跑了一段时间,测试环境里堆积了一堆脏数据,或者业务方要求把某个模块的数据清空重来。这时候很多人的第一反应是“上服务器,进容器,连数据库,DELETE FROM”,但实际操作起来,远比这四个词要曲折。尤其是你对着docker exec -it敲了半天,要么发现容器里连 mysql 客户端都没装,要么连上了却分不清自己该操作哪个库、哪张表。

这篇东西我按自己真实处理过的“进容器连接数据库删除数据”这类需求来写,把整个链路拆开讲清楚,包括为什么要进容器、进去之后怎么定位数据库、常见的删除姿势有哪些坑、以及什么情况下其实不该进容器。整个过程以 MySQL 为主,顺带提 PostgreSQL 和 MongoDB,因为这几类在 Docker 部署里最常碰见。

1. 为什么非要“进容器连数据库”,而不是直接在宿主机连

很多人觉得 Docker 部署的项目,数据库的端口都映射到宿主机了,直接用 Navicat 或者命令行连不就行了,干嘛要费劲进容器。这个想法没错,但要分情况。

1.1 Docker 部署下数据删除没那么“所见即所得”

Docker 容器本身是一个隔离的运行环境,数据库进程跑在容器里,数据文件则通过数据卷或 bind mount 挂在宿主机上。具体到删除数据这个动作,你直接删容器、删镜像,只会把运行环境销毁,数据卷还留在那。真正要清掉数据,还是得回到数据库服务本身去执行删除逻辑。

这里就有第一个关键点:你通过宿主机映射端口去连数据库,本质上和进容器连数据库,操作的是同一个数据库实例、同一份数据文件,结果没有区别。但为什么很多教程、很多同事都坚持“进容器里去连”呢?我总结下来有三类实际原因。

第一,不是所有项目都会把数据库端口暴露到宿主机。尤其现在 Compose 文件里经常只写expose而不写ports,这种部署方式意味着宿主机根本无法直接访问容器内的数据库端口,你只能进入容器网络内部去操作。这类项目在微服务架构里尤其常见,数据库只对内网服务开放,对外不留口子。

第二,容器启动时通过环境变量注入了数据库账号、密码、库名。你从宿主机命令行去连,得先去翻.env文件、Compose 配置或 Docker 容器的 inspect 信息,把账号密码拼接好;而进入容器之后,很多镜像自带的环境变量已经帮你把连接参数半准备好了。

第三,也是很多人忽略的一点:在容器内操作,能避免宿主机上的工具链和网络问题。比如宿主机只装了 Docker 没装 mysql 客户端,或者装了但版本太旧导致认证插件不兼容,你进容器直接用镜像内自带的客户端就不会有这种问题。

1.2 删容器不等于删数据,别把概念搞混

我见过不止一个人以为“数据删除”就是把容器删了,尤其测试环境里图省事。如果这个项目没有挂载数据卷,容器删了数据确实没了,但这也意味着你连回滚的机会都没有。而绝大多数生产或预发部署都会挂数据卷,删除容器只会让数据库进程消失,数据文件依然在/var/lib/docker/volumes/项目名_数据卷名/_data里。你重新启动一个新容器,挂上同一个数据卷,数据完好无损。

所以“删除数据”这个动作,必须落到数据库层面:要么执行业务相关的 DELETE 语句,要么 TRUNCATE 整张表,要么 DROP 整个库。而这些操作的第一步,就是你得能连上数据库。本文标题说的“进容器连接数据库删除”,正是这套动作里最标准的一条路径。

2. 删除数据前的准备工作,先搞清楚项目和数据长什么样

进容器之前,我通常会花几分钟把项目的部署情况摸清楚。磨刀不误砍柴工,这个环节省不得,尤其是对不熟悉的项目,冒然进容器乱敲命令,很容易删错数据。

2.1 摸清部署形态:容器名、数据卷、环境变量

先看容器列表。如果你的项目是用 Docker Compose 部署的,通常容器名会有固定的前缀,比如projectname_mysql_1或者projectname-db-1。用docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}"能快速看清当前有哪些容器在跑。

拿一个典型的 Java 后端项目举例:

docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}"

输出大概长这样:

NAMES IMAGE STATUS PORTS project-app openjdk:17-jdk-slim Up 12 minutes 0.0.0.0:8080->8080/tcp project-mysql mysql:8.0.32 Up 12 minutes 0.0.0.0:3306->3306/tcp project-redis redis:7.0-alpine Up 12 minutes 0.0.0.0:6379->6379/tcp

数据库容器一般就是project-mysql这种命名规律,一眼能认出来。接下来重点看两样东西:数据卷和环境变量。

docker inspect project-mysql --format '{{json .Mounts}}' | jq

这里输出的内容会包含 Type、Source、Destination,也就是数据卷的宿主机路径和容器内路径。确认数据卷挂载没问题之后,再查看环境变量里的数据库初始化参数:

docker inspect project-mysql --format '{{range .Config.Env}}{{println .}}{{end}}'

通常能输出MYSQL_ROOT_PASSWORD、MYSQL_DATABASE、MYSQL_USER、MYSQL_PASSWORD这些变量。这一步非常重要,因为很多项目并不直接用 root 账号连接数据库,而是单独建了一个应用账号。你进容器之后,要用这些环境变量里给出的账号才能正确连接。

2.2 确认连接方式和数据库类型

第二个准备工作是确认数据库类型和连接工具。虽然标题写的是“进容器连接数据库”,但不同数据库镜像自带的客户端工具完全不同。

我按项目里最常见的三类做了一个对照:

数据库类型典型容器镜像容器内客户端命令连接后典型操作
MySQL 5.7/8.0mysql:5.7, mysql:8.0mysqlDELETE / TRUNCATE / DROP
PostgreSQLpostgres:14, postgres:15psqlDELETE / TRUNCATE / DROP
MongoDBmongo:5.0, mongo:6.0mongoshdeleteMany / drop

对于 MySQL 来说,镜像的入口命令是mysql,PostgreSQL 是psql,MongoDB 在新版镜像里是mongosh(老版本才是mongo)。这些命令在容器内的 PATH 里已经配好了,不需要额外安装。

举个例子,一个简单的清理业务数据操作,在 MySQL 容器内执行,大致是这样的:

docker exec -it project-mysql mysql -uroot -p

输入密码后进入 mysql 命令行,然后执行 SQL。如果你连不上,大概率是密码或者 host 指定方式的问题。

2.3 设计一个“安全删除”的最小流程

准备工作的最后一步,是在动手之前想清楚怎么删。这个步骤决定了你后面会不会手忙脚乱。我习惯把整个删除动作设计成四个阶段:

备份 -> 确认目标 -> 执行删除 -> 验证结果

备份这一步看起来多余,实际非常关键。即使你只是清一张表,也可能有业务方反悔想要回数据。用mysqldump在容器内直接导一份备份出来,成本很低,但能给你上双重保险。

3. 实操全程:从 Docker Exec 到数据库命令行

这一节我把“进容器连数据库删除数据”的整个过程一步一步拆开,每一步都给出命令和说明,你照着做就能跑通。这里用 MySQL 8.0 作为主要演示对象。

3.1 进入容器:docker exec 的参数细节

进容器最常用的命令是docker exec -it <容器名> <命令>。这里的三个部分都有讲究:

  • -i表示保持标准输入打开,这样才能往容器里的进程输入内容。
  • -t表示分配一个伪终端,这样才能获得一个带交互界面的 shell,否则你看到的是一个 Docker 报错或者毫无反应的回显。
  • 最后是要执行的命令,最常见的是/bin/bash,因为像 MySQL 官方镜像基于 Oracle Linux 或 Debian,默认的 shell 就是 bash。有些精简镜像可能连 bash 都没有,只有 sh,这时可以改用/bin/sh。

常用写法:

docker exec -it project-mysql /bin/bash

进入之后,你自己会看到类似root@容器ID:/#的提示符,说明你已经进入了容器内的 shell。如果这里提示bash: /bin/bash: No such file or directory,改用/bin/sh就能进去。

3.2 在容器内定位数据库和表

进入 shell 之后,第一件事是确认当前环境。如果你是从零排查一个陌生项目,建议先执行几个命令看看。

首先确认 mysqld 进程是否还健康运行:

ps aux | grep mysqld

如果没有 ps 命令,可以看看 mysql 客户端是否能连上服务。其次确认环境变量里有没有 MYSQL_DATABASE 之类的情报:

env | grep MYSQL

接下来就是连接数据库。这里的 host 地址非常重要:因为你现在就处于容器内部,直接用-uroot -p走本地 socket 连接就行,不需要指定-h 127.0.0.1,也不需要走 TCP 的 3306 端口。

mysql -uroot -p

输入密码之后,进入 mysql 交互式命令行。如果你是从 Compose 环境变量里面读到初始密码,直接粘贴即可。这里有一个细节:输入密码时是看不到任何字符的,回显也不会有星号,这是正常现象,别以为键盘坏了,直接输完按 Enter。

3.3 用 SQL 完成数据删除

进入 MySQL 命令行之后,先确认当前连的是哪个库:

SELECT DATABASE();

如果这个查询返回 NULL,说明还没有选中数据库。一个很实用的习惯是用SHOW DATABASES;先看看有哪些库,然后USE选库。

以“订单表”为例,假设库名是mall,表名是orders,你想清空这张表里的所有数据:

USE mall; SELECT COUNT(*) FROM orders; TRUNCATE TABLE orders;

这里我特意加了一步SELECT COUNT(*),目的是在执行删除前看一眼数据量,顺便确认表名没有打错。如果业务上只要求删除某一部分数据,比如删除 90 天前的数据,那就用 DELETE 加 WHERE 条件:

DELETE FROM orders WHERE create_time < NOW() - INTERVAL 90 DAY;

DELETE 和 TRUNCATE 的区别也需要说清楚。TRUNCATE 是 DDL,执行完之后整个表的数据立即消失,且不可按行恢复,自增主键的计数也会被重置,通常比 DELETE 快得多;DELETE 是 DML,逐行删,支持 WHERE 过滤,会触发事务和触发器,也可以通过事务回滚。测试环境清理用 TRUNCATE 非常痛快,线上业务删除必须慎用,尽量用 DELETE 配合 WHERE。

3.4 PostgreSQL 和 MongoDB 的对照操作

如果你的项目用的不是 MySQL,下面这两段直接抄作业。

PostgreSQL 的容器通常叫project-postgres之类。进入容器后,默认的用户可能不是 root 而是postgres,这个用户是镜像初始化时创建的超级用户,可以直接用它连接数据库:

docker exec -it project-postgres psql -U postgres -d appdb

在 psql 里清空一张表的命令:

\c appdb TRUNCATE TABLE users;

MongoDB 则有所不同。它的客户端是 mongosh,连接方式和 SQL 数据库差异很大:

docker exec -it project-mongo mongosh

清空一个集合的数据用:

use appdb; db.users.deleteMany({});

如果要彻底删掉整个集合,用db.users.drop(),效果比 deleteMany 更彻底,索引也会一并被清掉(如果还需要集合结构,就用 deleteMany)。

4. 容器内连库删除的高频故障与排查实录

这一节是全文最有价值的部分,全是我自己试错试出来的经验。遇到问题时,可以对照这里的表格先自查。

4.1 容器里根本没有数据库客户端

这是最尴尬的情况,没有之一。有些数据库镜像为了精简体积,只包含服务端而没有客户端,或者反过来只有客户端。比如某些从云厂商直接拉下来的 “定制版” 数据库镜像,内部精简到只保留数据库服务,没有 mysql 二进制文件。

你敲mysql -uroot -p,返回bash: mysql: command not found。遇到这种情况,不要慌,有两条路可以走。

第一条路,去宿主机找 mysql 客户端连容器的映射端口。前提是你知道宿主机端口是 3306,并且数据库存在映射:

mysql -h 127.0.0.1 -P 3306 -uroot -p

第二条路,临时起一个带客户端的容器,链接到同一个 Docker 网络里:

docker run -it --rm --network project_default mysql:8.0 mysql -h project-mysql -uroot -p

这个容器会临时连接项目所在的 Docker 网络,然后从网络内部去访问project-mysql这个容器名,因为 Docker 网络默认支持容器名作为 DNS 解析。

4.2 连接时报错 “Access denied” 或认证插件问题

MySQL 8.0 默认的认证插件是caching_sha2_password,如果你在宿主机用的是老版本的 mysql 客户端(5.7 时代),报错信息通常长这样:

ERROR 2059 (HY000): Authentication plugin 'caching_sha2_password' cannot be loaded

解决方式有两种。一种是把数据库账号的认证插件改回mysql_native_password,但这不是长久之计。更推荐的做法是在容器内用镜像自带的客户端,因为它一定匹配当前数据库版本的认证方式。所以我一般劝人别在宿主机搞一个和你数据库版本不匹配的客户端进去硬连,这是问题的根源。

还有一种 Access denied 是直接用错了密码,尤其是用MYSQL_PASSWORD(应用账号的密码)去连 root,或者反过来用 root 密码去连应用账号。这里建议先执行env | grep MYSQL确认当前容器里的密码,再去连接。

4.3 删除时长时间卡住,或者一直报“Lock wait timeout exceeded”

这个坑主要出现在大表删除和线上联调环境。DELETE 一条条删的时候,如果表上有其他服务正在写入,很容易触发锁等待。MySQL 默认的innodb_lock_wait_timeout是 50 秒,超过之后会直接报错中断。

我的经验是,DELETE 大批量数据之前,先看看当前表的索引情况。比如你删除create_time < NOW() - INTERVAL 90 DAY的数据,如果create_time字段没有索引,MySQL 需要全表扫描并逐行判断,删除效率非常低,也更容易造成长事务。要么加索引,要么把一次删除拆成多批:

DELETE FROM orders WHERE create_time < NOW() - INTERVAL 90 DAY LIMIT 1000;

反复执行这条语句,每批删 1000 行,等删完后再查总行数,比一次性 DELETE 十万行要稳定得多。删到没有行数返回之后,就说明删干净了。

4.4 容器里的时区导致“时间条件”删错

这个坑非常隐蔽。很多 MySQL 镜像默认的时区是 UTC,而你的应用写入时间可能是东八区,导致你写 WHERE 条件时有偏差。排查方式很简单:

SELECT NOW(), UTC_TIMESTAMP();

如果两个时间差了 8 个小时,说明时区没设对。处理方式有两类:一是删除时直接用DATE_SUB(NOW(), INTERVAL 90 DAY)来和存储的本地时间比较,这样不受会话时区影响;二是执行SET time_zone = '+08:00';修改当前会话时区。但要注意,修改会话时区只对当前连接生效,下次进容器还是 UTC,所以最稳妥的办法还是在 Compose 文件里给数据库容器配置 TZ 环境变量,或者在 MySQL 配置文件里设置default-time-zone = '+08:00'。

4.5 外键约束与关联表数据

如果项目表结构里设置了外键,直接删父表数据大概率会报错:

ERROR 1451 (23000): Cannot delete or update a parent row: a foreign key constraint fails

这种就属于不能“无脑删”的类型,你得先理清依赖关系。最省事的办法是在一个事务里先禁用外键检查,删除完成后再恢复:

SET FOREIGN_KEY_CHECKS = 0; DELETE FROM parent_table WHERE id = 100; SET FOREIGN_KEY_CHECKS = 1;

但这个操作要非常小心,禁用了外键检查之后,你删除的数据可能造成子表产生孤立记录。我的建议是删除前先查一下子表的数据,确认确实不需要保留,或者子表本来就是要一起清空的关联数据,再禁用外键;否则还是老老实实按依赖顺序逐表删。

5. 进容器删数据的安全底线,以及另一种“更不推荐”的做法

最后这部分,我把它当作一个总结,也是一个提醒。

进容器连接数据库删除数据的核心操作,本身不复杂,复杂的是删除操作背后的安全边界。我踩过几次坑之后,给自己订了几条规矩,现在写下来,你也适用。

5.1 我的容器内删除检查清单

每次操作前,我会对照这份清单走一遍:

  • 备份做过没有。没有备份不执行任何删除操作,哪怕只是一个测试环境的清理。
  • 删除目标确认过没有。库名、表名、WHERE 条件里的字段,一个都不能错。
  • 当前执行的容器是不是对的那个。尤其同时跑多个类似项目容器时,很容易进错容器。
  • 有没有先在低峰期执行。如果业务量还很大,大批量 DELETE 会拖垮数据库性能。
  • 事务边界有没有想好。对于 DELETE 操作,我通常会先BEGIN;,删除后用SELECT COUNT(*)验证结果,确认无误再COMMIT;,发现不对就ROLLBACK;,非常管用。
  • 删除语句是不是用 LIMIT 分批执行。大批量数据一次删完的后果谁来都承担不起。

5.2 什么情况下,我不建议进容器去删

进容器是解决“只能从内部访问”的通用手段,但不是最优解。如果你的项目已经把数据库端口映射到了宿主机,我更推荐先在宿主机上用已安装的 GUI 工具(比如 Navicat、DBeaver)看一遍数据再操作。原因很简单:GUI 工具给的信息比命令行直观得多,你能直接看到行的内容,确认是不是要删的数据,误删风险低很多。

另外,那些需要定时清理的数据,根本不该走“进容器手动删”这条路。你应该在项目里增加定时任务(比如用 cron 加一条简单的 SQL 清理脚本,或者在应用层面实现定时清理),让系统自动处理,把人工操作从高频变成低频。手动进容器这种操作,就越少越好。

5.3 一个真实的现场片段作为收尾

最近一次处理一个 Docker 部署的订单模块,业务方要求把测试环境的订单全部清空,并重置自增主键。我先docker inspect确认了project-mysql容器的挂载和账号,进容器后mysqldump做了一份备份到宿主机/data/backup目录,然后执行:

USE mall; TRUNCATE TABLE orders;

其实核心操作就这么一句话,几十秒搞定。但如果没有前面对表结构的确认、没有备份、没有通过环境变量确认密码,我不可能这么痛快地输出这条命令。很多时候,所谓“老手和新手的差距”,就在动手之前的准备功夫上。

返回列表