基于脚手架微服务的视频点播系统-服务端开发部分[补充]文件子服务问题修正
服务端代码已经完全编写完毕,源码连接如上。上一篇文章有许多需要修正的地方,我们先来对这些问题进行修正再继续进行服务端代码的讲解:
一.文件子服务问题修正
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之后进行批量删除。