简介:《媒体数据库与云存储》实验(四)实验报告以PDF形式完整呈现OpenStack云计算与Swift对象存储实践全过程,面向中国传媒大学信息与通信工程学院学生及需要入门云平台部署的云计算学习者。报告从VMware虚拟网络配置、OpenStack环境搭建、Dashboard租户/项目与用户管理、云主机实例创建,到Swift客户端认证、容器创建、对象上传与元数据查看/增删改等均有分步截图和命令说明,并附思考题解答与public/private子网、路由器通信机制等网络架构分析,便于读者对照复现实验、修补课程记录或深入理解IaaS平台核心概念。资源涵盖数据库与云存储课程实验链路,含MySQL、SQL、XAMPP等标签相关背景,但主体聚焦OpenStack与Swift对象存储。资源包为1个PDF文件,大小3.66MB,图文结合、操作路径清晰,可直接用于课程复习与实操参考。目前已有84人学习下载,适合作为云计算与云存储方向实验教学和自学的配套资料。
1. 媒体数据库与云存储实验四:一门课为什么把对象存储和关系库焊在一起
媒体数据库与云存储不是两门独立的课,在实验(四)里它们第一次被焊成一套完整系统:媒体文件本身进云存储桶,文件的名字、大小、路径、上传人等元数据进数据库,业务代码负责让两边保持一致。这个标题下的实验报告,本质上是在复刻一个真实的媒体资源系统——无论是教学实训里的图片库,还是生产环境的音视频平台,核心链路都是同一套。适合直接照做的人群是正在补这门课的在校生、刚接手媒体资源模块的初级后端,以及想给本地文件库上云的从业者。下面直接按实验四的关键步骤拆开讲:环境怎么搭、双写怎么做、参数怎么设、坑在哪。
2. 先把实验环境搭稳:媒体数据库选型和阿里云存储桶的创建
2.1 媒体数据库选型:为什么元数据必须落库
实验报告里最容易被跳过的是建库这一步。很多同学觉得图片已经传到存储桶了,数据库好像没多大用,于是随便建一张表应付。这个想法会在实验四后半段付出代价:你要按分组检索媒体、要统计上传量、要按文件名模糊搜索,这些操作对象存储桶都没法做,OSS只支持前缀列举,不支持按内容字段查询。所以“媒体数据库”这个前缀不是修饰,是指你必须用一张关系表去描述媒体文件。
表结构按常见做法来,我会建一个 media_asset 表,字段设计至少包含业务主键、对象名、存储桶、文件类型、大小、哈希值、上传时间和状态。对象名用 object_name 单独存,不要直接用文件路径做主键,因为同名的媒体文件会被存储桶重命名,业务上需要稳定的逻辑 ID。给一个建表 SQL:
CREATE TABLE media_asset ( id BIGINT AUTO_INCREMENT PRIMARY KEY, asset_no VARCHAR(64) NOT NULL UNIQUE COMMENT '业务编号,生成后不可变', object_name VARCHAR(512) NOT NULL COMMENT '云存储桶中的对象名', bucket_name VARCHAR(128) NOT NULL COMMENT '对象所在的存储桶', content_type VARCHAR(128) NOT NULL DEFAULT 'application/octet-stream', file_size BIGINT NOT NULL DEFAULT 0 COMMENT '文件字节数', file_hash CHAR(64) NOT NULL COMMENT '上传时计算的SHA256', upload_status TINYINT NOT NULL DEFAULT 0 COMMENT '0待确认 1已上传 2失败', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_bucket_object (bucket_name, object_name), KEY idx_created_at (created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这段 SQL 的要点在于:asset_no 用唯一索引而不是直接把自增主键暴露给业务,避免遍历存储桶时对上数字主键;file_hash 字段用于数据一致性比对,后面验证章节会用到;upload_status 是给双写补偿留的开关,这比把所有状态堆在异常里更直观。utf8mb4 是必须的,媒体文件名经常带中文和特殊字符,老版本的 utf8 字符集会在这里翻车。
2.2 阿里云存储桶的创建:从 AccessKey 到权限模型
实验对象用阿里云存储桶(OSS)来做,普通上传场景下需要三个基本信息:Endpoint、Bucket 名称、AccessKey。Endpoint 形如 oss-cn-shanghai.aliyuncs.com,决定了你的存储桶建在哪个地域。Bucket 名称全局唯一,创建后不建议随意改。AccessKey 用于程序签名,实验阶段可以只用主账号的 AccessKey,但生产上一定要改用 RAM 子账号,只授予目标 Bucket 的读写权限。理由很简单:AccessKey 泄露后的爆炸半径,子账号可以限制在一个存储桶内。
创建存储桶时,权限选择“私有”而不是“公共读”。实验报告里很多人为了省事,直接设公共读,图片 URL 贴到浏览器就能开。但这会让整个存储桶暴露在公网,别人可以遍历你的对象名。实验四的正确做法是:Bucket 权限设为私有,访问时依赖签名 URL 对外暴露。后面第 4 章会专门讲私有桶访问的坑。关于地域,建议把数据库和 Bucket 放在同一地域,比如华东 2(上海),Endpoint 写死 oss-cn-shanghai.aliyuncs.com,数据库连接串用内网地址而不是公网地址。实验阶段跨地域看不出问题,上线后每次读写都走公网传输,费用和延迟都不可控。
创建完成后,我一般会先用一个最小脚本验证凭证有效。这个脚本在上传正式媒体前把链路打通:
# put_test_object.py import oss2 endpoint = 'oss-cn-shanghai.aliyuncs.com' access_key_id = '你的AccessKeyId' access_key_secret = '你的AccessKeySecret' bucket_name = 'lab-media-2024' bucket = oss2.Bucket( oss2.Auth(access_key_id, access_key_secret), endpoint, bucket_name ) # 上传一个测试对象,验证Endpoint、密钥、Bucket权限这三项配置是否同时正确 result = bucket.put_object_from_file('test/hello.txt', 'hello.txt') print('etag:', result.etag) print('request_id:', result.request_id)这段代码跑通,说明网络能到该地域的 OSS 服务、AccessKey 对此 Bucket 有写权限。如果在这一步就报 403 InvalidAccessKeyId 或 NoSuchBucket,不要继续往后做,先把环境中的这三项复制对齐。request_id 是排查时要记录的信息,线上反馈上传失败时,把 request_id 丢给技术支持比截图强得多。
2.3 最小联通验证:确认数据库和存储桶都能写入
2.2 的脚本只验证了存储桶,还需要验证数据库。我会把第 2.1 的建表 SQL 在 MySQL 里执行一次,然后写入一条测试记录,再删除,确保实验环境账号有建表和改数据的权限。这一步是前置条件检查,用实验台账的思路走:
mysql -h127.0.0.1 -ulab_user -p lab_media < media_asset.sql执行无误后,再用 pymysql 插入一条临时记录并回查:
import pymysql conn = pymysql.connect( host='127.0.0.1', user='lab_user', password='your_password', database='lab_media', charset='utf8mb4', autocommit=True ) with conn.cursor() as cur: cur.execute( "INSERT INTO media_asset (asset_no, object_name, bucket_name, content_type, file_size, file_hash, upload_status) " "VALUES ('INIT-001', 'test/hello.txt', 'lab-media-2024', 'text/plain', 5, '', 0) " "ON DUPLICATE KEY UPDATE object_name=VALUES(object_name)" ) cur.execute("SELECT asset_no, object_name FROM media_asset WHERE asset_no='INIT-001'") print(cur.fetchone()) conn.close()这里把 autocommit 打开,是为了减少实验阶段的连接悬挂。正式代码里会用事务处理,但联通验证阶段不需要事务,先把“能连、能写、能查”确认下来。INSERT 语句特意加了 ON DUPLICATE KEY UPDATE,让你反复跑这个脚本不产生重复记录,实验迭代时少踩一次坑。如果实验机没有外网,可以用 MinIO 在本地起一个兼容 S3 接口的对象存储服务来验证代码逻辑,但实验四既然叫云存储,至少一遍要走真实 OSS,否则验证不了签名有效期和权限差异这些云特性。
3. 实验四的主线任务:媒体文件上传与元数据双写
3.1 先传文件还是先写库:编排顺序不能拍脑袋
实验四的核心代码不多,真正决定成败的是编排顺序。先写库再传文件的方案在实验报告里很常见,流程是:INSERT 一条记录状态设为 0,上传成功后 UPDATE 状态为 1,失败则把记录删掉或标记失败。这个方案的好处是业务 ID 可以预先产生,坏处是状态值散落各处,很多人忘记做失败清理,数据库里堆了一堆 pending 记录,对象的实际状态成了一个黑匣子。
我一般用先传文件再写库的顺序,它的链路由两步构成:第一步 put_object 拿到 object_name 和 etag,第二步 INSERT media_asset 记录。如果 INSERT 失败,调用 delete_object 回滚这个刚上传的对象。这个方案的数据一致性检查最直观:上传成功但库里没有记录的情况,可以通过 delete_object 立即回收,不给存储桶留下孤儿对象。两条路没有绝对对错,实验报告里选一种并说清理由即可,但代码结构必须是可回滚的。
3.2 一段可复现的完整上传实验代码
下面是实验四常见做法:用 Python 的 oss2 和 pymysql 把一张图片双写到存储桶与数据库。这段代码包含了计算哈希、上传、插入、失败回滚四个环节。
import os import uuid import hashlib import datetime import oss2 import pymysql def calc_sha256(file_path): h = hashlib.sha256() with open(file_path, 'rb') as f: for chunk in iter(lambda: f.read(65536), b''): h.update(chunk) return h.hexdigest() def upload_media(file_path, asset_no): endpoint = 'oss-cn-shanghai.aliyuncs.com' ak_id = '你的AccessKeyId' ak_secret = '你的AccessKeySecret' bucket_name = 'lab-media-2024' auth = oss2.Auth(ak_id, ak_secret) bucket = oss2.Bucket(auth, endpoint, bucket_name) # 对象名按 年/月/日/asset_no 组织,避免同一目录文件数过多 now = datetime.datetime.now() object_name = f"{now:%Y/%m/%d}/{asset_no}_{os.path.basename(file_path)}" # 先传对象到存储桶 with open(file_path, 'rb') as f: result = bucket.put_object(object_name, f) print('upload etag:', result.etag) return bucket, object_name, os.path.getsize(file_path) def insert_record(asset_no, object_name, bucket_name, file_path): content_type = 'image/jpeg' if file_path.endswith(('.jpg', '.jpeg')) else 'application/octet-stream' file_size = os.path.getsize(file_path) file_hash = calc_sha256(file_path) conn = pymysql.connect( host='127.0.0.1', user='lab_user', password='your_password', database='lab_media', charset='utf8mb4' ) try: with conn.cursor() as cur: cur.execute( "INSERT INTO media_asset " "(asset_no, object_name, bucket_name, content_type, file_size, file_hash, upload_status) " "VALUES (%s, %s, %s, %s, %s, %s, 1)", (asset_no, object_name, bucket_name, content_type, file_size, file_hash) ) conn.commit() except Exception: conn.rollback() raise finally: conn.close() def main(): file_path = './sample.jpg' asset_no = uuid.uuid4().hex[:12] try: bucket, object_name, _ = upload_media(file_path, asset_no) except Exception: print('上传存储桶失败,不再写库') return try: insert_record(asset_no, object_name, bucket.bucket_name, file_path) except Exception as e: print('写库失败,回滚删除对象:', e) bucket.delete_object(object_name) raise if __name__ == '__main__': main()逻辑说明:main 里先执行 upload_media,成功后才执行 insert_record;insert_record 失败,delete_object 回滚。这样“文件传了但库里没有”的场景被压缩到极短的时间窗。参数说明:asset_no 用 uuid 的 12 位 hex,避免和别人的实验记录撞主键;对象名按年月日分目录,是为了绕开 OSS 单目录对象数量过多时的列举性能问题;content_type 这段用最简判断,完全可以用 mimetypes 库做更精确匹配。这里的失败回滚不是终极方案,但作为实验四的代码结构,逻辑清晰且每步可重放。
3.3 读取链路:从数据库记录到带签名的可访问 URL
写入链路搭完,读取链路是实验报告里另一个占分的点。从数据库查到记录后,不能直接拼接一个公网 URL 给前端,因为 Bucket 是私有的。常见做法是用 oss2 的 sign_url 生成一个临时授权 URL,把过期时间参数传进去。
def gen_access_url(bucket_name, object_name, expires=3600): endpoint = 'oss-cn-shanghai.aliyuncs.com' ak_id = '你的AccessKeyId' ak_secret = '你的AccessKeySecret' bucket = oss2.Bucket(oss2.Auth(ak_id, ak_secret), endpoint, bucket_name) # 生成GET签名URL,有效期为expires秒 url = bucket.sign_url('GET', object_name, expires) return url签名 URL 的本质是“把 AccessKeyId、有效时间、对象名、请求方法拼进签名串,由服务端校验”。实验过程中最容易把桶名称写错,API 返回 404 但看不出是桶不存在还是对象不存在。建议在 gen_access_url 里加一句 bucket.object_exists(object_name) 判断,方便日志里区分。expires 参数在实验里用 3600 够,但做报表导出这类离线任务建议直接把文件抓回服务器再用内部链接分发,不要把有效期拉到数天。
4. 实验四避坑记录:权限、签名、一致性最容易翻车的三个地方
4.1 私有桶的图片 URL 打不开
现象:把数据库里的 object_name 拼成 https://bucket.endpoint/object_name 丢到浏览器,返回 AccessDenied。
原因:Bucket 创建时选了私有,而第 3.3 生成的签名 URL 没有透传到前端,前端拿的是不带签名的直链。这是实验报告最常见的翻车点,代码逻辑是对着“公有读”写的,环境里却用的私有桶。
解决:统一走签名 URL,不能有一个代码路径偷偷拼接直链。建议在 SQL 查询后直接调用 gen_access_url,把签好的 URL 作为接口返回值下发给前端,而不是只返回 object_name。同时确认 Bucket 访问域名用的是 OSS 默认域名,如果绑了自定义域名,签名 URL 的域名需要和签名参数保持一致,否则签名校验会失败。
4.2 签名 URL 过期导致页面图片一会儿能开一会儿不能开
现象:前端打开了页面,图片正常;过了几分钟再刷新,部分图片报 403,部分还是好的。
原因:oss2.sign_url 生成的 URL 带有过期时间参数,实验里习惯统一设 3600 秒。前端浏览器缓存了 HTML,但图片 URL 已经过期,于是出现“页面还在、图挂了”的状态。
解决:把过期时间设成一个相对业务合理的值,比如预览场景 600 秒,下载场景 30 秒;更稳的做法是让接口每次返回签名 URL 时都重新生成,而不是把 URL 存到数据库里重放。签名 URL 一旦生成,服务端无法主动撤销,只能等它自然过期,所以有效期能省的不要多给。
4.3 文件传完数据库没有记录,存储桶里出现孤儿对象
现象:上传接口返回失败,但去存储桶控制台看,对象已经存在;数据库里查不到记录,重试后同一个文件被传了多次。
原因:双写流程没有回滚,或者写库失败时异常被吞掉。很多实验代码把 insert_record 的 try/except 写成 print 了事,没有 delete_object。媒体文件一旦落地 OSS,没有数据库索引,想靠人工控制台找到它非常痛苦。
解决:在主流程里用第 3.2 的回滚结构,写库失败主动调 delete_object。另外在实验收尾时做一次对账:用 list_objects 把 Bucket 中所有对象列出来,和 media_asset 表里的 object_name 做差集。会写这个对账脚本,实验报告的质量会明显高一个档次。
import oss2 import pymysql bucket = oss2.Bucket(oss2.Auth('ak', 'secret'), 'oss-cn-shanghai.aliyuncs.com', 'lab-media-2024') conn = pymysql.connect(host='127.0.0.1', user='lab_user', password='your_password', database='lab_media') cur = conn.cursor() # 列举Bucket中全部对象 for obj in oss2.ObjectIterator(bucket): cur.execute("SELECT COUNT(*) FROM media_asset WHERE object_name=%s", (obj.key,)) if cur.fetchone()[0] == 0: print('孤儿对象:', obj.key) conn.close()这个脚本还可以加一行 bucket.delete_object(obj.key) 做成自动清理,但从实验角度我建议先跑只读版,确认差集逻辑可靠后再删,不然容易出现误删。
4.4 文件名编码和类型伪装
现象:上传“风景_2024.jpg”,控制台里对象名变成乱码;或者往上传了一个 exe,ContentType 却写成 image/jpeg。
原因:put_object_from_file 默认按 UTF-8 处理对象名,但数据库连接的 charset 如果还是 utf8 而不是 utf8mb4,写入时中文会被转成问号。类型伪装则是客户端可篡改 ContentType,你不能信任浏览器传上来的值。
解决:数据库连接串统一用 utf8mb4,对象名在上传前做一次 unicodedata.normalize;服务端用 mimetypes.guess_type 识别文件后缀再做一次覆盖,既不能盲信用户输入,也不能盲信本地文件后缀。文件扩展名改了但文件内容没变的场景,这份校验只能兜底,安全的验证是读取文件头部的 magic number。
5. 参数与边界:把存储桶和数据库调成能交付的状态
5.1 存储桶权限的几种配置怎么选
实验四的存储桶权限配置只有三个主要选项:私有、公共读、公共读写。很多人只知道私有和公共读,忽略了公共读和公共写同时开是最危险的状态。做实验时选私有,再结合签名 URL;做内部演示时可以临时公共读,但存储桶里不允许存在敏感文件。生产上的标准做法是 Bucket 私有 + RAM 子账号 + STS 临时凭证,STS 这部分实验不要求,但报告里能写出它和 AccessKey 的区别,属于加分项。
权限模型直接决定代码怎么写。如果把 Bucket 设成公共读,第 3.3 的签名 URL 逻辑就没意义,反而给自己增加复杂度;如果坚持私有,就必须在每个读取接口串联 gen_access_url。我见过有人一边用公共读,一边在代码里做签名,最后发现签名 URL 在公共读桶上一样能访问,但安全模型已经糊了。选定一种模型,全流程保持一致,比混搭更接近生产习惯。
5.2 数据库连接池和批量写入的实验参数
实验里每次上传都新建一个 pymysql 连接,数据量小还看不出问题,一旦用循环脚本批量灌入 1000 张图片,就会报 Too many connections。常见做法是引入连接池,比如用 DBUtils 的 PooledDB。连接池参数不是越大越好,maxconnections 设为 5 到 10,配合 preping=True,能保证从池里拿到的连接是可用的。
批量写入时,INSERT 一次一行是性能瓶颈。把多条记录拼成一条多 VALUES 语句,或者使用 executemany,速度能差出几十倍。实验四如果只是验证流程,不需要优化到这一步,但报告里如果提到批量场景,值得把这两个参数写出来:pool 的 maxconnections、batch 的 executemany 容量,我一般 500 条一批。
5.3 生命周期规则:给冷数据降成本的存储桶配置
实验报告写到生命周期规则属于进阶内容,但它和“云存储”三个字关系最近。OSS 的存储类型分标准、低频、归档、冷归档,价格递减但访问成本递增。生命周期规则做的事情就是“超过 XX 天后自动转归档/删除指定前缀对象”。实验里可以建一个 backup/ 前缀,写一条规则把它做成 7 天后转低频、30 天后删除,用来模拟真实系统的冷数据沉降。
配置生命周期规则不要全局一把梭。如果 Bucket 里还有正在被访问的媒体,全局沉降会把热数据也拖进归档,导致前端拉取时需要先解冻,响应时间变成分钟级。正确边界是:只针对明确的前缀或标签做沉降,规则的“前缀”输入框不是摆设。归档文件不支持秒级取回,这条会在实验演示时让人措手不及,最好单独测试一次再写进报告。
6. 一致性自查与进阶:把实验报告变成可交付的习惯
实验收尾别急着提交报告,先跑三种自查。第一种是用第 4.3 的孤儿对象对账脚本,把 Bucket 里的对象和库里的 object_name 做差集;第二种是数量对账,统计对象总数和数据库记录数是否一致;第三种是哈希抽查,随机抽几条数据库记录,从 OSS HEAD 拿到 ETag 或重新下载后计算 SHA256,核对一致。实验数据少时直接全量比对,数据多时按比例抽样,十天半个月跑一次成了日常机制,比出了事故再补救强。
进阶我更推荐先做 OSS 版本控制。版本控制开启后,上传覆盖会保留历史版本,这个开关相当于后悔药,能防手滑覆盖;开启前先在实验桶里验证,确认旧版本对象可以通过控制台回滚再上线。CDN 加速可以作为下一个进阶动作,但这里有个坑:CDN 会缓存签名 URL,如果 URL 带过期参数,缓存层级会让过期问题更隐蔽。我的教训是:先在任何 CDN 缓存配置里关掉对带签名参数 URL 的缓存,再谈加速。
我自己的习惯是,每次实验结尾都会跑一遍第 4.3 的对账脚本,看看库里有没有孤儿对象;中途改过权限模型时,会把所有调用签名 URL 的地方重新过一遍。这个习惯让我在把报告里的方案做成线上系统后避开过几次事故。实验四练的不是传文件这个动作,而是双写场景下的状态一致意识。希望帮到你。
本文还有配套的精品资源,点击获取