一、为什么要批量读写
在业务开发中,我们经常需要更新玩家多处数据,如果每条数据都单独更新的话,效率非常低,分别存储的话还需要处理异常回滚,非常麻烦。
二、数据存储类型
为此我建议在数据存储时尽量使用string存储也就是key-value形式,这一可以利用Redis的Mget和Mset进行批量读写操作,来提高性能。当然例如排名和消息队列这些不适用string的也不用勉强。
三、Module层父类
该父类封装了玩家和逻辑两个数据库的Mget和Mset方法,用于在夸模块时string的批量数据操作。
class Modules{
protected $_app;
protected $_redis;
/**
* 创建Redis的连接
*/
public function __construct(){
global $app;
$this->_app = $app;
$this->_redis = new MyRedis();
}
/**
* 公共数据库批量读
*
* @param $arr
*
* @return array
*/
public function CommonMget($arr){
return $this->_redis->getCommonRedis()->mget($arr);
}
/**
* 公共数据库批量写
*
* @param $arr
*
* @return bool
*/
public function CommonMset($arr){
if(empty($arr)){
return null;
}
foreach($arr as $index => $val){
$arr[$index] = setRedis($val);
}
return $this->_redis->getCommonRedis()->mset($arr);
}
/**
* 玩家数据库批量读
*
* @param $puid
* @param $arr
*
* @return array
*/
public function Mget($puid, $arr){
if(empty($arr)){
return true;
}
return $this->_redis->getUserRedis($puid)->mget($arr);
}
/**
* 批量写
*
* @param $puid
* @param $arr
*
* @return bool
*/
public function Mset($puid, $arr){
if(empty($arr)){
return true;
}
foreach($arr as $index => $val){
$arr[$index] = setRedis($val);
}
return $this->_redis->getUserRedis($puid)->mset($arr);
}
}
四、获取数据
当Domain层需要一条数据时,通常调用Module层的查询方法即可。
$hero_pack = $this->hero_module->getHeroPackByPuid($puid);
而当需要查询多个模块的数据时,如果英雄、装备、道具背包和角色信息时,使用常规方法就会是这样
$hero_pack = $hero_module->getHeroPackByPuid($puid);
$equip_pack = $equip_module->getEquipPackByPuid($puid);
$item_pack = $item_module->getItemPackByPuid($puid);
$role_info = $role_module->getRoleInfoByPuid($puid);
而如果使用Mget则只需一次查询即可
//以Role模块的Domain层为例
$keys = array(
Redis_Pfx_RoleInfo . $puid,
Redis_Pfx_HeroPack . $puid,
Redis_Pfx_EquipPack . $puid,
Redis_Pfx_ItemPack . $puid,
);
//Role模块调用自己Module层里父类的Mget方法
$data = $this->role_module->Mget($puid, $keys);
//将批量获取的数据分配到变量,千万不要直接拿$data[0]去用,维护起来糟糕
$role_info_data = $data[0];
$hero_pack_data = $data[1];
$equip_pack_data = $data[2];
$item_pack_data = $data[3];
//调用数据所属模块的数据格式化方法
$role_info = $role_class->getRoleInfoByData($role_info_data);//角色信息
$hero_pack = $hero_class->getHeroPackByData($hero_pack_data);//英雄背包
$equip_pack = $equip_class->getEquipPackByData($equip_pack_data);//装备背包
$item_pack = $item_class->getItemPackByData($item_pack_data);//道具背包
四、批量存储
批量存储与批量读也是类似的,但需要多一步数据转换的操作,我会将数组转换为Json再存入数据库,为什么选择Json而不是序列化,后面细讲。批量存储还有一个好处就是,在业务操作时中断对数据库中的数据都没有任何影响,不需要处理太多的数据回滚,只有所有业务都跑通之后统一进行数据库修改。类似于事务(也可以直接用Multi开发事务),当然跨库操作需要手动回滚数据。这样不但写库效率高,而且数据操作的安全性也得到保障。
//以Role模块的Domain层为例
$sets = array(
Redis_Pfx_RoleInfo . $puid => $role_info,
Redis_Pfx_HeroPack . $puid => $hero_pack,
Redis_Pfx_EquipPack . $puid => $equip_pack,
Redis_Pfx_ItemPack . $puid => $item_pack,
);
//Role模块调用自己Module层里父类的Mset方法
$result = $this->role_module->Mset($puid, $sets);
五、函数支持
处于对批量查询的支持,我们应该在Domain层提供一个格式化数据的函数,它的主要用处有两点:
1. 初始化数据,例如获取玩家的英雄背包,新注册的玩家该数据是空的,而使用Redis的话不会像Mysql哪有有表结构,所以需要返回一个数据结构如:
/**
* 初始化英雄背包(包含英雄和契约石-也就是碎片)
*
* @param $data
* @param null $config
*
* @return array
*/
public function getHeroPackByData($data){
$hero_pack = dataToArray($data);//自定义函数:如果数据是Json则转换为数组
if(empty($hero_pack['Hero'])){//如果英雄列表不存在
$hero_pack['Hero'] = array();//初始化
}
if(empty($hero_pack['Contract'])){//如果契约石不存在
$hero_pack['Contract'] = array();//初始化
}
return $hero_pack;
}
/**
* 获取英雄背包
*
* @param $puid
*
* @return array
*/
public function getHeroPackByPuid($puid){
$hero_data = $this->hero_module->getHeroPackByPuid($puid);
return $this->getHeroPackByData($hero_data);
}
2. 数据的统一业务处理
例如,每次获取角色信息时,我们应该根据上次体力的恢复时间和恢复间隔为玩家恢复体力,更新玩家的活跃时间,只需要写在初始化数据函数中即可。
无论是单次查询还是批量查询都用格式化函数处理一下原始数据再使用,形成这样的约束。