zoukankan      html  css  js  c++  java
  • redis-storage介绍[转]

    背景

    当时我们正在做一个游戏项目,游戏项目相比于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 ", __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

  • 相关阅读:
    JavaScript操作符instanceof揭秘
    Linux打开txt文件乱码的解决方法
    Working copy locked run svn cleanup not work
    poj 2299 UltraQuickSort 归并排序求解逆序对
    poj 2312 Battle City 优先队列+bfs 或 记忆化广搜
    poj2352 stars 树状数组
    poj 2286 The Rotation Game 迭代加深
    hdu 1800 Flying to the Mars
    poj 3038 Children of the Candy Corn bfs dfs
    hdu 1983 Kaitou Kid The Phantom Thief (2) DFS + BFS
  • 原文地址:https://www.cnblogs.com/qqflying/p/9190916.html
Copyright © 2011-2022 走看看