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的演进
Redis的独特性
1.独特的键值对模型
很多数据库只能处理一种数据结构
Redis也是键值对数据库,但和Memcached不同的是,Redis的值不仅可以是字符串,它还可以其他5种数据结构中的任意一种。通过选用不同的数据结构,用户可以使用Redis解决各式各样的问题。
它的键是字符串,关联的值可以是字符串,列表,散列,集合,有序集合,哈希;
2.内存存储,速度极快
硬盘数据库的工作模式:数据存储在硬盘,储存索引在内存
内存数据库的工作模式:数据直接存储在内存中。
两种数据库的工作模式
3.丰富的附加功能
丰富的附加功能
4.良好的支持
5.广泛的使用
广泛的使用
一.字符串键
redis中最简单的数据结构,可以储存文字(比如:"hello world"),数字(比如整数和浮点数),二进制数据
字符串基本操作
1.为字符串键设置值
001set 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里面(也即是内存里面),程序可以以极快的速度取得这些内容。
redis缓存
缓存程序的API及其实现
缓存程序的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
002
索引
索引
范围设值(值的更改)
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
范围取值
数字操作
设置和获取数字
数字参照增加或者减少数字的值
0102
增一或减一
02示例:计数器(counter)
很多网站都使用了计数器来记录页面被访问的次数。
每当用户访问页面时,程序首先将页面访问计数器的值增一,然后将计数器当前的值返回给用户观看,以便用户通过页面的访问次数来判断页面内容的受关注程度。
使用字符串键以及INCR,GET等命令,我们也可以实现这样的计数器。
计数器API及其实现
计数器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,以此类推。
03
id生成器API及其实现
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,命令返回被设置的位原来储存的旧值。
04
获取二进制位的值
getbit key index 返回给定索引上的二进制位的值。复杂度为O(1)。
05
计算值为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这四种操作中的任意一种:
06
07
示例:实现在线人数统计
08
在用户id和位索引之间进行关联
09在线用户统计的API及其实现
10
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)
关于用户在线统计的更多信息
11
示例:使用Redis缓存热门图片
图片网站要储存大量的图片(通常放在硬盘里面),而少部分热门的图片会经常地被访问到。
为了加快网站获取热门图片的速度,我们可以利用Redis能够储存二进制数据这一特性,使用之前构建
缓存程序来缓存图片网站中的热门图片。
12
储存中文时的注意事项
一个英文字符只需要使用单个字节来储存,而一个中文字符却需要使用多个字节来储存。
strlen,setrange和getrange都是为英文设置的,它们只会在字符为单个字节的情况下正常工作,而一旦我们储存的是类似中文这样的多字节字符,那么这三个命令就不在适用了。
13
strlen示例
14
setrange和getrange的示例
15
字符串键的总结
字符串键的总结二.散列键
一个散列由多个域值对(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)。
16
仅当域不存在时,关联域值对
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
批量操作
一次对多个域、多个值或者多个域值对进行操作。
17
获取散列包含的所有域、值、或者域值对
18数字操作
和字符串键的值一样,在散列里面,域的值也可以被解释为数字,并执行相应的数字操作。
对域的值执行自增操作
1920
使用散列的好处
(1)将数据放到同一个地方,而并不是直接分散地储存在整个数据库里面,这不仅方便数据管理,还避免误操作的发生。
21
(2)避免键名冲突
22
23
(3)减少内存占用
在一般情况下,保存相同数量的键值对信息,使用散列键比使用字符串键更节约内存。
因为在数据库里面创建的每一个键都会带有数据库附加的管理信息(比如这个键的类型、最后一次被访问的时间等等),所以数据库里面的键越多,服务器在储存附加管理信息方面耗费的内存就越多,花在管理数据库键上的CPU也会越多。
除此之外,当散列包含的域值对数量比较少的时候,Redis会自动使用一种占用内存非常少的数据结构来做散列的底层实现,在散列的数量比较多的时候,这一措施对减少内存有很大的帮助。
结论
23示例:使用散列重新实现计数器
24
# 保存所有计数器的散列键
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)
列表
以有序的方式储存多个可重复的值
01
1.推入和弹出操作
了解如何向列表添加项,及如何从列表里面删除项
02
03
04
05
06
07
2.长度、索引和范围操作
0102
03
3.列表示例
0102
03
04
# 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)
05
4.插入和删除操作
0102
03
04
05
06
07
08
# 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)
09
5.阻塞式弹出操作
0102
03
04
05
06
07
08
09
# 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):
a
b
6复习
a集合
储存多个各不相同的元素
a
1.元素操作
ab
c
d
e
f
g
h
image.png
# 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)
image.png
image.png
# 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)
image.png
image.png
image.png
image.png
image.png
image.png
# 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)
image.png
2.集合运算操作
计算并集,交集和差集
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
# 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)
image.png
3.复习
有序集合
按照元素的分值来有序的存储不同的元素
image.png
image.png
1.有序集合的基本操作
image.pngimage.png
image.png
image.png
image.png
image.png
image.png
2.范围操作
基于有序集合的排序性质,对处于某种范围之内的多个元素进行操作
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
# 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)
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
# 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
image.png
3.集合运算操作
计算并存储多个有序集合的交集、并集的运算结果
image.png
image.png
image.png
image.png
4.复习
image.pngHyperLogLog
使用常量空间估算大量元素的基数
问题:如何记录网站每天获得的独立IP的数量
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
# 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)
image.png
image.png
复习
image.png数据库
image.pngimage.png
1.处理数据库中的单个键
查看键的类型、删除键、检查键是否存在、修改键的名字
image.png
image.png
image.png
image.png
image.png
image.png
2.对键的值进行排序
SORT命令以及他们的各个参数及选项
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
3.获取数据库中的键
随机获取数据库中的某个键,遍历数据库中的所有键
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
网友评论