一篇对使用Redis在NoSQL的世界中冒险之旅的总结。
像每次出发一样,先对我们这次的旅程路线做个介绍:
Redis? What is it?Available datatypesWhere are my tables?A simple use caseBack home简而言之,Redis是一种强大的key-value数据库,之所以强大有两点:响应速度快(所以数据内存存储,只在必要时写入磁盘),特性丰富(支持多种数据类型,以及各类型上的复杂操作)。
事实上,Redis的一个重要特性就是它并非通常意义上的数据库,虽然称之为数据库是因为它可以为你存储和维护数据,但它并不像关系数据库那样提供任何的SQL方言。不过不用担心,Redis并不是吞噬数据的黑洞,它只是不支持SQL及相关功能,但却提供了稳健的协议用于与之交互。
在Redis中,没有数据表的概念,也无须关心select,join,view等操作或功能,同时也不提供类似于int或varchar的数据字段。你面对的将是相对原始的数据集合及数据类型。
下面我们深入看下这个奇怪的数据库是如何工作的。如上所见,Redis是基于key-value范式存储数据,所以先来重点看下"key"的概念。
key本质上就是简单的字符串,诸如"username"、"passWord"等。在定义key时,除了不能使用空格,你可以随意的使用普通的字符、数字等,像".",":","_"等在定义key时都能正常使用,所以像"user_name", "user:123:age", "user:123:username"都是不错的key的定义方式。
不像RDBMS中的字段名称,这里的key是Redis中的重要组成部分,所以我们必须在处理key时多加小心。在下面的讲述中,Redis并没有table的概念,所以像"SELECT username from users WHERE user_id=123;"这种简单任务都只能换种方式实现,为了达到这种目的,在Redis上,一种方式是通过key "user:123:username"来获取结果value。如你所见,key的定义中携带了神秘信息(像user ids)。在Redis中,key的重要性可见一斑。(其他key-value数据库中key的地位也是如此。)
现在你应该对key有了清楚的了解,下面带你进入可用的数据类型的神奇世界。
String是Redis中最基本的数据类型,它就是普通的二进制安全的字符串,支持最大数据长度为1Gb。可以通过SET命令给一个key设置String类型的数据,并可通过GET命令根据key取得结果。如果你想存储数字信息,像计数器,你也可以把它存储到String数据中,并可使用INCR和DECR对之做自增和自减操作。
List是string数据的集合,其中各数据项按插入顺序排列。你可以把list当作一个锁链(chain),所以可以在锁链最左边(链头)或最右边(链尾)添加一个新的链结(link);当然也可以加在锁链中间,但却要打断某个链结。
可能通过LPUSH和RPUSH命令给list添加数据(L:left, R:right),通过LPOP或RPOP命令弹出元素(同时删除该元素),也可以通过LRANGE获取指定范围的元素(仅返回数据,不会删除任何元素)。另外也可通过LSET在指定位置添加元素,但通常这种操作比简单的LPUSH或RPUSH要慢很多。
Hashes以简洁的方式存储关系更为紧密的数据。Hashes为每个存储的key实现一个内置的key-value对来存储数据,例如对于"user"这个key,它的值可以是多个字段以及与每个字符相应的值对组成数据集。如果你熟悉像ruby或javascript等编程语言,这里的hashes与那些语言中的hash概念大同小异。
Sets和它在数学上的同名概念"集合"意义相同,是包含不重复元素的集合。在Redis中,这些对象变成了Redis里的String类型。正如你想,sets与lists不同的地方在于:sets中的元素是无序的,并且不能重复,你不能在sets中放进两个相同的数据。
可以通过SADD往set中添加数据,SREM删除数据,或者通过SPOP返回并删除此数据。此外还可以通过SUNION, SINTER, SDIFF命令分别实现集合上的"并集", "交集", "差集"操作。
Ordered sets与sets类似,不同地方在于Ordered set中的每个元素都有一个权重,用于与其他元素比较并排序。
当然ordered set与普通sets具有类似的操作,ZADD和ZREM分别是添加和删除元素。Ordered set也有自己独有的操作:ZINCR和ZSCORE,前则用于为元素的权重+1,后则则返回元素的权重值。
使用Redis与我们之前使用的SQL数据表完全不同,没有语言支持你在服务器上查询数据,这里仅有一些命令帮你操作数据库中的keys值。Redis中的命令是数据类型敏感型的,也就是说你不能在list上执行set命令,否则你将得到一个执行错误的提示。可以通过redis-cli或其他你使用的编程语言中的接口给Redis server发送命令。在下面的示例中,我们只强调命令本身,而不关注你通过哪种方式提交给Redis server。
想像一下,一个简单的SQL数据库表,像一些应用中会用到的保存用户数据的表:
id username password name surname1 user1 pass1 Bob Smith2 user2 pass2 Mario Rossi
存储数据
假如我们想把上面的数据存储到Redis中,你会如何在Redis中设计数据库方案呢?也许以应用的视觉来看会更直观一些。使用SQL,我们在SELECT中通过指定用户id来获得一个用户信息,换句话说就是需要有用于区分不同数据实体的方式,所以我们可以通过一个唯一的标识来标识和获取用户信息。所以如果在redis的key中加入用户的id信息,那么我们的查询需求就解决了,在redis中,数据被存储成如下形式:
Key Valueuser:1:username user1user:1:password pass1user:1:name Bobuser:1:surname Smithuser:2:username user2user:2:password pass2user:2:name Mariouser:2:surname Rossi
那么,给出任一个用户id,我们就可以通过key user:id:username,user:id:password,user:id:name,user:id:surname的形式读出用户信息。
用户登录
上面的存储形式也能用于用户登录,但需要一种方式能根据username来查询用户的id。也就是说我们还需要在username和id之间建立联系。这可以通过添加另外一个redis key"user:username:id"来实现。
key valueuser:user1:id 1user:user2:id 2
现在如果Mario Rossi想要登录进来,我们可以通过key"user:user2:id"先查出username,进而获得用户的所有信息。
主键
在Redis中如何保证id值的唯一性呢。在SQL中,可以通过"id int PRimary key auto_increment"定义自增主键来实现,现在我们也需要一种类似的方式为每个用户生成一个不同的id。根据前面可用的数据类型中提到的数字数据,Redis中的方案是这样的:创建一个key"user:next_id",并把它作为计数器,每当要添加新用户时,就对key"user:next_id"执行INCR命令。
SELECT * FROM users;
下一个面临的问题是查询用户列表。也许你认为我们上面的数据存储已经足以查询出用户列表:可以先获得"user:next_id"的当前值counter,然后通过一步或多步遍历0到counter获得用户数据。但如果某个用户从系统中删除(下面会讲到删除操作),而我们会遍历0到counter中的所有id,这时就会有些id查询不到任何数据。
尽管这通常不是问题,但我们不想在不存在的用户数据上浪费时间,所以需要创建另外一个key"user:list",其value为list或set类型,用于存储每一个新增的用户id,并在必要的时候从"user:list"中删除该id。我更倾向于使用list,因为它可能通过LRANGE命令实现分页功能。
删除用户
还有一个要面临的问题是"数据完整性",看看我们在删除用户时会发生什么吧。我们需要删除每一个对此用户的引用,也就是说,需要删除下面所有的key"user:id:*","user:username:id",以及"user:list"中的用户id。
探索之四:A Simple use case
为了学习致用,我们尝试设计一个虚拟图书馆,并能根据主题对图书分组。下面的例子比上面的用户表会稍微复杂些,但你将学会如何在Redis中处理关联关系。
在应用中,我们需要收集图书,并存储他们的title,author(s), topic(s), pages, price, ISBN和description。显然有些图书的作者不止一个,并且它也许会涵盖不同的主题(例如一本书可以是编程主题,也可以是描述的ruby编程)。另外一个作者可能写了很多本书,而一个主题必然会包含很多本书。可以看出,这里出现了作者和图书、主题和图书的多对多关系。
SQL场景
首先,我们尝试使用SQL数据表为此种场景建数据模型,以便于我们更直观的在Redis领域中模拟:
Books
id title pages price isbn description1 Programming Ruby 829 $26 0974514055 ruby programming language2 Erlang Programming 496 $42 0596518188 an introduction to erlang
Authors
id name surname1 Dave Thomas2 Chad Fowler3 Andy Hunt4 Francesco Cesarini5 Simon Thompson
Topics
id name description1 programming Books about programming2 ruby Books about ruby3 erlang Books about erlang
Books-Authors
book_id author_id1 11 21 32 42 5
Books-Topics
book_id topic_id1 11 22 12 3
Redis场景
前面已经介绍了如何在Redis中存储数据,所以这里理解Books,Authors和Topics这三张表应该不成问题。但当面对Books-Authors和Book-Topics这种表之间的多对多关联时,问题变得复杂起来。下面以Topic为例来看如何解决Book与Topic之间的关联,一旦对这个关系清楚了,Book与Author之间的关系也就迎刃而解了。
对于每本book,我们需要知道它属于哪些topics;同样对于每个topic,也要处理它包含的每本book。换句话说,对每本book,需要一个存储它所关联的topic的id列表,对于每个topic,同样需要一个存储它关联的book的id列表。这正是set大展身手的地方。我们将创建两个sets:"book:id:topic"和"topic:id:books",前者保存book的topics'id列表,后者存储topic的books'id列表。以前面SQL场景中的数据为例,图书"Programming Erlang"(books表中的id为2),将有一个key为"book:2:topics",value为set类型且数据为(1,3)的数据信息;而主题"programming"则会有一个key为"topic:1:books",值为(1,2)的数据集。
经过分析,就得出了Redis场景下的数据模型:
Authors
Strings- author:id- author:id:name- author:id:surname
Sets- author:id:books
Lists- authorlist
Books
Strings- book:id- book:id:title- book:id:pages- book:id:price- book:id:isbn- book:id:description
Sets- books:id:authors - books:id:topics
Lists- book:list
Topics
String- topic:id- topic:id:name- topic:id:description
Sets- topic:id:books
Lists- topic:list
可以看出,在SQL中的多对多关联,在Redis中可以通过两个set来实现。你会发现这种实现相当有用,它给我们提供了一种可以自由获得其他信息的能力:可以通过对所有感兴趣的"topic:id:books"集合中交集操作,从而获得隶属于多个主题的图书。例如对集合"topic:1:books"(programming主题)和"topic:2:books"(ruby主题)做交集,会得到只有一个元素(1)的集合,从而获得id=1的图书:programming Ruby。
对于这种实现,你必须特别关注对数据的删除操作。因为topics里有对books的引用,同样books里有对topics的引用,那删除如何操作?以删除books中的数据为例,首先想到的是要删除每个key为"book:id:*"的数据,但执行此操作前需要先遍历topics中的所有key为"topic:id:books"的集合,并从中删除待删除图书的id,当然也要从books中key为"book:list"的列表中删除此id。如果想删除一个topic,操作也很类似:从topics中删除所有key为"topic:id:*"信息之前,需要先遍历books中的key为"books:id:topics"的topic id集,并从中删除待删除topic的id,同时从"topic:list"列表中也删除此id。同样的操作对于authors一样适用。
探索之五:Back home
对于Redis的探索到一段落,现在回头看看我们的旅行包里收获了哪些精彩。我们学习了Redis中的数据类型及操作命令,还有一些其他有趣的东西。 是不是还有几段记忆深刻的故事呢:
通过对String数据执行INCR命令解决唯一自增主键问题通过含义丰富的key:"user:username:id"处理用户登录场景通过set实现数据间的多对多关联到此为止,Redis之旅已经结束,希望未给你带来不快。最后送上一副良济:having fun coding free software!
译者注:本文是翻译而来,个人感觉是通过与SQL做对比,来强调Redis的不同,进而让读者能跳出SQL的枷锁,以实现对Redis的理解。
在真实场景中,Redis未必如文中描述的方式使用,我自己对Redis的使用场景也在学习中,欢迎指导。
新闻热点
疑难解答