redis的诞生
redis的创建者 是一个叫做antriez的意大利男士,个人网站http://invece.org/
2007年和朋友创建了是一个访客信息追踪网站,网站可以通过JS脚本,将访客的IP地址,所属国家,阅览器信息,被访问页面的地址等数据传送给LLOOGG.com,然后LLOOGG.com将这些数据通过web页面实时地展示给用户,并存储起最新的5至10000条浏览记录以便进行查阅。为了记录每个被追踪的浏览信息,LLOOGG.com需要为每个被追踪的网站创建一个列表,每个列表需要根据用户的设置,存储最新的5至10000条浏览记录。当时使用mysql数据库。
为了解决负载问题,自己写一个具有列表结构的内存数据库原型,这个数据库原型支持复杂的推入和弹出操作,并且将数据存储在内存而不是硬盘,所以程序的性能不会受到硬盘I/O限制,可以以极快的速度执行针对列表的推入和弹出操作,解决了LLOOGG.com当时的负载问题,于是antriez使用c语言重写了这个内存数据库,并给它加上持久化功能,Redis就诞生!
Redis的演进
图片如下:
Redis的独特性
1.独特的键值对模型
很多数据库只能处理一种数据结构
Redis也是键值对数据库,但和Memcached不同的是,Redis的值不仅可以是字符串,它还可以其他5种数据结构中的任意一种。通过选用不同的数据结构,用户可以使用Redis解决各式各样的问题。
它的键是字符串,关联的值可以是字符串,列表,散列,集合,有序集合,哈希;
2.内存存储,速度极快
硬盘数据库的工作模式:数据存储在硬盘,储存索引在内存
内存数据库的工作模式:数据直接存储在内存中。
3.丰富的附加功能
4.良好的支持
5.广泛的使用
一.字符串键
redis中最简单的数据结构,可以储存文字(比如:"hello world"),数字(比如整数和浮点数),二进制数据
字符串基本操作
1.为字符串键设置值
set key value
将字符串键key的值设置为value,命令返回OK表示设置成功;如果字符串键key已经存在,那么用新值覆盖原来的旧值。
set msg "hello world"
set msg "goodbye"//覆盖原来的值"hello world"
set key value [NX|XX]
set命令还支持可选的NX选项和XX选项:
如果给定了NX选项,那么命令仅在键key不存在的情况下,才进行设置操作;如果键key存在,set...NX命令不做动作(不会覆盖旧值)
如果给定了XX选项,那么命令仅在键key已经存在的情况下,才进行设置操作;如果键key不存在,set..XX命令不做动作(一定会覆盖旧值)
在给定NX选项和XX选项的情况下,set命令在设置成功时返回OK,设置失败时返回nil
示例:使用Redis来进行缓存
我们可以使用Redis来缓存一些经常会被用到,或者需要耗费大量资源的内容,通过将这些内容放到Redis里面(也即是内存里面),程序可以以极快的速度取得这些内容。
缓存程序的API及其实现
cache.py
class Cache:
def __init__(self, client):
self.client = client
def put(self, name, content):
self.client.set(name, content)
def get(self, name):
return self.client.get(name)
仅在键不存在的情况下进行设置
SETNX key value
键不存在并且设置成功时,命令返回1,因为键已经存在而导致设置失败时,命令返回0;复杂度为O(1).
同时设置或获取多个字符串键的值
示例:设置或获取个人信息
键的命名
一次设置多个不存在的键
MSETNX key value[key value...]
只有在所有给定键不存在的情况下,MSETNX会给所有规定键设置值,效果和执行多个SETNX一样,如果给定的键至少有一个是存在的,那么MSETNX将不执行任何设置任务。
返回1 表示设置成功,返回0表示设置失败。复杂度为O(N),N为给定的键的个数
设置新值并返回旧值
GETSET key new-value
将字符串键的值设置为new-value,并返回字符串键在设置新值之前储存的旧值(old value).复杂度为O(1).
set getset-str "i am old value"
getset getset-str "i am new value"
get getset-str
追加内容到字符串末尾
APPEND key value
将值value推入到字符串键key已储存内容的末尾
复杂度为O(N),其中N为被推入值的长度
set myPhone "nokia"
append myPhone "-1110"
get myPhone
返回值的长度
strlen key
返回字符串键key储存的值的长度
因为redis会记录每个字符串值的长度,所以获取该值的复杂度为O(1)
set msg "hello"
strlen msg
append msg " world"
strlen msg
索引
范围设值(值的更改)
setrange key index value
从索引index开始,用value覆写给定key所储存的字符串值。只接受正数索引,命令返回覆写之后,字符串值的长度,复杂度为O(N),N为value的长度。
set msg "hello"//ok
setrange msg 1 "appy"//(integer)5
get msg //"happy"
范围取值
getrange key start end
数字操作
设置和获取数字
增加或者减少数字的值
增一或减一
示例:计数器(counter)
很多网站都使用了计数器来记录页面被访问的次数。
每当用户访问页面时,程序首先将页面访问计数器的值增一,然后将计数器当前的值返回给用户观看,以便用户通过页面的访问次数来判断页面内容的受关注程度。
使用字符串键以及INCR,GET等命令,我们也可以实现这样的计数器。
计数器API及其实现
counter.py
# encoding: utf-8
class Counter:
def __init__(self, key, client):
self.key = key
self.client = client
def incr(self, n=1):
counter = self.client.incr(self.key, n)
return int(counter)
def decr(self, n=1):
counter = self.client.decr(self.key, n)
return int(counter)
def reset(self, n=0):
counter = self.client.getset(self.key, n)
if counter is None:
counter = 0
return int(counter)
def get(self):
counter = self.client.get(self.key)
if counter is None:
示例:id生成器
很多网站在创建新条目的时候,都会使用id生成器来为条目创建唯一标识符。
举个例子,对于一个论坛来说,每注册一个新用户,论坛都会为这个新用户创建一个用户id,比如12345,然后访问/user/12345就可以看到这个用户的个人页面。
又比如说,当论坛里的用户创建一个新帖子的时候,论坛都会为这个新帖子创建一个帖子id,比如10086,然后访问/topic/10086就可以看到这个帖子的内容。
被创建的id通常都是连续的,比如说,如果最新创建的id为1003,那么下一个生成的id就会是1004,再下一个id就是1005,以此类推。
id生成器API及其实现
id_generator.py
class IdGenerator:
def __init__(self, key, client):
self.key = key
self.client = client
def init(self, n):
self.client.set(self.key, n)
def gen(self):
new_id = self.client.incr(self.key)
return int(new_id)
浮点数的自增和自减
incrbyfloat key increment
为字符串键key储存的值加上浮点数增量increment,命令返回操作执行之后键key的值。
没有相应的decrbyfloat,但可以通过给定负值来达到decrbyfloat的效果。
复杂度为O(1).
set num 10//ok
incrbyfloat num 3.14//"13.14"
incrbyfloat num -2.04//"11.1"通过传递负值来达到减法的效果
注意事项
即使字符串键储存的是数字值,它也可以执行append、strlen、setrange和getrange.当用户针对一个数字值执行这些命令的时候,Redis会先将数字值转换为字符串,然后再执行命令。
set number 123 //ok
strlen number //3 转换为"123",然后计算这个字符串的长度
append number 456 //6 转换为"123",然后与"456"进行拼接
get number //123456
二进制数据操作
set、get、setnx、append等命令同样可以用于设置二进制数据。
//因为Redis自带的客户端redis-cli没办法方便的设置二进制数据。所以这里使用python客户端来进行
import redis
r = redis.Redis()
r.set('bits',0b10010100) //True 将字符串键bits的值设置为二进制10010100
bin(int(r.get('bits'))) //'0b10010100' 获取字符串键bits储存的二进制值(需要进行转换)
r.append('bits',0b111) //4L 将0b111(也即是十进制的7)推入到bit已有二进制位的末尾
bin(int(r.get('bit'))) //'0b10111001111' 推入之后的值为0b10111001111=1487
二进制位的索引
设置二进制位的值
setbit key index value
将给定索引上的二进制位的值设置为value,命令返回被设置的位原来储存的旧值。
获取二进制位的值
getbit key index 返回给定索引上的二进制位的值。复杂度为O(1)。
计算值为1的二进制位的数量
bitcount key [start] [end]
计算并返回字符串键储存的值中被设置为1的二进制位的数量。
一般情况下,给定的整个字符串键都会进行计数操作,但通过指定额外的start或end参数,可以让计数只在特定索引范围的位上进行。
start和end参数的设置和getrange命令类似,都可以使用负数值:比如-1表示最后一个位,而-2表示倒数第二个位,以此类推。
复杂度为O(N),其中N为被计算二进制位的数量。
二进制位运算
bitop operation destkey key[key...]
对一个或多个保存二进制位的字符串键执行位元操作,并将结果保存到destkey上。operation可以是and、or、not、not、xor这四种操作中的任意一种:
示例:实现在线人数统计
在用户id和位索引之间进行关联
在线用户统计的API及其实现
online_count.py
class OnlineCount:
def __init__(self, when, client):
self.when = when
self.client = client
def include(self, user_id):
return self.client.setbit(self.when, user_id, 1)
def result(self):
return self.client.bitcount(self.when)
关于用户在线统计的更多信息
示例:使用Redis缓存热门图片
图片网站要储存大量的图片(通常放在硬盘里面),而少部分热门的图片会经常地被访问到。
为了加快网站获取热门图片的速度,我们可以利用Redis能够储存二进制数据这一特性,使用之前构建
缓存程序来缓存图片网站中的热门图片。
储存中文时的注意事项
一个英文字符只需要使用单个字节来储存,而一个中文字符却需要使用多个字节来储存。
strlen,setrange和getrange都是为英文设置的,它们只会在字符为单个字节的情况下正常工作,而一旦我们储存的是类似中文这样的多字节字符,那么这三个命令就不在适用了。
strlen示例
setrange和getrange的示例
字符串键的总结
二.散列键
一个散列由多个域值对(field-value pair)组成,散列的域和值都可以是文字、整数、浮点数或者二进制数据。
同一个散列里面的每个域必须是独一无二、各不相同的,而域内的值可以是重复的。
通过命令,用户可以对散列执行设置域值对、获取域的值、检查域是否存在等操作,也可以让Redis返回散列包含的所有域、所有值或者所有域值对。
散列的基本操作
关联域值对
hset key field value
在散列键key中关联给定的域值对field和value。
如果域field之前没有关联值,那么命令返回1;
如果field已经有关联值,那么命令用新值覆盖旧值,并返回0
复杂度为O(1).
HSET message "id" 10086 //1
HSET message "sender" "peter" //1
HSET message "receiver" "jack" //1
获取域关联的值
hget key field
返回散列键key中,域field所关联的值。如果域field没有关联值,那么返回
nil。复杂度为O(1)。
仅当域不存在时,关联域值对
HSETNX key field value
如果散列键key中,域field不存在(也即是,还没有与之相关联的值),那么关联给定的域值对field和value。如果域field已经有与之相关联的值,那么命令不做动作。复杂度为O(1)。
HSETNX message "content" "Good morning,jack!" //1
HSETNX message "content" "Good morning,jack!" //0
检查域是否存在
HEXISTS key field
查看散列键key中,给定域field是否存在:存在返回1,不存在返回0.复杂度为O(1)。
hexists message "id" //1
hexists message "sender" //1
hexists message "age" //0
hexists message "NotExistsField" //0
删除给定的域值对
hdel key field[field...]
删除散列键key中的一个或多个指定域,以及那些域的值。不存在的域将被忽略。命令返回被成功删除的域值对数量。
复杂度为O(N),N为被删除的域值对数量。
hdel message "id" //1
hdel message "receiver" //1
hdel message "sender" //1
获取散列包含的键值对数量
HLEN key
返回散列键key包含的域值对数量,复杂度为O(1)
hlen message //4
hdel message "date" //1
hlen message //3
批量操作
一次对多个域、多个值或者多个域值对进行操作。
获取散列包含的所有域、值、或者域值对
数字操作
和字符串键的值一样,在散列里面,域的值也可以被解释为数字,并执行相应的数字操作。
对域的值执行自增操作
使用散列的好处
(1)将数据放到同一个地方,而并不是直接分散地储存在整个数据库里面,这不仅方便数据管理,还避免误操作的发生。
(2)避免键名冲突
(3)减少内存占用
在一般情况下,保存相同数量的键值对信息,使用散列键比使用字符串键更节约内存。
因为在数据库里面创建的每一个键都会带有数据库附加的管理信息(比如这个键的类型、最后一次被访问的时间等等),所以数据库里面的键越多,服务器在储存附加管理信息方面耗费的内存就越多,花在管理数据库键上的CPU也会越多。
除此之外,当散列包含的域值对数量比较少的时候,Redis会自动使用一种占用内存非常少的数据结构来做散列的底层实现,在散列的数量比较多的时候,这一措施对减少内存有很大的帮助。
结论
示例:使用散列重新实现计数器
# 保存所有计数器的散列键
COUNTER_KEY = 'hash-counter'
class Counter:
def __init__(self, name, client):
self.name = name
self.client = client
def incr(self, n=1):
counter = self.client.hincrby(COUNTER_KEY, self.name, n)
return int(counter)
def decr(self, n=1):
minus_n = -n
counter = self.client.hincrby(COUNTER_KEY, self.name, minus_n)
return int(counter)
def reset(self, n=0):
counter = self.client.hget(COUNTER_KEY, self.name)
if counter is None:
counter = 0
self.client.hset(COUNTER_KEY, self.name, n)
return int(counter)
def get(self):
counter = self.client.hget(COUNTER_KEY, self.name)
if counter is None:
counter = 0
return int(counter)
列表
以有序的方式储存多个可重复的值
1.推入和弹出操作
了解如何向列表添加项,及如何从列表里面删除项
2.长度、索引和范围操作
3.列表示例
# encoding: utf-8
def create_timeline_key(user_name):
"""
创建 'user::<name>::timeline' 格式的时间线键名
举个例子,输入 'huangz' 将返回键名 'user::huangz::timeline'
"""
return 'user::' + user_name + '::timeline'
class Timeline:
def __init__(self, user_name, client):
self.key = create_timeline_key(user_name)
self.client = client
def push(self, message_id):
return self.client.lpush(self.key, message_id)
def fetch_recent(self, n):
return self.client.lrange(self.key, 0, n-1)
def fetch_from_index(self, start_index, n):
return self.client.lrange(self.key, start_index, start_index+n-1)
4.插入和删除操作
# encoding: utf-8
class MessageQueue:
def __init__(self, key, client):
self.key = key
self.client = client
def enqueue(self, item):
self.client.lpush(self.key, item)
def dequeue(self, timeout):
result = self.client.brpop(self.key, timeout)
if result:
poped_list, poped_item = result
return poped_item
def length(self):
return self.client.llen(self.key)
def get_all_items(self):
return self.client.lrange(self.key, 0, -1)
5.阻塞式弹出操作
# encoding: utf-8
class FixedFIFO:
def __init__(self, key, max_length, client):
self.key = key
self.max_length = max_length
self.client = client
def enqueue(self,item):
# 这里存在一个竞争条件:
# 如果客户端在 LPUSH 成功之后断线
# 那么队列里将有超过最大长度数量的值存在
# 等我们学习了事务之后就来修复这个竞争条件
# 将值推入列表
self.client.lpush(self.key, item)
# 如果有必要的话,进行修剪以便让列表保持在最大长度之内
self.client.ltrim(self.key, 0, self.max_length-1)
# 返回 1 表示入队成功
return 1
def dequeue(self):
return self.client.rpop(self.key)
def get_all_items(self):
6复习
集合
储存多个各不相同的元素
1.元素操作
# encoding: utf-8
class Vote:
def __init__(self, key, client):
self.key = key
self.client = client
def cast(self, user):
# 因为 SADD 命令会自动忽略已存在的元素
# 所以我们无须在投票之前检查用户是否已经投票
self.client.sadd(self.key, user)
def undo(self, user):
self.client.srem(self.key, user)
def is_voted(self, user):
if self.client.sismember(self.key, user):
return True
else:
return False
def voted_members(self):
return self.client.smembers(self.key)
# encoding: utf-8
class Tag:
def __init__(self, key, client):
self.key = key
self.client = client
def add(self, *tags):
self.client.sadd(self.key, *tags)
def remove(self, *tags):
self.client.srem(self.key, *tags)
def is_include(self, tag):
return self.client.sismember(self.key, tag)
def get_all(self):
return self.client.smembers(self.key)
def count(self):
return self.client.scard(self.key)
# encoding: utf-8
class Lottery:
def __init__(self, key, client):
self.key = key
self.client = client
def add_player(self, *users):
self.client.sadd(self.key, *users)
def get_all_players(self):
return self.client.smembers(self.key)
def player_count(self):
return self.client.scard(self.key)
def draw(self, n):
return self.client.srandmember(self.key, n)
2.集合运算操作
计算并集,交集和差集
# encoding: utf-8
class ItemFilter:
def __init__(self, client):
self.client = client
self.options = set()
def add_option(self, item_set):
self.options.add(item_set)
def result(self):
return self.client.sinter(*self.options)
def store_result(self, key):
return self.client.sinterstore(key, *self.options)
3.复习
有序集合
按照元素的分值来有序的存储不同的元素
1.有序集合的基本操作
2.范围操作
基于有序集合的排序性质,对处于某种范围之内的多个元素进行操作
# encoding: utf-8
class RankList:
def __init__(self, key, client):
self.key = key
self.client = client
def incr(self, item, increment=1):
# 注意在 redis-py 客户端中
# zincrby() 方法是先给定元素后给定分值
# 这和 ZINCRBY 命令规定的顺序正好相反
self.client.zincrby(self.key, item, increment)
def get_top(self, n, show_score=False):
return self.client.zrevrange(self.key, 0, n-1, withscores=show_score)
# encoding: utf-8
class AutoComplete:
def __init__(self, client):
self.client = client
def feed(self, phrase):
# 对于输入 u'周杰伦' 来说
# 这个 for 循环会执行以下命令:
# ZINCRBY u'auto-complete::周' u'周杰伦' 1
# ZINCRBY u'auto-complete::周杰' u'周杰伦' 1
# ZINCRBY u'auto-complete::周杰伦' u'周杰伦' 1
for i in range(len(phrase)+1):
key = 'auto-complete::' + phrase[:i]
self.client.zincrby(key, phrase, 1)
def get_hint(self, phrase, n):
key = 'auto-complete::' + phrase
result = []
for hint in self.client.zrevrange(key, 0, n-1):
result.append(unicode(hint, 'utf-8'))
return result
3.集合运算操作
计算并存储多个有序集合的交集、并集的运算结果
4.复习
HyperLogLog
使用常量空间估算大量元素的基数
问题:如何记录网站每天获得的独立IP的数量
# encoding: utf-8
class UniqueCounter:
def __init__(self, client, key):
self.client = client
self.key = key
def include(self, element):
self.client.pfadd(self.key, element)
def result(self):
return self.client.pfcount(self.key)
复习
数据库
1.处理数据库中的单个键
查看键的类型、删除键、检查键是否存在、修改键的名字
2.对键的值进行排序
SORT命令以及他们的各个参数及选项
3.获取数据库中的键
随机获取数据库中的某个键,遍历数据库中的所有键