Redis官网对Redis的定义是:“Redis is an open source, BSD licensed, advanced key-value cache and store”,可以看出,Redis是一种键值系统,可以用来缓存或存储数据。Redis是“Remote Dictionary Server”(远程字典服务)的缩写,提供了字符串(string),列表(list),哈希(hash),集合(set)和有序集合(sorted set)等5种数据结构,这些数据结构使它成为一种便于使用的键值系统。
作为Redis提供的最简单的数据结构,字符串示意图及其常用命令如下图所示:
图1
可见,Redis字符串类型实际上类似于C++中的map,一个键对应一个值。列表类型则是一种相对高级的数据结构,其示意图和常用命令如下图所示:
图2
列表实际上由一个双向链表实现,所以在列表两端插入或删除数据效率极高,可以用于实现类似于微博、人人网上的“新鲜事”功能。 Redis提供的一个更有趣的结构称为哈希,其示意图和常用命令如下图所示:
图3
一个哈希实际上是一组键值对的集合,哈希本身的键是“父键”,哈希中包含的若干字段是“子键”,每个“子键”都有相应的值。“子键”之间并没有一定的联系,但是它们共同组成了一个完整的哈希结构。我们可以把一个哈希看做关系数据库中的一行,哈希的每个子键对应行的一个字段。因此,当把关系数据库中的数据缓存至Redis时,使用哈希结构可能会带来方便。需要注意的是,哈希结构内部的子键之间是没有顺序关系的。Redis提供的另一个无顺序关系的结构是集合,其示意图和常用命令如下图所示:
图4
集合中的数据满足唯一性和无序性。集合在Redis内部是用哈希表实现的,所以插入和删除操作的时间复杂度均为O(1)。Redis为集合提供了求交并差等运算命令,使用起来非常方便。与集合密切相关的另一个数据结构是有序集合,其示意图和常用命令如下图所示:
图5
有序集合与集合的最大差异在于有序集合中的每个数据都有一个对应的分数,集合中的元素就是按照这些分数进行排序的。从结构功能和使用方法上来看,有序集合可能是Redis提供的5种数据结构中最高级的一种。
Redis是一种内存数据库,这使其在数据存取效率方面表现突出。由于内存中的数据时刻面临丢失的危险,Redis提供了两种持久化机制,及时将内存中的数据写入硬盘。第一种机制为RDB,利用存储快照的方式把内存数据定期写入硬盘;第二种机制为AOF,利用写日志的方式,每执行一条更改Redis数据的命令,就在日志里附加上该命令并保存在硬盘上。Redis默认开启RDB模式,关闭AOF模式,这是因为AOF更加耗时。但是,如果对数据安全要求极高,无法承担任何数据丢失的后果,AOF模式就变成了持久化的首选。
当然,即使Redis具备持久化机制,但是一旦本地硬盘损坏,数据丢失依然在所难免。所以,Redis又提供了复制功能,作用是将一个主数据库(master)的数据自动同步到多个从数据库(slave),从而尽可能防止数据丢失。Redis主从机制可以简单描述为:从数据库启动后,先向主数据库发送SYNC命令;主数据库接到SYNC命令后就开始保存快照,在此期间,所有发给主数据库的命令都被缓存起来;快照保存完成后,主数据库把快照和缓存的命令一起发给从数据库;从数据库保存主数据库发来的快照文件,并依次执行主数据库发来的缓存命令。在同步过程中,从数据库不会阻塞,它默认使用同步之前的数据继续响应客户端发来的命令。
事务是Redis提供的特性之一。首先利用MULTI命令通知Redis,接下来的若干命令属于同一事务;然后输入若干命令,这些命令都被放入命令队列而不会被立即执行;最后,利用EXEC命令通知Redis,属于同一事务的所有命令均已输入完成,现在开始执行该事物。属于同一个事务的所有有效命令或者全部被执行,或者全部不执行,且在执行过程中不会插入其他命令。管道(pipeline)是Redis提供的另一个特性。该特性使Redis能够一次性接收多个命令,执行之后再一次性返回结果。这样能够减少客户端与Redis服务器的通信次数,从而降低往返时延。
除事务和管道以外,Redis还提供了很多其他特性,如设置键的过期时间,利用BLPOP/BRPOP命令实现优先级队列,利用PUBLISH/SUBSCRIBE命令实现消息订阅和发布等,这里不再赘述。
Redis由ANSI C写成,能够运行在包括Linux和OS X在内的大部分POSIX系统上,官方推荐的操作系统为Linux。Redis没有官方的Windows版本,但是微软移植并维护了一个能够运行在Windows上的Redis并放在github上(https://github.com/MSOpenTech/redis)。
Redis的编程接口被称为客户端(clients),绝大部分主流编程语言都有官方推荐的客户端。下图中,具有Redis官方推荐客户端的语言标为绿色,没有官方推荐客户端的标为蓝色:
图6
由于Redis没有官方推荐的C++客户端,因此可以使用C客户端予以代替。官方推荐的C客户端是hiredis,可以从github上找到(https://github.com/redis/hiredis)。
作为一款功能强大,效率极高的NoSQL数据库,Redis已被包括新浪微博,知乎,Stackoverflow,暴雪和Flickr在内的多家公司成功用于实战。
注:图1-5均来自《Redis in Action》,Josiah L.Carlson, Manning Publications, 2013
我们用Redis作MySQL数据库缓存举例:
用Redis作MySQL数据库缓存,必须解决2个问题。首先,应该确定用何种数据结构存储来自Mysql的数据;在确定数据结构之后,还要考虑用什么标识作为该数据结构的键。
直观上看,Mysql中的数据都是按表存储的;更微观地看,这些表都是按行存储的。每执行一次select查询,Mysql都会返回一个结果集,这个结果集由若干行组成。所以,一个自然而然的想法就是在Redis中找到一种对应于Mysql行的数据结构。Redis中提供了五种基本数据结构,即字符串(string)、列表(list)、哈希(hash)、集合(set)和有序集合(sorted set)。经过调研,发现适合存储行的数据结构有两种,即string和hash。
要把Mysql的行数据存入string,首先需要对行数据进行格式化。事实上,结果集的每一行都可以看做若干由字段名和其对应值组成的键值对集合。这种键值对结构很容易让我们想起Json格式。因此,这里选用Json格式作为结果集每一行的格式化模板。根据这一想法,我们可以实现将结果集格式化为若干Json对象,并将Json对象转化为字符串存入Redis的代码:
-
-
- string Cache2String(sql::Connection *mysql_connection,
- redisContext *redis_connection,
- sql::ResultSet *resultset,
- const string &resultset_id, int ttl) {
- if (resultset->rowsCount() == 0) {
- throw runtime_error("FAILURE - no rows");
- }
-
- string prefix("cache.string:" + resultset_id + ":");
- unsigned int num_row = 1;
- sql::ResultSetMetaData *meta = resultset->getMetaData();
- unsigned int num_col = meta->getColumnCount();
-
- string redis_row_set_key("resultset.string:" + resultset_id);
- redisReply *reply;
- string ttlstr;
- stringstream ttlstream;
- ttlstream << ttl;
- ttlstr = ttlstream.str();
- resultset->beforeFirst();
-
-
- while (resultset->next()) {
- string redis_row_key;
- stringstream keystream;
- keystream << prefix << num_row;
- redis_row_key = keystream.str();
- Json::Value row;
- for (int i = 1; i <= num_col; ++i) {
- string col_label = meta->getColumnLabel(i);
- string col_value = resultset->getString(col_label);
- row[col_label] = col_value;
- }
- Json::FastWriter writer;
- string redis_row_value = writer.write(row);
-
- reply = static_cast<redisReply*>(redisCommand(redis_connection,
- "SET %s %s",
- redis_row_key.c_str(),
- redis_row_value.c_str()));
- freeReplyObject(reply);
-
- reply = static_cast<redisReply*>(redisCommand(redis_connection,
- "SADD %s %s",
- redis_row_set_key.c_str(),
- redis_row_key.c_str()));
- freeReplyObject(reply);
-
- reply = static_cast<redisReply*>(redisCommand(redis_connection,
- "EXPIRE %s %s",
- redis_row_key.c_str(),
- ttlstr.c_str()));
- freeReplyObject(reply);
- ++num_row;
- }
-
- reply = static_cast<redisReply*>(redisCommand(redis_connection,
- "EXPIRE %s %s",
- redis_row_set_key.c_str(),
- ttlstr.c_str()));
- freeReplyObject(reply);
- return redis_row_set_key;
- }
要把Mysql的行数据存入hash,过程要比把数据存入string直观很多。这是由hash的结构性质决定的——hash本身就是一个键值对集合:一个“父键”下面包含了很多“子键”,每个“子键”都对应一个值。根据前面的分析可知,结果集中的每一行实际上也是键值对集合。用Redis键值对集合表示Mysql键值对集合应该再合适不过了:对于结果集中的某一行,字段对应于hash的“子键”,字段对应的值就是hash“子键”对应的值,即结果集的一行刚好对应一个hash。这一想法的实现代码如下:
-
-
- string Cache2Hash(sql::Connection *mysql_connection,
- redisContext *redis_connection,
- sql::ResultSet *resultset,
- const string &resultset_id, int ttl) {
- if (resultset->rowsCount() == 0) {
- throw runtime_error("FAILURE - no rows");
- }
-
- string prefix("cache.hash:" + resultset_id + ":");
- unsigned int num_row = 1;
- sql::ResultSetMetaData *meta = resultset->getMetaData();
- unsigned int num_col = meta->getColumnCount();
-
- string redis_row_set_key("resultset.hash:" + resultset_id);
- redisReply *reply;
- string ttlstr;
- stringstream ttlstream;
- ttlstream << ttl;
- ttlstr = ttlstream.str();
-
- resultset->beforeFirst();
- while (resultset->next()) {
- string redis_row_key;
- stringstream keystream;
- keystream << prefix << num_row;
- redis_row_key = keystream.str();
- for (int i = 1; i <= num_col; ++i) {
- string col_label = meta->getColumnLabel(i);
- string col_value = resultset->getString(col_label);
-
- reply = static_cast<redisReply*>(redisCommand(redis_connection,
- "HSET %s %s %s",
- redis_row_key.c_str(),
- col_label.c_str(),
- col_value.c_str()));
- freeReplyObject(reply);
- }
-
- reply = static_cast<redisReply*>(redisCommand(redis_connection,
- "SADD %s %s",
- redis_row_set_key.c_str(),
- redis_row_key.c_str()));
- freeReplyObject(reply);
-
- reply = static_cast<redisReply*>(redisCommand(redis_connection,
- "EXPIRE %s %s",
- redis_row_key.c_str(),
- ttlstr.c_str()));
- freeReplyObject(reply);
- ++num_row;
- }
-
- reply = static_cast<redisReply*>(redisCommand(redis_connection,
- "EXPIRE %s %s",
- redis_row_set_key.c_str(),
- ttlstr.c_str()));
- freeReplyObject(reply);
- return redis_row_set_key;
- }
至此,我们已经给出了两种存储Mysql结果集的方案,这就是我们在篇首提出的第一个问题,即选择何种数据结构存储Mysql结果集的答案。下一个将研究第二个问题,即数据结构键的标识符选择问题。
把MySQL结果集缓存到Redis的字符串或哈希结构中以后,我们面临一个新的问题,即如何为这些字符串或哈希命名,也就是如何确定它们的键。因为这些数据结构所对应的行都属于某个结果集,假如可以找到一种唯一标识结果集的方法,那么只需为这些数据结构分配一个唯一的序号,然后把结果集标识符与该序号结合起来,就能唯一标识一个数据结构了。于是,为字符串和哈希命名的问题就转化为确定结果集标识符的问题。
经过调研,发现一种较为通用的确定结果集标识符的方法。正如我们所知道的,缓存在Redis中的结果集数据都是利用select等sql语句从Mysql中获取的。同样的查询语句会生成同样的结果集(这里暂时不讨论结果集中每条记录的顺序问题),这一性质刚好可以用来确定结果集的唯一标识符。当然,简单地把整个sql语句作为结果集标识符是不可取的,一个显而易见的理由是,未经处理的sql查询语句均包含若干空格,而Redis的键是不允许存在空格的。这时,我们需要一个可以把sql语句转换为唯一标识符的函数。通常,这一功能由散列函数完成,包括MD5,SHA系列等加密散列函数在内的很多算法均可达到这一目的。
确定结果集标识符之后,从Redis读数据或向Redis写数据的思路就很清晰了。对于一个sql语句格式的数据请求,首先计算该语句的MD5并据此得到结果集标识符,然后利用该标识符在Redis中查找该结果集。注意,结果集中的每一行都有一个相应的键,这些键都存储在一个Redis集合结构中。这个集合恰好对应了所需的结果集,所以,该集合的键必须包含结果集标识符。如果Redis中不存在这样一个集合,说明要找的结果集不在Redis中,所以需要执行相应的sql语句,在Mysql中查询到相应的结果集,然后按照上面所说的办法把结果集中的每一行以字符串或哈希的形式存入Redis。在Redis中查找相应结果集的代码如下:
-
- vector<string> GetCache(sql::Connection *mysql_connection,
- redisContext *redis_connection,
- const string &sql, int ttl, int type) {
- vector<string> redis_row_key_vector;
- string resultset_id = md5(sql);
-
- string cache_type = (type == 1) ? "string" : "hash";
-
- string redis_row_set_key = "resultset." + cache_type + ":" + resultset_id;
- redisReply *reply;
-
- reply = static_cast<redisReply*>(redisCommand(redis_connection,
- "SMEMBERS %s",
- redis_row_set_key.c_str()));
- if (reply->type == REDIS_REPLY_ARRAY) {
-
-
- if (reply->elements == 0) {
- freeReplyObject(reply);
- sql::Statement *stmt = mysql_connection->createStatement();
- sql::ResultSet *resultset = stmt->executeQuery(sql);
- if (type == 1) {
- redis_row_set_key = Cache2String(mysql_connection, redis_connection,
- resultset, resultset_id, ttl);
- } else {
- redis_row_set_key = Cache2Hash(mysql_connection, redis_connection,
- resultset, resultset_id, ttl);
- }
-
- reply = static_cast<redisReply*>(redisCommand(redis_connection,
- "SMEMBERS %s",
- redis_row_set_key.c_str()));
- delete resultset;
- delete stmt;
- }
-
- string redis_row_key;
- for (int i = 0; i < reply->elements; ++i) {
- redis_row_key = reply->element[i]->str;
- redis_row_key_vector.push_back(redis_row_key);
- }
- freeReplyObject(reply);
- } else {
- freeReplyObject(reply);
- throw runtime_error("FAILURE - SMEMBERS error");
- }
- return redis_row_key_vector;
- }
现在我们已经掌握了确定Redis中的结果集标识符以及各数据结构的键的方法。下一个将研究结果集在Redis中的排序和分页问题。
在实现缓存排序功能之前,必须先明白这一功能的合理性。不妨思考一下,既然可以在数据库中排序,为什么还要把排序功能放在缓存中实现呢?这里简单总结了两个原因:首先,排序会增加数据库的负载,难以支撑高并发的应用;其次,在缓存中排序不会遇到表锁定的问题。Redis恰好提供了排序功能,使我们可以方便地实现缓存排序。
Redis中用于实现排序功能的是SORT命令。该命令提供了多种参数,可以对列表,集合和有序集合进行排序。SORT命令格式如下:
- SORT key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASC | DESC] [ALPHA] [STORE destination]
BY参数用于指定排序字段,功能类似于SQL中的order by。对于列表和集合而言,仅按照它们的值进行排序往往没有实际意义。以函数Cache2Hash返回的集合为例(实际上返回的是集合键),该集合中存储的是一系列完整的哈希键,只按照这些键进行排序,结果无非是按照数字或字典顺序排列,其用处显然不大。这是因为真正存储行数据的是哈希结构本身,而非哈希键。假设集合键为"resultset.hash:123456",集合中每个哈希键对应的哈希结构中都有一个名为“timestamp”的字段,现在要把集合中的所有哈希键按照timestamp字段进行排序,这时,只需执行以下命令:
- SORT resultset.hash:123456 BY *->timestamp
从上例可以看出,BY的真正威力在于它可以让SORT命令按照一个指定的外部键的外部字段进行排序。SORT用集合
resultset.hash:123456中的每个值(即每个哈希键)替换BY参数后的第一个“*”,并依据“->”后面给出的字段获取其值,最后根据这些字段值对哈希键进行排序。
LIMIT参数用于限制排序以后返回元素的数量,功能类似于SQL中的limit。该参数接受另外两个参数,即offset和count,LIMIT offset count表示跳过前offset个元素,返回之后的连续count个元素。可见,LIMIT参数可以用于实现分页功能。
GET参数用于返回指定的字段值。以集合resultset.hash:123456为例,使用BY参数对集合中的所有哈希键按照哈希结构中的timestamp字段排序后,SORT命令返回所有排序之后的哈希键。如果某个请求需要不是键而是某些字段值,这时就要使用GET参数,使SORT命令返回指定字段值。假设除timestamp字段以外,集合中每个哈希键对应的哈希结构中还有一个名为“id”的字段,通过以下命令可以使SORT返回按照timestamp排序以后的每个哈希键对应的哈希结构中的timestamp和id值:
- SORT resultset.hash:123456 BY *->timestamp GET *->timestamp GET *->id
SORT用
集合
resultset.hash:123456中的每个值(即每个哈希键)替换GET参数之后的第一个“*”,并将其作为返回值。值得注意的是,利用GET #能够得到集合中的哈希键本身。
ASC和DESC参数用于指定排序顺序(默认为ASC,即从低到高),ALPHA参数用于按照字典顺序排列非数字元素。
STORE参数用于将SORT命令的返回值,即排序结果存入一个指定的列表。加上STORE参数后,SORT命令的返回值就变为排序结果的个数。
下面的代码实现了按照哈希的某个字段对集合中的哈希键排序,并将结果存入列表的过程:
-
-
-
- string SortHash(sql::Connection *mysql_connection,
- redisContext *redis_connection,
- const string &resultset_id,
- const string &sort_field,
- int offset, int count, int order, int ttl) {
-
- string redis_row_set_key = "resultset.hash:" + resultset_id;
- redisReply *reply;
-
- reply = static_cast<redisReply*>(redisCommand(redis_connection,
- "EXISTS %s",
- redis_row_set_key.c_str()));
- if (reply->integer == 0) {
- freeReplyObject(reply);
- throw runtime_error("FAILURE - no resultsets");
- } else {
- freeReplyObject(reply);
- }
- string field_md5 = md5(sort_field);
-
- string redis_sorted_list_key = "sorted:" + resultset_id + ":" + field_md5;
- string by("*->" + sort_field);
- string ord = (order == 1) ? "ASC" : "DESC";
- stringstream ofsstream, cntstream;
- ofsstream << offset;
- cntstream << count;
-
- reply = static_cast<redisReply*>(redisCommand(
- redis_connection,
- "SORT %s BY %s LIMIT %s %s GET %s ALPHA STORE %s",
- redis_row_set_key.c_str(),
- by.c_str(),
- ofsstream.str().c_str(),
- cntstream.str().c_str(),
- "#",
- redis_sorted_list_key.c_str()));
- freeReplyObject(reply);
- stringstream ttlstream;
- ttlstream << ttl;
-
- reply = static_cast<redisReply*>(redisCommand(redis_connection,
- "EXPIRE %s %s",
- redis_sorted_list_key.c_str(),
- ttlstream.str().c_str()));
- freeReplyObject(reply);
- return redis_sorted_list_key;
显然,对结果集中的哈希键进行排序要比对字符串键排序更加直观和方便。借助于排序函数,可以方便地实现在Redis中查询排序后的结果集,代码如下:
-
-
- vector<string> GetSortedCache(sql::Connection *mysql_connection,
- redisContext *redis_connection,
- const string &sql, const string &sort_field,
- int offset, int count, int order, int ttl) {
- vector<string> redis_row_key_vector;
- redisReply *reply;
- string resultset_id = md5(sql);
- string field_md5 = md5(sort_field);
-
- string redis_sorted_list_key = "sorted:" + resultset_id + ":" + field_md5;
-
- reply = static_cast<redisReply*>(redisCommand(redis_connection,
- "LRANGE %s %s %s",
- redis_sorted_list_key.c_str(),
- "0",
- "-1"));
- if (reply->type == REDIS_REPLY_ARRAY) {
-
-
- if (reply->elements == 0) {
- freeReplyObject(reply);
- sql::Statement *stmt = mysql_connection->createStatement();
- sql::ResultSet *resultset = stmt->executeQuery(sql);
- Cache2Hash(mysql_connection, redis_connection, resultset,
- resultset_id, ttl);
- redis_sorted_list_key = SortHash(mysql_connection, redis_connection,
- resultset_id, sort_field, offset,
- count, order, ttl);
-
- reply = static_cast<redisReply*>(redisCommand(
- redis_connection,
- "LRANGE %s %s %s",
- redis_sorted_list_key.c_str(),
- "0",
- "-1"));
- delete resultset;
- delete stmt;
- }
-
- string redis_row_key;
- for (int i = 0; i < reply->elements; ++i) {
- redis_row_key = reply->element[i]->str;
- redis_row_key_vector.push_back(redis_row_key);
- }
- freeReplyObject(reply);
- } else {
- freeReplyObject(reply);
- throw runtime_error("FAILURE - LRANGE error");
- }
- return redis_row_key_vector;
- }
这样,在Redis中对结果集进行简单排序操作的功能就实现了。
所有评论(0)