【后台开发拾遗】数据访问、缓存与更新

时间:2023-01-12 23:43:46

一个App或者网站,其数据内容是需要不断地更新的,为方便日常运营,我们建立了一个内部使用的运营管理系统。通过运营管理系统,可以配置一系列的运营数据,并写入到DB中。而后台服务器则读取这些数据,做一系列处理之后传输给客户端做展示。

由于后台服务端对数据的读取是十分频繁的,因此每次都从数据库读取是不切实际的,因此需要将数据缓存在本地,并定时更新缓存。

本文将介绍一种数据缓存、更新的方案,以供学习、参考。

1. 数据库操作封装

对于C++,mysql为我们提供了最原始的接口(文件mysql.h),我们的框架对原始接口进行了更好地封装,保证SQL注入。

#include "mysql.h"

/**
* @brief Mysql数据库操作类
*
* 非线程安全,通常一个线程一个TC_Mysql对象;
*
* 对于insert/update可以有更好的函数封装,保证SQL注入;
*
* TC_Mysql::DB_INT表示组装sql语句时,不加””和转义;
*
* TC_Mysql::DB_STR表示组装sql语句时,加””并转义;
*/

class TC_Mysql
{
public:
/**
* @brief 构造函数
*/
TC_Mysql();

/**
* @brief 构造函数.
*
* @param sHost 主机IP
* @param sUser 用户
* @param sPasswd 密码
* @param sDatebase 数据库
* @param port 端口
* @param iUnixSocket socket
* @param iFlag 客户端标识
*/

TC_Mysql(const string& sHost, const string& sUser = "", const string& sPasswd = "", const string& sDatabase = "", const string &sCharSet = "", int port = 0, int iFlag = 0);

/**
* @brief 构造函数.
* @param tcDBConf 数据库配置
*/

TC_Mysql(const TC_DBConf& tcDBConf);

/**
* @brief 析构函数.
*/

~TC_Mysql();

/**
* @brief 初始化.
*
* @param sHost 主机IP
* @param sUser 用户
* @param sPasswd 密码
* @param sDatebase 数据库
* @param port 端口
* @param iUnixSocket socket
* @param iFlag 客户端标识
* @return
*/

void init(const string& sHost, const string& sUser = "", const string& sPasswd = "", const string& sDatabase = "", const string &sCharSet = "", int port = 0, int iFlag = 0);

/**
* @brief 初始化.
*
* @param tcDBConf 数据库配置
*/

void init(const TC_DBConf& tcDBConf);

/**
* @brief 连接数据库.
*
* @throws TC_Mysql_Exception
* @return
*/

void connect();

/**
* @brief 断开数据库连接.
* @return
*/

void disconnect();

/**
* @brief 直接获取数据库指针.
*
* @return MYSQL* 数据库指针
*/

MYSQL *getMysql();

/**
* @brief 字符转义.
*
* @param sFrom 源字符串
* @param sTo 输出字符串
* @return 输出字符串
*/

string escapeString(const string& sFrom);

/**
* @brief 更新或者插入数据.
*
* @param sSql sql语句
* @throws TC_Mysql_Exception
* @return
*/

void execute(const string& sSql);

/**
* @brief mysql的一条记录
*/

class MysqlRecord
{
public:
/**
* @brief 构造函数.
*
* @param record
*/
MysqlRecord(const map<string, string> &record);

/**
* @brief 获取数据,s一般是指数据表的某个字段名
* @param s 要获取的字段
* @return 符合查询条件的记录的s字段名
*/

const string& operator[](const string &s);
protected:
const map<string, string> &_record;
};

/**
* @brief 查询出来的mysql数据
*/

class MysqlData
{
public:
/**
* @brief 所有数据.
*
* @return vector<map<string,string>>&
*/
vector<map<string, string> >& data();

/**
* 数据的记录条数
*
* @return size_t
*/

size_t size();

/**
* @brief 获取某一条记录.
*
* @param i 要获取第几条记录
* @return MysqlRecord类型的数据,可以根据字段获取相关信息,
*/

MysqlRecord operator[](size_t i);

protected:
vector<map<string, string> > _data;
};

/**
* @brief Query Record.
*
* @param sSql sql语句
* @throws TC_Mysql_Exception
* @return MysqlData类型的数据,可以根据字段获取相关信息
*/

MysqlData queryRecord(const string& sSql);

/**
* @brief 定义字段类型,
* DB_INT:数字类型
* DB_STR:字符串类型
*/

enum FT
{
DB_INT,
DB_STR,
};

/**
* 数据记录
*/

typedef map<string, pair<FT, string> > RECORD_DATA;

/**
* @brief 更新记录.
*
* @param sTableName 表名
* @param mpColumns 列名/值对
* @param sCondition where子语句,例如:where A = B
* @throws TC_Mysql_Exception
* @return size_t 影响的行数
*/

size_t updateRecord(const string &sTableName, const map<string, pair<FT, string> > &mpColumns, const string &sCondition);

/**
* @brief 插入记录.
*
* @param sTableName 表名
* @param mpColumns 列名/值对
* @throws TC_Mysql_Exception
* @return size_t 影响的行数
*/

size_t insertRecord(const string &sTableName, const map<string, pair<FT, string> > &mpColumns);

/**
* @brief 替换记录.
*
* @param sTableName 表名
* @param mpColumns 列名/值对
* @throws TC_Mysql_Exception
* @return size_t 影响的行数
*/

size_t replaceRecord(const string &sTableName, const map<string, pair<FT, string> > &mpColumns);

/**
* @brief 删除记录.
*
* @param sTableName 表名
* @param sCondition where子语句,例如:where A = B
* @throws TC_Mysql_Exception
* @return size_t 影响的行数
*/

size_t deleteRecord(const string &sTableName, const string &sCondition = "");

/**
* @brief 获取Table查询结果的数目.
*
* @param sTableName 用于查询的表名
* @param sCondition where子语句,例如:where A = B
* @throws TC_Mysql_Exception
* @return size_t 查询的记录数目
*/

size_t getRecordCount(const string& sTableName, const string &sCondition = "");

/**
* @brief 获取Sql返回结果集的个数.
*
* @param sCondition where子语句,例如:where A = B
* @throws TC_Mysql_Exception
* @return 查询的记录数目
*/

size_t getSqlCount(const string &sCondition = "");

/**
* @brief 存在记录.
*
* @param sql sql语句
* @throws TC_Mysql_Exception
* @return 操作是否成功
*/

bool existRecord(const string& sql);

/**
* @brief 获取字段最大值.
*
* @param sTableName 用于查询的表名
* @param sFieldName 用于查询的字段
* @param sCondition where子语句,例如:where A = B
* @throws TC_Mysql_Exception
* @return 查询的记录数目
*/

int getMaxValue(const string& sTableName, const string& sFieldName, const string &sCondition = "");

/**
* @brief 获取auto_increment最后插入得ID.
*
* @return ID值
*/

long lastInsertID();

/**
* @brief 构造Insert-SQL语句.
*
* @param sTableName 表名
* @param mpColumns 列名/值对
* @return string insert-SQL语句
*/

string buildInsertSQL(const string &sTableName, const map<string, pair<FT, string> > &mpColumns);

/**
* @brief 构造Replace-SQL语句.
*
* @param sTableName 表名
* @param mpColumns 列名/值对
* @return string insert-SQL语句
*/

string buildReplaceSQL(const string &sTableName, const map<string, pair<FT, string> > &mpColumns);

/**
* @brief 构造Update-SQL语句.
*
* @param sTableName 表名
* @param mpColumns 列名/值对
* @param sCondition where子语句
* @return string Update-SQL语句
*/

string buildUpdateSQL(const string &sTableName,const map<string, pair<FT, string> > &mpColumns, const string &sCondition);

/**
* @brief 获取最后执行的SQL语句.
*
* @return SQL语句
*/

string getLastSQL() { return _sLastSql; }

/**
* @brief 获取查询影响数
* @return int
*/

size_t getAffectedRows();
protected:
/**
* @brief copy contructor,只申明,不定义,保证不被使用
*/
TC_Mysql(const TC_Mysql &tcMysql);

/**
*
* @brief 只申明,不定义,保证不被使用
*/

TC_Mysql &operator=(const TC_Mysql &tcMysql);


private:

/**
* 数据库指针
*/
MYSQL *_pstMql;

/**
* 数据库配置
*/

TC_DBConf _dbConf;

/**
* 是否已经连接
*/

bool _bConnected;

/**
* 最后执行的sql
*/

string _sLastSql;

};

经过上面一层的封装,我们的数据库操作接口已经比较友好了,但是我们还可以做进一步的封装:提供一个接口,在得到查询结果MysqlData之后,将其解析成我们想要的C++对象。下面我们对TC_Mysql 做进一步封装,通过MysqlAccessor 来实现对mysql的真正访问,在调用selectVector时,传入一个CmsMysqlRecordparser解析器,即可灵活地按照我们的需求将数据库记录转换为C++对象。

//数据库model解析器接口,将record解析为c++对象
template<typename ItemType>
class CmsMysqlRecordparser{
public:
CmsMysqlRecordparser(){}
virtual ~CmsMysqlRecordparser(){}
virtual void parse(CmsMysqlRecord & record ,ItemType &item ) =0 ;
};

//mysql访问封装
class MysqlAccessor : public TC_ThreadLock
{
public:
MysqlAccessor(){};
~MysqlAccessor(){};

public:
bool Initialize(const string sHost, const string sUser, const string spwd, const string sDb, const string sChaSet, int16_t wPort);
bool Query(string sQuery, TC_Mysql::MysqlData &rdData);
bool Execute(string sExecute);

//查询数据库,获取批量数据,使用parser解析成C++对象
template<typename ItemType>
bool selectVector( const string& sql, std::vector<ItemType> & result,CmsMysqlRecordparser<ItemType>& parser )
{
bool ret=false;
TC_Mysql::MysqlData recordSet;
__TRY__;
if(this->Query(sql, recordSet))
{
if (recordSet.size() > 0)
{
for (size_t i = 0; i < recordSet.size(); i++)
{
CmsMysqlRecord record(recordSet.data()[i]);
ItemType item;
parser.parse(record,item);
result.push_back(item);
}
}
//只要无异常,就算数据为空,也算成功
ret= true;
}
__CATCH_EXCEPTION_WITH__(sql);

return ret;
}

private:
TC_Mysql _dbMysql;
};

2. 数据缓存

上面我们对数据库访问进行了一系列的封装,下面我们来看看数据是如何从数据库取出来并缓存在本地的。

现假设我们数据的C++类型定义如下:

struct BlackChannelInfo
{
long id;
string channelId;
};

我们提供了以下的类来存储以上的数据对象集合blackChannelList,并提供对外的访问接口getBlackChannelInfo()。

class BlackChannelInfoData
{
public:
BlackChannelInfoData()
{ }

virtual ~BlackChannelInfoData() {}
public:
bool Update();
bool getBlackChannelInfo(const string& channelId, BlackChannelInfo& info);

private:
map<string, BlackChannelInfo> blackChannelList; // key是channelId
};

其中Update方法如下:

bool BlackChannelInfoData::Update()
{
string sql = string("select * from channel_black;");

vector<BlackChannelInfo> tmpVecData;
// MysqlAccessPtr指向一个MysqlAccessor对象
// BlackChannelInfoParser继承自
// CmsMysqlRecordparser<BlackChannelInfo>
// 并重写了parser函数
BlackChannelInfoParser parser;
if (MysqlAccessPtr->selectVector(sql, tmpVecData, parser))
{
for (size_t i = 0; i < tmpVecData.size(); ++i)
{
blackChannelList.insert(
make_pair(tmpVecData[i].channelId,
tmpVecData[i]));
}
return true;
}
return false;
}

以上的Update操作属于所有数据缓存对象的共性,我们可以抽象出一个父类DataBase,该类包含虚成员函数Update(),这为我们后面数据更新做准备——数据更新线程只需要调用DataBase的Update()即可完成对数据的刷新,而不需要关心DataBase的子类BlackChannelInfoData。

同时,我们发现只要有一个BlackChannelInfo,就需要一个BlackChannelInfoParser 来做解析工作,我们可以把BlackChannelInfoParser 整合到类DataBase中——让DataBase继承CmsMysqlRecordparser< T >。

于是就有了如下继承关系:

BlackChannelInfoData继承自DataBase< BlackChannelInfo >;
DataBase< BlackChannelInfo >继承自CmsMysqlRecordparser< BlackChannelInfo >。

在实现BlackChannelInfoData时,重写函数parse() 和函数Update()。

并将Update()中的语句:

    BlackChannelInfoParser parser;
if (MysqlAccessPtr->selectVector(sql, tmpVecData, parser))

替换为:

    if (MysqlAccessPtr->selectVector(sql, tmpVecData, *this))

如此便不用额外地去定义类BlackChannelInfoParser 了。

template<typename T>
class DataBase:public CmsMysqlRecordparser<T>, public TC_HandleBase {
public:
DataBase() {
_sTableName = "no name";
}
virtual ~DataBase() {}
//所依赖的数据库表名
string GetTableName() {
return _sTableName;
}
public:
virtual bool needUpdate(string const& tableName) {
//表名为空则全部刷新,否则只刷新指定表
return tableName=="" || GetTableName()==tableName;
}
virtual bool Update();
virtual void Clear();
public:
string _sTableName;//该数据类型的名称,用于打日志
};

数据缓存各个类的关系图如下:

【后台开发拾遗】数据访问、缓存与更新

有了以上良好的封装,在写业务代码的时候,我们只需要做两件事:
1. 创建一个用于缓存数据的类,该类继承自DataBase;
2. 实现Update() 和 parse()函数。

3. 数据更新

数据定时更新可以使用一个线程来做:

class NeedToUpdate {
public:
NeedToUpdate();
virtual ~NeedToUpdate();
void setNeedUpate(bool need = true);
void doUpdateNow(time_t now);
virtual std::string getCacheName() = 0;
virtual void Update() = 0;
private:
volatile time_t m_update_time; //上次更新时间
volatile bool m_need_update; //是否需要更新
};

class DataWrapUpdater: public TC_Singleton<DataWrapUpdater>, protected TC_Thread {
public:
DataWrapUpdater();
virtual ~DataWrapUpdater();
void add(NeedToUpdate * update);
protected:
void run();
private:
taf::TC_ThreadRecLock m_lock;
std::vector<NeedToUpdate *> m_updates;
time_t m_update_interval; //更新间隔 s
useconds_t m_run_interval; //运行间隔 us
};

这里引入了一个新的类NeedToUpdate ,因为有多个数据缓存对象需要更新,每个对象的更新频率也可能不同,同时除了定时更新,还有主动更新,因此每个数据缓存对象对应有一个NeedToUpdate,用来管理对象的更新时机。

线程DataWrapUpdater通过add() 函数将各个NeedToUpdate 的指针添加注册进来,然后定时运行,逐个检查是否满足更新条件,如果满足则调用doUpdateNow()函数,该函数调用纯虚函数Update() 。

定时的被动更新条件:update->m_update_time + m_update_interval < now

人为的主动更新条件:update->m_need_update
(比如向服务器发一条命令,服务器收到命令之后,调用setNeedUpate设置标志m_need_update

以前面介绍的数据缓存类型BlackChannelInfoData为例,由于我们的数据在执行更新的时候,可能同时还被服务使用着,如果加锁则代价比较大,我们选择用一个临时对象来存放更新后的新数据,并在更新完成之后,与当前旧数据交换(只需交换指针)。为完成这个操作,我们使用类DataWrap< T >对数据缓存类型T做进一层的包裹,同时DataWrap< T >继承了上面的NeedToUpdate ,从而拥有了管理对象的更新时机的能力。

template<class T>
class DataWrap: public TC_ThreadLock, public NeedToUpdate {
public:
DataWrap()
: m_last(NULL),
m_curr(new T) {
DataWrapUpdater::getInstance()->add(this);
}
~DataWrap() {
}

public:
bool Update(string const& tableName);
// 设置主动更新标志
TC_AutoPtr<T> Get();

virtual void Update();
private:
taf::TC_AutoPtr<T> m_last;
taf::TC_AutoPtr<T> m_curr;
};

template<class T>
bool DataWrap<T>::Update(string const& tableName) {
if (m_curr->needUpdate(tableName)) {
setNeedUpate();
}
return true;
}

template<class T>
void DataWrap<T>::Update() {
TC_ThreadLock::TryLock lock(*this);
if (lock.acquired()) {
TC_AutoPtr<T> ptr = new T;
bool succ = ptr->Update();
if (succ) {
m_last = m_curr;
m_curr = ptr;
}
}
}

template<class T>
taf::TC_AutoPtr<T> DataWrap<T>:Get() {
return m_curr;
}

完成了以上工作,我们只需要一行代码,便可以完成数据的定时自动更新了:

DataWrap<BlackChannelInfoData> blackChannelInfoDataWrap;

访问数据:

blackChannelInfoDataWrap.Get()->getBlackChannelInfo(channelId, info);

主动更新:

blackChannelInfoDataWrap.Update(tableName);

【后台开发拾遗】数据访问、缓存与更新