redis-storage介绍[转]

为了解决Redis在游戏项目中因数据无限增长导致的内存压力问题,本文介绍了一种将LevelDB作为Redis的持久化存储引擎的方法。该方案不仅实现了冷热数据分离,还保证了系统的稳定性和内存的有效利用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

背景

当时我们正在做一个游戏项目,游戏项目相比于web项目,更追求的是单机的性能,而我们对单个请求的处理时间有着bt级的需求(一个完整的api请求控制在10ms以内)。当时我们的数据层用的是ttserver,但他在我们之前项目中有一些比较不好处理的问题,所以我一直在寻找的替代方向,而这时redis的横空出世,给nosql世界带来了不小的震动,相比于memcache, 丰富的数据结构,给了很多人更换cache层的理由,而数据能落地,使之有成为数据库的可能。后来新浪的大面积使用,稳定性得到保障,我果断在新项目中使用了redis。

问题

redis的一些丰富结构,特别适合游戏,我们当时用的非常爽。后来游戏也顺利上线了。性能也是非常好,但运营了将近一年后,我们发现了一个致命的问题,由于我们是偏社交游戏,没有分服,所有的用户都在一个服务器上,这里面有一个矛盾了:

  1)  数据在无止境的增长    (对内存的需求越来越多)

  2)  活跃用户( dau )基本稳定了  (热数据占比较小20%以内)

 

思考:

问题的根源在于redis提供的两种持久化机制,都只是起到备份作用,所有的数据都必需在内存中:
     1)  数据落地只是备份

     2)  redis服务在重启之后,需要把备份数据一次性load回内存(数据量很大需要load时间很长)

 

思路:

用一个成熟的持久化的存储引擎来替代。落地的数据能直接对外提供服务。而只要保证把热数据留在内存中,冷数据在持久化的存储引擎中。这样就可以解决几个问题:

 

   1 ) 对内存的需求基本只是热数据的需求

   2 ) redis服务重启,不需要再load回内存,可以空重启

 

方案:

最后经过一翻寻找对比,确定了用leveldb (关于leveldb的介绍自行google), 理由:
   1) 由google开源,而且有很多成熟应用,质量可靠。

   2) leveldb性能好,特别写性能,几乎和读性能一致。

   3) 提供c的api,方便直接嵌入到redis中

 

实现 :

 

1) leveldb的嵌入

封装一个方法,以便redis服务初始化的时候,把leveldb引擎载入

void ds_init() {    if(!server.ds_open) {        return ;    }    char *err = NULL;
   server.ds_cache = leveldb_cache_create_lru(server.ds_lru_cache * 1048576);    server.ds_options = leveldb_options_create();
   server.policy = leveldb_filterpolicy_create_bloom(10);
   //leveldb_options_set_comparator(server.ds_options, cmp);    leveldb_options_set_filter_policy(server.ds_options, server.policy);    leveldb_options_set_create_if_missing(server.ds_options, server.ds_create_if_missing);    leveldb_options_set_error_if_exists(server.ds_options, server.ds_error_if_exists);    leveldb_options_set_cache(server.ds_options, server.ds_cache);    leveldb_options_set_info_log(server.ds_options, NULL);    leveldb_options_set_write_buffer_size(server.ds_options, server.ds_write_buffer_size * 1048576);    leveldb_options_set_paranoid_checks(server.ds_options, server.ds_paranoid_checks);    leveldb_options_set_max_open_files(server.ds_options, server.ds_max_open_files);    leveldb_options_set_block_size(server.ds_options, server.ds_block_size * 1024);    leveldb_options_set_block_restart_interval(server.ds_options, server.ds_block_restart_interval);    leveldb_options_set_compression(server.ds_options, leveldb_snappy_compression);
   server.ds_db = leveldb_open(server.ds_options, server.ds_path, &err);    if (err != NULL) {        fprintf(stderr, "%s:%d: %s\n", __FILE__, __LINE__, err);        leveldb_free(err);        exit(1);    }
   server.woptions = leveldb_writeoptions_create();    server.roptions = leveldb_readoptions_create();    leveldb_readoptions_set_verify_checksums(server.roptions, 0);    leveldb_readoptions_set_fill_cache(server.roptions, 1);
   leveldb_writeoptions_set_sync(server.woptions, 0); }

我们在redis.c里的initServer方法最后调用ds_init()即可。这样我们就可以在redis内部对leveldb进行操作了。

 

2) 一个简单的读取流程 (rl_get命令)

当client连上redis的时候,他的标准读取流程是:先从redis读取, 如果redis没有,则到leveldb读取。代码示例:

 

static void rl_getCommand(redisClient *c, int set) {    //从redis里取数据    robj *o;
   if ((o = lookupKeyRead(c->db, c->argv[1])) == NULL) {  //没有读取数据        ds_getCommand(c, set);  //从leveldb读取        checkRlTTL(c->db, c->argv[1]);        return;    }
   if (o->type == REDIS_STRING) {        addReplyBulk(c, o);        checkRlTTL(c->db, c->argv[1]);        return;    }
   addReply(c, shared.nullbulk);
}

 

3) 一个简单的写入流程(rl_set)

当client连上redis的时候,他的标准写入流程是:先写到leveldb中,写成功了,再写到redis中, 代码示例:

void rl_set(redisClient *c) {    if(!server.ds_open) {        addReplyError(c,"REDIS_STORAGE CLOSED");        return;    }    char *key, *value;    char *err = NULL;
   key = (char *) c->argv[1]->ptr;    value = (char *) c->argv[2]->ptr;    leveldb_put(server.ds_db, server.woptions, key, sdslen((sds) key), value, sdslen((sds) value), &err);    if (err != NULL) {   //leveldb写入失败,直接返回错误        addReplyError(c, err);        leveldb_free(err);        return;    }    //addReply(c,shared.ok);
   //存到redis    setCommand(c);    checkRlTTL(c->db, c->argv[1]); 
}

 

4)各种组合:

=======string数据操作======
rl_get key            (从redis或leveldb取值, 优先顺序:redis > leveldb)
rl_getset key         (返回同rl_get, 当leveldb有值,redis无值时,会回写到redis)
rl_mget k1 k2 k3      (取redis和leveldb的并集,优先级:redis>leveldb)
rl_mgetset k1 k2 k3   (返回同rl_mget, 当leveldb有值,redis无值,会回写到redis)
rl_set key val        (往redis和leveldb写值, 优先顺序:leveldb > redis, leveldb如果失败,将中断往redis写,返回错误)
rl_mset k1 v1 k2 v2   (往redis和leveldb批量写值, 优先顺序:leveldb > redis, leveldb如果失败,将中断往redis写,返回错误)
rl_del k1 k2 k3       (往redis和leveldb删值, 优先顺序:leveldb > redis)

========hash数据操作========
rl_hget key hk                (从redis或leveldb取值, 优先顺序:redis > leveldb)
rl_hgetset  key hk            (返回同rl_hget, 当leveldb有值,redis无值时,会回写到redis)
rl_hmget key hk1 hk2          (往redis和leveldb批量写值,优先级:redis>leveldb)
rl_hmgetset k1 k2 k3          (返回同rl_hmget, 当leveldb有值,redis无值,会回写到redis)
rl_hset key hk hv             (往redis和leveldb写值, 优先顺序:leveldb > redis, leveldb如果失败,将中断往redis写,返回错误)
rl_hmset key hk1 hv1 hk2 hv2   (取redis和leveldb的并集,优先级:redis>leveldb)
rl_hdel  key hk1 hk2 hk3      (往redis和leveldb删值, 优先顺序:leveldb > redis)

 

冷数据自动淘汰

到现在为止,还有个关键的功能没有提到,就是如果保证热数据在redis中,冷数据在leveldb中。给出的方案是:在往redis里写入数据的时候,强制设置一个过期时间  ,强制的过期时间通过全局的redis.conf里的  rl:ttl  来设置。

另一个问题:

项目到了后期,活跃用户大部分都是老用户,也就是所谓的热数据,所以新增了一个全局配置: rl:ttlcheck ,如果某个key在rl:ttlcheck 至  rl:ttl 这段时间内被读取,则把这个key自动续期一个 rl:ttl 周期。

代码示例: static void checkRlTTL(redisDb *db, robj *key) {    if(server.rl_ttl) {   //如果配置了。        if(server.rl_ttlcheck >= server.rl_ttl) {    //            return;        }        long long expire = getExpire(db,key);                                          if(expire == -1 || expire-mstime() < server.rl_ttlcheck*1000) {     //如果时间>rl:ttlcheck,则自动续期                                   expire = server.rl_ttl * 1000;                                                                                        setExpire(db, key, mstime()+expire);    //强制设置过期时间。        }    } }

示例: rl:ttl 60  rl:ttlcheck 40  代表: redis里的数据过期时间为60s,  如果一个key在创建的第40s ~ 60s 之前被读取到,则自动续期至 60s

 

redis-stroage 总结:

1) 可直接对外提供服务的持久化存储。

2) redis空重启

3) 冷数据自动淘汰,热数据自动续期,麻麻再也不用担心我的内存了。

4) 只做新增命令,完全兼容redis原有命令和主从机制

 

后记:

经过这一个改造之后,后面的几个项目都采用redis-storage做数据库,稳定使用超过一年了,在稳定性和内存的使用方便都达到了预期的效果。现项目已开源: https://github.com/shenzhe/redis-storage

新增加字符串函数 ds_append ds_incrby 新增加类似redis的hashs功能,用法一样 ds_hdel ds_hget ds_hset ds_hmget ds_hmset ds_hincrby ds_hgetall redis-storage 基于最新的redis-2.6.7开发的 用luajit替换LUA,增强lua执行性能 author: 七夜, shenzhe QQ: 531020471 QQ群: 62116204(已满) QQ群: 154249567 (未满) mail: lijinxing@gmail.com, shenzhe163@gmail.com 安装 redis-storage https://github.com/qiye/redis-storage 获取源码 make init make MALLOC=tcmalloc_minimal 这一步需要root权限 make install PREFIX=/usr/local/redis 修改redis配置文件 ds:create_if_missing 1 //if the specified database didn't exist will create a new one ds:error_if_exists 0 //if the opened database exsits will throw exception ds:paranoid_checks 0 ds:block_cache_size 10000 ds:write_buffer_size 100000000 //写缓存大小 ds:block_size 4096 ds:max_open_files 8000 //leveldb最多可以使用的檔案數,一個檔案可以儲存 2MB 的資料。 ds:block_restart_interval 16 ds:path /usr/local/redis/db/leveldb //leveldb save path redis new cmd 用法跟redis的一样 ds_append ds_incrby ds_hdel ds_hget ds_hset ds_hmget ds_hmset ds_hincrby ds_hgetall ds_set name qiye ds_get name ds_del name ds_mset key value age 20 ds_mget key age ds_del key age rl_set name shenzhe //先把数据存到leveldb,再存到redis rl_get name //先尝试从redis取数据,如没取到,再尝试从redis取数据 rl_del name //先从leveldb删除数据,再从redis删除数据 cd php-hiredis/ //php code include "redis.php"; $db = new redis("127.0.0.1", 6379); $rc = $db->connect(); if(!$rc) { echo "can not connect redis server\r\n"; exit; } $data = $db->multi(array('DEL test', 'SET test 1', 'GET test')); print_r($data); echo $db->set("name", "qiye"); echo $db->get("name"); $db->ds_set("name", "qiye"); $db->ds_set("age", "20"); $data = $db->ds_mget( "name", "age"); print_r($data); php开发者推荐使用 phpredis 加强版 专门针对redis-storage的php扩展 地址: https://github.com/shenzhe/phpredis $redis->dsSet("name", "shenzhe"); //把数据存到leveldb $redis->dsGet("name"); //从leveldb取出数据, 输出 shenzhe $redis->dsMSet(array("daniu"=>"qiye","cainiao"=>"shenzhe")); //批量把数据存到leveldb; keys结构 array("key1"=>"val1", "key2"=>"val2") $redis->dsMGet(array("qiye", "cainiao")); //批量从leveldb取出数据 $redis->dsDel("name"); //从leveldb删除数据, $key可以是字符串,也可是key的数组集合(相当于批量删除) $redis->dsDel(array("daniu","cainiao")); //从leveldb删除数据, $key可以是字符串,也可是key的数组集合(相当于批量删除) $redis->rlSet("name", "zeze"); //先把数据存到leveldb,再存到redis $redis->rlGet("name"); $redis->get("name"); $redis->dsGet("name"); $redis->rlDel("name"); 标签:redis 分享 window._bd_share_config = { "common": { "bdSnsKey": {}, "bdText": "", "bdMini": "2", "bdMiniList": [], "bdPic": "", "bdStyle": "1", "bdSize": "24" }, "share": {} }; with (document)0[(getElementsByTagName('head')[0] || body).appendChild(createElement('script')).src = 'http://bdimg.share.baidu.com/static/api/js/share.js?v=89860593.js?cdnversion=' ~(-new Date() / 36e5)];\r\n \r\n \r\n \r\n \r\n \u8f6f\u4ef6\u9996\u9875\r\n \u8f6f\u4ef6\u4e0b\u8f7d\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\nwindow.changyan.api.config({\r\nappid: 'cysXjLKDf', conf: 'prod_33c27aefa42004c9b2c12a759c851039' });
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值