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

版权声明:本文为博主原创文章,转载请注明出处http://blog.csdn.net/jiange_zh https://blog.csdn.net/jiange_zh/article/details/77972277

一个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);

数据更新

猜你喜欢

转载自blog.csdn.net/jiange_zh/article/details/77972277
今日推荐