基于脚手架微服务的视频点播系统-服务端开发部分(补充)文件子服务问题修正

基于脚手架微服务的视频点播系统-服务端开发部分[补充]文件子服务问题修正

项目源码地址

服务端代码已经完全编写完毕,源码连接如上。上一篇文章有许多需要修正的地方,我们先来对这些问题进行修正再继续进行服务端代码的讲解:

一.文件子服务问题修正

1.1文件元信息表文件mime字段长度大小修正

将其类型从VARCHAR(16)修正为64即可。

1.2文件子服务的数据操作类部分没有进行异常捕获

将其加上一层异常捕获即可:

cpp 复制代码
#include "session.h"
#include <lime_scaffold/limelog.h>
#include "../common/error.h"

namespace limevp_data{
    const std::string SessionData::_cache_key_prefix = "vp_session_";
    const int SessionData::_cache_expire_seconds = 3600; //1小时
    const std::string SessionData::_session_id = "sessionid";
    const std::string SessionData::_user_id = "userid";//与设计的RESTful API一致
    SessionData::SessionData(odb::transaction& mtx,
            const std::shared_ptr<sw::redis::Redis>& redis)
            : _db(mtx.database()),_redis(redis)
    {}
    SessionData::SessionData(odb::transaction& mtx,const std::shared_ptr<sw::redis::Redis>& redis,const limevp_sync::CacheSync::Ptr& rmcahe)
        : _db(mtx.database()),_redis(redis),_rmcahe(rmcahe)
    {}
    //向数据库新增会话
    void SessionData::addSession2Db(Session& session) 
    {
        try{
            //此时因为是数据库新增,所以不需要添加到缓存
            addSessionToDb(session);
        }catch(const std::exception& e)
        {
            throw;
        }
    }
    //更新数据库会话,并删除会话缓存,发布删除消息
    void SessionData::updateSession2Db(Session::Ptr& session) 
    {
        try{
            updateSessionToDb(session);
            //删除缓存
            _rmcahe->sync(getCacheKey(session->get_session_id()));
        }catch(const std::exception& e)
        {
            throw;
        }
    }
    //通过会话ID删除数据库会话,并删除会话缓存,发布删除消息
    void SessionData::delSessionFromDbBySessionId(const std::string& sessionId) 
    {
        try{
            delSessionBySidFromDb(sessionId);
            //删除缓存
            _rmcahe->sync(getCacheKey(sessionId));
        }catch(const std::exception& e)
        {
            throw;
        }
    }
    //通过用户ID删除数据库会话,并删除会话缓存,发布删除消息
    void SessionData::delSessionFromDbByUserId(const std::string& userId) 
    {
        try{
            //先通过用户ID获取所有会话ID
            auto sessions = getSessionsByUidFromDb(userId);
            if(sessions.empty())
            {
                return;
            }
            //删除数据库会话
            delSessionByUidFromDb(userId);
            //删除缓存
            for(auto& session : sessions)
            {
                _rmcahe->sync(getCacheKey(session->get_session_id()));
                DBG("通过用户ID删除会话,删除缓存key: {}", getCacheKey(session->get_session_id()));
            }
        }catch(const std::exception& e)
        {
            throw;
        }
    }
    //获取会话信息(优先从缓存获取,缓存未命中则从数据库获取,并添加缓存)
    Session::Ptr SessionData::getSession(const std::string& sessionId) 
    {
        try{
            auto session = getSessionFromCache(sessionId);
            if(session)
            {
                return session;
            }
            session = getSessionBySidFromDb(sessionId);
            if(session)
            {
                addSessionToCache(session);
                return session;
            }
            return nullptr;
        }catch(const std::exception& e)
        {
            throw;
        }
        return nullptr;//防止编译器告警,实际上不会执行到这里
    }

    //私有接口
    std::string SessionData::getCacheKey(const std::string& sessionId)
    {
        return _cache_key_prefix + sessionId;
    }
    //向数据库添加会话信息
    void SessionData::addSessionToDb(Session& session)
    {
        try{
            _db.persist(session);
        }catch(const odb::exception& e)
        {
            ERR("添加会话到数据库失败: {}", e.what());
            throw limevp_error::ErrorException(limevp_error::ErrorCode::DATABASE_OP_FAILED);
        }
    }
    //从数据库获取会话信息-通过会话ID
    Session::Ptr SessionData::getSessionBySidFromDb(const std::string& sessionId)
    {
        try{
            Session::Ptr result(_db.query_one<Session>(odb::query<Session>::session_id == sessionId));
            return result;
        }catch(const odb::exception& e)
        {
            ERR("从数据库获取会话信息失败: {}", e.what());
            throw limevp_error::ErrorException(limevp_error::ErrorCode::DATABASE_OP_FAILED);
        }
    }
    //从数据库获取会话信息-通过用户ID
    std::vector<Session::Ptr> SessionData::getSessionsByUidFromDb(const std::string& userId)
    {
        try{
            //因为一个用户可能对应多个会话,比如不同客户端登录同一个账号时,就会有多个sessionid对应一个userid
            typedef odb::query<SessionPtr> Query;
            typedef odb::result<SessionPtr> Result;
            Result r = _db.query<SessionPtr>(Query::user_id == userId);
            std::vector<Session::Ptr> result;
            for (auto& item : r)
            {
                result.push_back(item.session);
            }
            return result;
        }catch(const odb::exception& e)
        {
            ERR("从数据库获取会话信息失败: {}", e.what());
            throw limevp_error::ErrorException(limevp_error::ErrorCode::DATABASE_OP_FAILED);
        }
    }
    //修改数据库会话信息-内部直接进行查找,方便外部调用
    void SessionData::updateSessionToDb(Session::Ptr& session)
    {
        try{
            auto old_session = getSessionBySidFromDb(session->get_session_id());
            //如果为空则插入
            if (!old_session)
            {
                addSessionToDb(*session);
                return;
            }
            //如果不为空则进行更新
            old_session->set_user_id(session->get_user_id());
            _db.update(old_session.get());
        }catch(const odb::exception& e)
        {
            ERR("更新会话信息到数据库失败: {}", e.what());
            throw limevp_error::ErrorException(limevp_error::ErrorCode::DATABASE_OP_FAILED);
        }
    }
    //通过会话ID删除数据库会话
    void SessionData::delSessionBySidFromDb(const std::string& sessionId)
    {
        try{
            _db.erase_query<Session>(odb::query<Session>::session_id == sessionId);
        }catch(const odb::exception& e)
        {
            ERR("删除会话信息到数据库失败: {}", e.what());
            throw limevp_error::ErrorException(limevp_error::ErrorCode::DATABASE_OP_FAILED);
        }
    }
    //通过用户ID删除数据库会话
    void SessionData::delSessionByUidFromDb(const std::string& userId)
    {
        try{
            _db.erase_query<Session>(odb::query<Session>::user_id == userId);
        }catch(const odb::exception& e)
        {
            ERR("删除会话信息到数据库失败: {}", e.what());
            throw limevp_error::ErrorException(limevp_error::ErrorCode::DATABASE_OP_FAILED);
        }
    }
    //向redis添加会话缓存
    void SessionData::addSessionToCache(const Session::Ptr& session)
    {
        try{
            auto rtx = _redis->transaction(false,false);
            auto r = rtx.redis();
            std::string key = getCacheKey(session->get_session_id());
            std::unordered_map<std::string, std::string> values;
            values[_session_id] = session->get_session_id();
            values[_user_id] = session->get_user_id().get();
            r.hmset(key, values.begin(), values.end());
            r.expire(key, std::chrono::seconds(_cache_expire_seconds + limeutil::LimeRandom::number(0,_cache_expire_seconds)));
        }catch(const sw::redis::Error& e)
        {
            ERR("向redis添加会话缓存失败: {}", e.what());
            throw limevp_error::ErrorException(limevp_error::ErrorCode::REDIS_OP_FAILED);
        }
    }
    //从redis获取会话缓存
    Session::Ptr SessionData::getSessionFromCache(const std::string& sessionId)
    {
        try{
            auto rtx = _redis->transaction(false,false);
            auto r = rtx.redis();
            std::string key = getCacheKey(sessionId);
            auto value = r.hget(key,sessionId);
            if(!value)
            {
                return nullptr;
            }
            Session::Ptr session = std::make_shared<Session>();
            session->set_session_id(sessionId);
            session->set_user_id(*value);
            return session;
        }catch(const sw::redis::Error& e)
        {
            ERR("从redis获取会话缓存失败: {}", e.what());
            throw limevp_error::ErrorException(limevp_error::ErrorCode::REDIS_OP_FAILED);
        }
    }
    //通过会话ID删除缓存会话
    void SessionData::delSessionFromCache(const std::string& sessionId)
    {
        try{
            auto rtx = _redis->transaction(false,false);
            auto r = rtx.redis();
            std::string key = getCacheKey(sessionId);
            r.del(key);
        }catch(const sw::redis::Error& e)
        {
            ERR("删除会话缓存失败: {}", e.what());
            throw limevp_error::ErrorException(limevp_error::ErrorCode::REDIS_OP_FAILED);
        }
    }
} // namespace limevp_data
cpp 复制代码
#include "file.h"
#include <lime_scaffold/limelog.h>
#include "../common/error.h"

namespace limevp_data{
    FileData::FileData(odb::transaction& mtx)
        : _db(mtx.database())
    {}
    //新增文件信息
    void FileData::addFile2Db(File& file)
    {
        try{
            _db.persist(file);
        }catch(const odb::exception& e){
            ERR("向数据库新增文件信息失败: {}",e.what());
            throw limevp_error::ErrorException(limevp_error::ErrorCode::DATABASE_OP_FAILED);
        }
    }
    //获取文件信息
    File::Ptr FileData::getFileFromDb(const std::string& fileId)
    {
        try{
            File::Ptr result(_db.query_one<File>(odb::query<File>::file_id == fileId));
            return result;
        }catch(const odb::exception& e){
            ERR("从数据库获取文件信息失败: {}",e.what());
            throw limevp_error::ErrorException(limevp_error::ErrorCode::DATABASE_OP_FAILED);
        }
    }
    //修改文件信息-必须是getFileFromDb获取到的对象
    void FileData::updateFile2Db(File& file)
    {
        try{
            //先查询,再修改,如果没有此对象则返回
            auto result = getFileFromDb(file.get_file_id());
            if(!result)
            {
                return;
            }
            //更新对应数据
            _db.update(file);
        }catch(const odb::exception& e){
            ERR("更新文件信息失败: {}",e.what());
            throw limevp_error::ErrorException(limevp_error::ErrorCode::DATABASE_OP_FAILED);
        }
    }
    //删除文件信息
    void FileData::delFromDb(const std::string& fileId)
    {
        try{
            _db.erase_query<File>(odb::query<File>::file_id == fileId);
        }catch(const odb::exception& e){
            ERR("从数据库删除文件信息失败: {}",e.what());
            throw limevp_error::ErrorException(limevp_error::ErrorCode::DATABASE_OP_FAILED);
        }
    }
}// namespace limevp_data

1.3缓存同步策略修正

之前我们不是在data文件夹下实现了rmcache类进行缓存同步删除,但是当博主实现到后面时发现这种方式太鸡肋了。比如我们视频子服务到时候就需要整两个不同类型的缓存同步指针传到svc_data中供其进行使用。所以我们修改了缓存同步的实现方式,即实现一个纯虚基类CacheSync:

cpp 复制代码
#pragma once
#include <iostream>
#include <memory>
#include <string>

namespace limevp_sync{
    class CacheSync{
        public:
            using Ptr = std::shared_ptr<CacheSync>;
            virtual void sync(const std::string& key) = 0;
    };
}

数据操作类均使用此纯虚基类的共享指针进行缓存同步,每个svc_data进行实现时实现自己的具体的缓存同步操作,比如我们的文件子服务,他是需要进行缓存同步双写删除,我们可以实现如下的svc_sync:

cpp 复制代码
#pragma once
#include "../common/sync.h"
#include "../common/error.h"
#include <lime_scaffold/limelog.h>
#include <lime_scaffold/limemq.h>
#include <lime_scaffold/limeredis.h>
#include <lime_scaffold/limeodb.h>
#include "message.pb.h"

namespace svc_file{
    class CacheDelete : public limevp_sync::CacheSync{
        public:
            using Ptr = std::shared_ptr<CacheDelete>;
            CacheDelete(const limemq::MQClient::ptr& mqclient,
                        const limemq::declare_settings& settings,
                        const std::shared_ptr<sw::redis::Redis>& redis);
            virtual void sync(const std::string& key) override;
        private:
            void callback(const char* body,size_t size);
        private:
            limemq::Publisher::ptr _publisher;
            limemq::Subscriber::ptr _subscriber;
            std::shared_ptr<sw::redis::Redis> _redis;
    };
}
cpp 复制代码
#include "svc_sync.h"

namespace svc_file{
    CacheDelete::CacheDelete(const limemq::MQClient::ptr& mqclient,
                const limemq::declare_settings& settings,
                const std::shared_ptr<sw::redis::Redis>& redis)
        : _redis(redis)
    {
        _publisher = std::make_shared<limemq::Publisher>(mqclient,settings);
        _subscriber = std::make_shared<limemq::Subscriber>(mqclient,settings);
        _subscriber->consume(std::bind(&CacheDelete::callback,this,std::placeholders::_1,std::placeholders::_2));
    }
    void CacheDelete::sync(const std::string& key){
        try{
            auto rtx = _redis->transaction(false,false);
            auto r = rtx.redis();
            r.del(key);
            CommunicationInterface::DeleteCacheMsg msg;
            msg.add_key(key);
            bool ret = _publisher->publish(msg.SerializeAsString());
            if(!ret){
                throw limevp_error::ErrorException(limevp_error::ErrorCode::MQ_OP_FAILED);
            }
        }catch(const limevp_error::ErrorException& e){
            ERR("发布缓存延迟删除消息失败!:{}", e.what());
            throw limevp_error::ErrorException(limevp_error::ErrorCode::MQ_OP_FAILED);
        }catch(const std::exception& e){
            ERR("发布缓存延迟删除消息失败!:{}",e.what());
            throw limevp_error::ErrorException(limevp_error::ErrorCode::SERRVER_INTERNAL_ERROR);
        }
    }
    void CacheDelete::callback(const char* body,size_t size)
    {
        try{
            CommunicationInterface::DeleteCacheMsg msg;
            if(!msg.ParseFromArray(body, size))
            {
                ERR("缓存同步消息key解析消息失败");
                return;
            }
            int sz = msg.key_size();
            auto rtx = _redis->transaction(false,false);
            auto r = rtx.redis();
            for(int i = 0; i < sz; ++i)
            {
                std::string key = msg.key(i);
                r.del(key);
                DBG("收到缓存同步消息,删除缓存key:{}", key);
            }
        }catch(const std::exception& e){
            ERR("缓存同步消息处理失败:{}", e.what());
            throw limevp_error::ErrorException(limevp_error::ErrorCode::SERRVER_INTERNAL_ERROR);
        }
    }
} // namespace svc_file

之后实例化一个CacheDelete的共享指针用CacheSync的基类共享指针调用,替换原来的rmcache即可实现与原来一样的效果。替换后的版本可见源码部分文件子服务的svc_data.cc

1.4删除文件时没有考虑删除m3u8文件的情况

我们在实现客户端的时候已经知道,一个视频在我们服务端存储时是一个m3u8文件对应多个ts分片文件。如果我们像原来那样删除,仅仅只能删除m3u8文件而不能删除其分片文件,所以我们需要在svc_mq中加入一个子方法用于删除分片文件:

cpp 复制代码
    void SvcDelFileMQ::remove_subfile(const std::string& path)
    {
        //判断文件后缀是否为.m3u8如果不是则直接返回
        std::string ext = std::filesystem::path(path).extension();
        if (ext != ".m3u8") { return; }
        DBG("要删除的文件是一个M3U8文件, 需要优先删除子文件: {}", path);
        //2. 将文件下载到本地,通过M3U8Info解析文件,获取子文件ID
        std::string tmp_path = "./" + limeutil::LimeRandom::code();
        bool ret = limefds::FdfsClient::downloadToFile(path, tmp_path);
        if (ret == false) {
            ERR("从FDFS下载文件失败: {}", path);
            return;
        }
        limeffmpeg::M3U8InFo m3u8_info(tmp_path);
        ret = m3u8_info.parse();
        if (ret == false) {
            ERR("解析M3U8文件失败: {}", path);
            return;
        }
        auto ts_pieces = m3u8_info.getPieces();
        //3. 删除子文件
        for (auto &piece : ts_pieces) {
            // 1. 解析获取文件ID
            std::string url = piece.second;
            std::string file_id = url.substr(url.find_last_of("=") + 1);
            DBG("删除子文件: {}", file_id);
            // 2. 通过文件ID,获取文件元信息
            auto file = _svc_data->getFileByFid(file_id);
            if (!file) {
                ERR("文件ID[{}]不存在!", file_id);
                continue;
            }
            std::string sub_path = file->get_file_path();
            // 3. 通过元信息中的文件path,删除FDFS上的文件
            limefds::FdfsClient::deleteFile(sub_path);
            // 4. 删除文件元信息
            _svc_data->delFileMeta(file_id);
        }
        //4.删除本地临时文件
        std::filesystem::remove(tmp_path);
    }

需要知道我们分片文件信息也是要存到文件元信息表中的,m3u8文件中会存储所有分片文件的文件id,所以我们便可以先将源m3u8文件下载到本地,提取所有分片文件id之后进行批量删除。

相关推荐
ITFLY837 分钟前
架构很简单:系统拆分与组合
架构
恋爱绝缘体11 小时前
2020重学C++重构你的C++知识体系
java·开发语言·c++·算法·junit
Z1Jxxx1 小时前
加密算法加密算法
开发语言·c++·算法
乌萨奇也要立志学C++2 小时前
【洛谷】递归初阶 三道经典递归算法题(汉诺塔 / 占卜 DIY/FBI 树)详解
数据结构·c++·算法
踏浪无痕2 小时前
AI 时代架构师如何有效成长?
人工智能·后端·架构
️停云️2 小时前
【滑动窗口与双指针】不定长滑动窗口
c++·算法·leetcode·剪枝·哈希
charlie1145141912 小时前
嵌入式现代C++教程: 构造函数优化:初始化列表 vs 成员赋值
开发语言·c++·笔记·学习·嵌入式·现代c++
IT=>小脑虎3 小时前
C++零基础衔接进阶知识点【详解版】
开发语言·c++·学习
anyup3 小时前
2026第一站:分享我在高德大赛现场学到的技术、产品与心得
前端·架构·harmonyos
在路上看风景3 小时前
01. C++是如何工作的
开发语言·c++