前段时间做的开发有缓存数据持久化的需求, 最终采用了 redis, 实行效果还不错, 聊做总结.
, 技术选型
要解决问题首先要分析问题. 应用角色是移动物联网平台 OneNET 和公司 SCADA 系统 MQ 中间件的数据透传 worker, 但是从 MQ 到 OneNET 的命令需要暂存, 等 OneNET 平台推送设备上线消息后才能下发, OneNET 上挂设备数量可能会有数十万只, 且设备上线后 1~2 分钟未收到命令就会下线. 可以看出这些暂存命令需要极高的数据一致性, 不然给用户多充了几次钱或者没充上, 说好了要开阀结果没开或者关阀没关上, 很容易影响用户体验和公司利益.
鉴于透传命令是结构简单的二进制字符串, 且对于每个设备都需要有设备信息哈希表和暂存命令队列, 但既不需要做复杂的 SQL 查询也不需要事务等特性支持, 因此
根据实际情况, 考虑以下几点:
每个设备需要有对应的设备信息哈希表和暂存命令队列, 有嵌套数据结构, 透传命令是二进制字符串;
数据需要备份容灾, 且有一致性需求;
不需要进行复杂 SQL 查询也不需要 ACID 事务等 RDMS 特性支持.
可以得出 redis 十分适合此场景的结论:
redis 具备较多的数据结构, 其中 list 和 hash 可以联合解决实际的嵌套数据结构问题, 只需要做一次映射即可, 不用序列化;
redis 具备 AOF 数据持久化策略, 采用 always 选项可以获得极高的一致性保证;
redis 是 NoSQL 内存数据库, 抛弃掉冗余的 RDMS 特性可以获得更高的性能.
一, redis 的编译和部署
由于生产环境是 Windows, 开发语言采用 C++, 因此采用微软改写存档的 hiredis on Windows https://github.com/MicrosoftArchive/redis , 版本采用的是 3.0, 发布时采用的是 vs2013, 我用 vs2015 直接迁移可以成功编译, 十分顺利, 但是我的机器是 win10, 后续发布到 xp 时才发现其源码里有致命的逻辑错误, 我的另一篇文章里面进行了详细说明, 如果你需要 xp 兼容的话可以去看一下: windows xp 下 redis 客户端无法连接服务端.
由于 redis 自己的依赖库都是静态库, 因此易于使用, 编译后把 redis-server.exe 和 redis-cli.exe 拷到部署环境即可, 可以选择命令行方式运行或者 Windows 服务方式运行 https://raw.githubusercontent.com/MSOpenTech/redis/3.0/Windows Service Documentation.md , 通过 xxx.conf 选项来选择要加载的 redis 配置文件.
e.g.
- redis-server redis.conf
- or
- redis-server --service-install redis.service.conf
- .
NOTE: 说到配置文件, 有一个地方需要特别说明, 微软存档的 conf 文件里 logfile 选项值的格式是错误的, 这会导致加载配置文件时 redis 无法正常启动, 需要手动修改 conf 文件的 logfile 选项为合法路径.
二, redis 的 API 使用
可调用的 API 都在 hiredis.h 里了, 命名都很清晰, 可以顾名思义, 需要注意的有以下几点:
建议不要用 redisConnect 而是 redisConnectWithTimeout;
返回的 void * 类型的 redisReply * 对象需要手动释放(freeReplyObject);
redisContext * 对象使用完毕后也需要手动释放;
如果返回的 redisReply * 对象为空, 说明与 redis 服务器的连接中断, 需要重连或排查问题;
多条 redis 指令在不是特别在意事务性的情况下建议采用 pipeline 的方式提高效率.
贴一些自己的实际应用代码 (有部分 Qt API) 以供参考:
- ...
- // some redis api encapsulations
- void ClassName::SafeFree(redisReply* &reply)
- {
- if (!reply) return;
- freeReplyObject(reply);
- reply = nullptr;
- }
- void ClassName::TryConn()
- {
- if (m_con)
- {
- redisFree(m_con);
- m_con = nullptr;
- }
- timeval interval = { 3,0 };// timeout when exceed 3s
- m_con = redisConnectWithTimeout(m_host, m_port, interval);
- !m_con || m_con->err ? qFatal("Connect to redis failed!") : qInfo("Connect to redis successed.");
- }
- bool ClassName::Validate(redisReply* &reply)
- {
- if (!reply)
- {
- qWarning("Connection to redis interrupted.");
- TryConn();
- return false;
- }
- if (reply->type == REDIS_REPLY_ERROR)
- {
- SafeFree(reply);
- return false;
- }
- return true;
- }
- ...
- ...
- // some sample code
- // redis data structure:
- // hash:{key:dev_id,status:online or offline,imei:imei}
- // list:{key:imei,value:commands}
- // list key(imei) is mapped to hash(key is dev id) field(imei) value
- // one imei is singly corresponding to one dev id and both of them are unique
- m_sendThread = std::thread([this,dev_id] {
- std::this_thread::sleep_for(std::chrono::seconds(m_delay));
- qInfo("Send thread for device %d start.", dev_id);
- QString imei, redis_cmd;
- QByteArray cmd;
- redisReply* reply;
- int err = 0, nil = 0, max = m_persist / 3;
- // is online?->get imei->command dequeue->enqueue when command failed
- while (err <3 && nil < max)
- {// 3s * max = m_persist seconds retain for nil retry
- redis_cmd = QString("hget %1 status").arg(dev_id);
- reply = static_cast<redisReply*>(redisCommand(m_con, qPrintable(redis_cmd)));
- if (!Validate(reply)) goto _err_;
- if (*reply->str == '0') goto _quit_;
- SafeFree(reply);
- redis_cmd = QString("hget %1 imei").arg(dev_id);
- reply = static_cast<redisReply*>(redisCommand(m_con, qPrintable(redis_cmd)));
- if (!Validate(reply)) goto _err_;
- if ((imei = reply->str).isEmpty()) goto _nil_;
- SafeFree(reply);
- redis_cmd = QString("lpop %1").arg(imei);
- reply = static_cast<redisReply*>(redisCommand(m_con, qPrintable(redis_cmd)));
- if (!Validate(reply)) goto _err_;
- if ((cmd = reply->str).isEmpty()) goto _nil_;
- SafeFree(reply);
- if (!WriteDeviceResource(imei, QString::number(dev_id), cmd))
- {
- redis_cmd = QString("rpush %1 %2").arg(imei, (QString)cmd);
- reply = static_cast<redisReply*>(redisCommand(m_con, qPrintable(redis_cmd)));
- if(Validate(reply)) SafeFree(reply);
- goto _err_;
- }
- err = nil = 0; continue;// reset when success
- _err_:
- qWarning("Perform redis cmd failed: %s", qPrintable(redis_cmd));
- err++; continue;
- _nil_:
- SafeFree(reply);
- std::this_thread::sleep_for(std::chrono::seconds(3));
- nil++;
- }
- _quit_:
- SafeFree(reply);
- qInfo("Send thread for device %d quitted.", dev_id);
- });
- ...
三, redis 的持久化策略与并发处理
redis 的持久化策略有两种, 一是 RDB 方式(Redis DataBase), 二是 AOF 方式(AppendOnly File), 前者每隔一段时间或每经一定数量的数据库更改操作 fork 一个子进程备份当前时间点的快照, 后者把对数据库的每次更改操作写入 AOF 文件, 当 AOF 文件过大时会 fork 子进程进行重写.
RDB 优势:
更好的性能, 更快的加载速度(启动时), 按计划备份.
RDB 劣势:
较差的数据安全性, 最多可能会丢失两次快照间的所有数据更改;
每次备份时 fork 出的子进程会导致 redis 短暂的服务无响应状态(依据数据量和 CPU 性能从毫秒级到秒级).
相关配置:
- # 900/300/60 秒内如果有至少 1/10/10000 次更改时进行备份
- save 900 1
- save 300 10
- save 60 10000
- # 当无法正常备份时是否允许客户端进行更改 redis 数据的操作
- stop-writes-on-bgsave-error yes
- # 是否压缩
- rdbcompression yes
- # 是否校验 RDB 文件
- rdbchecksum yes
- # RDB 文件名
- dbfilename dump.rdb
- # RDB 文件路径
- dir ./
AOF 优势:
更好的数据安全性, 容灾效果好, 极小的 fork 影响(重写 AOF 文件的频率很低).
AOF 劣势:
性能较差, 这在每次更改都 fsync 时影响最为明显, 但这也带来了最高的安全性;
AOF 文件总是要比 RDB 大.
相关配置:
- # 是否开启 AOF
- appendonly yes
- # AOF 策略(always/everysec/no: 安全性递减, 性能递增)
- appendfsync everysec
- # 在重写 AOF 文件时是否允许主进程调用 fsync
- # (因为重写 AOF 文件时会有大量 I/O,
- # 如果此时主进程也写入 AOF 文件会造成阻塞,
- # 如果不写入则有可能造成数据丢失)
- no-appendfsync-on-rewrite no
- # 当超过 min-size 且当前大小超过上一次重写时大小的 percentage% 时自动重写 AOF
- auto-aof-rewrite-percentage 100
- auto-aof-rewrite-min-size 64mb
- # 当 AOF 文件尾部出错时是否尽可能多地加载正确部分(加载前建议先 redis-check-aof)
- aof-load-truncated yes
可以通过官方文档和 antirez 的 blog 对它们更进一步地详细了解:
- https://redis.io/topics/persistence
- http://oldblog.antirez.com/post/redis-persistence-demystified.html
关于并发问题, redis 的数据库操作是单线程的, 因此可以不必担心它的线程安全问题, 至于性能, I/O 本身就是最大的开销不是吗.
四, 其他配置项
用到的几个比较关键的配置项有:
bind: 如果配置此项的话, 非 bind ip 无法访问 redis, 为了数据库安全性建议配置;
loglevel: 一般在调试开发的时候用 debug, 部署的时候用 notice;
maxclients: 允许的最大客户端连接数;
maxmemory: 允许使用的最大内存, 单位为字节, 建议配置以防内存溢出;
maxmemory-policy: 内存淘汰策略, 这部分我是自己写的, 因为 redis 提供的不满足需求, 如果符合需求想省事儿的话可以在这里采用 redis 提供的策略.
其他还有许多配置项就不一一列举了, 需要的时候可以自己查看 conf 文件, 里面的注释也十分详尽, 注意秉承用不到就不修改的原则.
一些感想:
一个程序员最根本的武器还是是数据结构和算法, 当有数据库的需求时, 如果它内置了熟悉的数据结构或算法就可以直接拿来用, 否则需要重新审视一下技术选型或者是自己造一个好用的轮子来解决.
如果对程序有更高的性能需求, 任何组件当做黑盒来用都是不合适的, 必须先了解它的基本原理才能得心应手.
来源: http://www.jianshu.com/p/e44bf5d7b321