文章目录
- 1.redis持久化机制
- 2.缓存问题
- 3.redis数据淘汰策略
- 4.redis数据类型和应用场景
- 5.redis数据类型对象
- 6.五大数据类型底层实现
- 6.redis-HyperLogLog基数统计
- 7.redis分布式锁方案
- 8.Memcache与Redis的区别
- 9.单线程的redis为什么这么快
- 10.高并发知识点
- 11.Redis事务
- 12.redis协议规范(RESP)
- 13.redis的持久化开启了RDB和AOF下重启服务是如何加载的?
- 14.redis位操作:用于bitmap
- 15.10万用户一年365天的登录情况如何用redis存储,并快速检索任意时间窗内的活跃用户?
- 16.100万并发4G数据,10万并发400G数据,如何设计Redis存储方式?
- 17.redis AKF/CAP如何实现和设计?
- 18.redis哨兵模式(过半机制)
- 19.redis如果做集群该如何规划?
- 20.缓存一致性
- 21.lua
- 22.redis 过期键删除策略
- 23.keys与scan命令使用的场景
- 24.在Redis中如何实现延时队列?
- 25.安全队列:RPOPLPUSH
1.redis持久化机制
1)实现:单独创建fork()一个子进程,将当前父进程的数据库数据复制到子进程的内存中,
然后由子进程写入到临时文件中,持久化的过程结束了,
再用这个临时文件替换上次的快照文件,然后子进程退出,内存释放。
2)RDB(默认):按照一定的时间周期策略把内存的数据以快照的形式保存到硬盘的二进制文件。
对应产生的数据文件为dump.rdb,通过配置文件中的save参数来定义快照的周期。
(快照可以是其所表示的数据的一个副本,也可以是数据的一个复制品。)
(体积小,速度快,丢数据)
3)AOF:Redis会将每一个收到的写命令都通过Write函数追加到文件最后,类似于MySQL的binlog。
当Redis重启是会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。
(体积大,速度慢,数据完整)
4)混合持久化(4.0以后版本支持):同时结合RDB持久化以及AOF持久化混合写入AOF文件。
4)当两种方式同时开启时,数据恢复Redis会优先选择AOF恢复。
2.缓存问题
1)缓存雪崩:缓存雪崩是指原本应该访问缓存的都直接访问底层数据库了,
例如大量缓存数据在同一时间过期,导致大量请求直接访问底层数据库,导致底层数据库崩溃。
解决方案:
(1)缓存数据过期时间分散(过期时间加随机数)
(2)底层数据库访问加锁或者将读写请求放入消费队列
2)缓存穿透:用户查询的数据,数据库中没有,那缓存中也不会有,用户就会绕过缓存直接查询底层数据库,
如果用户大量进行这种操作,比如黑客攻击,就会造成底层数据库崩溃,这种现象成为缓存穿透。
解决方案:
(1)对查询结果为空的情况也进行缓存,缓存时间设置短一点,或者该key对应的数据insert了之后清理缓存。
(2)对一定不存在的key进行过滤。可以把所有的可能存在的key放到一个大的Bitmap中,查询时通过该bitmap过滤。
(3)布隆过滤器:布隆过滤器的原理是,当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,
把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,
则被检元素一定不在;如果都是1,则被检元素很可能在。这就是布隆过滤器的基本思想。
redis的布隆过滤器插件可以很好的解决这个问题
(4)接口层校验,不合法请求直接拒绝
3)缓存击穿:缓存击穿和和缓存雪崩相似,例如某个key的访问量非常大,突然过期了,
导致大量的访问直接打到底层数据库,造成底层数据库崩溃,这种现象成为缓存击穿。
解决方案:
(1)热点数据永不过期
(2)定时脚本,定时更新热点数据
4)缓存预热:为了避免系统上线后大量直接访问底层数据库操作,事先将某些数据放入缓存,以缓解底层数据库压力
3.redis数据淘汰策略
1)volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
2)volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰
3)volatile-lfu:从已设置过期时间的数据集中挑选最不经常使用的数据淘汰
4)volatile-random:从已设置过期时间的数据集中任意选择数据淘汰
5)allkeys-lru:淘汰最近最少使用的数据
6)allkeys-random:从数据集中任意选择数据淘汰
7)allkeys-lfu:移除最不经常使用的数据
8)no-eviction:禁止驱逐数据
# coding: utf-8
# python实现简单的lru算法
import collections
class LRUCache(object):
def __init__(self, size=5):
self.size = size
self.cache = collections.OrderedDict()
def get(self, key):
if key in self.cache:
val = self.cache.pop(key)
self.cache[key] = val
else:
val = None
return val
def set(self, key, val):
if key in self.cache:
val = self.cache.pop(key)
self.cache[key] = val
else:
if len(self.cache) == self.size:
self.cache.popitem(last=False)
self.cache[key] = val
else:
self.cache[key] = val
if __name__ == '__main__':
""" test """
cache = LRUCache(6)
for i in range(10):
cache.set(i, i)
for i in range(10):
import random
data = random.randint(1, 20)
print 'cache', cache.cache.keys()
if cache.get(data):
print 'hit, %s\n' % data
else:
print 'not hit, %s\n' % data
4.redis数据类型和应用场景
1)string:Redis的字符串是动态字符串(SDS),是可以修改的字符串,它的内部结构是一个带长度信息的字节数组,可以包含任何数据。
使用场景:缓存等
2)hash:相当于python中的dict。
使用场景:适合存储json对象等。
3)list:相当于python中的list。
使用场景:消息队列
4)set:相当于python中的set,内部元素唯一,增删改查复杂度都是O(1)操作,支持交并补操作。
使用场景:获取购买相同记录,唯一性等
5)sorted set:有序集合,在set的基础上增加了value的权重,利用跳跃表实现(64层)
使用场景:排行榜
5.redis数据类型对象
redis有五大数据类型,字符串对象(string)、列表对象(list)、哈希对象(hash)、集合(set)对象和有序集合对象(zset),
redis构建了一个对象系统,每个数据类型都是一个对象,由于redis是key,value的形式存储的,
所以每新建一个数据,系统就会新建两个对象,一个是key对象,一个是value对象,每个对象有五个属性,如下:
typedef struct redisObject{
//类型
unsigned type:4;
//编码
unsigned encoding:4;
//指向底层数据结构的指针
void *ptr;
//引用计数
int refcount;
//记录最后一次被程序访问的时间
unsigned lru:22;
}robj
1)type:type属性对应的是对象的类型,可以通过type命令查看当前对象的类型
127.0.0.1:6379> set key1 100
OK
127.0.0.1:6379> type key1
string
2)encoding和ptr:对象的prt指针指向对象底层的数据结构,而数据结构由encoding属性来决定,
可以通过OBJECT ENCODING命令查看当前对象的底层数据结构。
127.0.0.1:6379> set key2 'qwer'
OK
127.0.0.1:6379> OBJECT ENCODING key2
"embstr"
3)refcount:对象的refcount属性主要用于统计当前对象被引用次数,用来释放内存。
对象被引用一次,refcount加一,当对象refcount为0时,当前对象占用的内存会被释放。
用OBJECT REFCOUNT命令查看当前对象被引用次数。
127.0.0.1:6379> set key3 123
OK
127.0.0.1:6379> OBJECT REFCOUNT key3
(integer) 2
127.0.0.1:6379> set key4 123
OK
127.0.0.1:6379> OBJECT REFCOUNT key4
(integer) 3
redis可以看到123这个对象由于被key3和key4引用,所以recount key4值为3,但是为什么refcount key3是2呢,
这里并不是因为之前123被引用过,而是redis在初始化的时候默认创建0~9999的对象,可以看下边的例子。
127.0.0.1:6379> set key5 9999
OK
127.0.0.1:6379> OBJECT REFCOUNT key5
(integer) 2
127.0.0.1:6379> set key6 10000
OK
127.0.0.1:6379> OBJECT REFCOUNT key6
(integer) 1
5)lru:lru属性记录的是该对象最后一次被掉用距离现在的时间间隔,用于释放资源,单位是秒。使用OBJECT IDLETIME调用。
127.0.0.1:6379> set key7 'qwer'
OK
127.0.0.1:6379> OBJECT IDLETIME key7
(integer) 7
127.0.0.1:6379> OBJECT IDLETIME key7
(integer) 9
127.0.0.1:6379> OBJECT IDLETIME key7
(integer) 10
上文说了encoding属性记录了五大数据类型的底层实现的数据结构,接下来详细介绍下redis的底层实现。
redis为每个数据类型都至少提供了两种底层数据结构实现。具体如下表:
redis数据类型 | ENCODING返回的编码 | 底层对应的数据结构 |
---|---|---|
string | int | long类型的整数 |
string | embstr | embstr编码的简单动态字符串 |
string | raw | 简单动态字符串 |
list | ziplist | 压缩列表(数据量小时候用) |
list | linkedlist | 双向链表 |
hash | ziplist | 压缩列表(数据量小时候用) |
hash | ht | 字典 |
set | intset | 整数集合 |
set | ht | 字典 |
zset | ziplist | 压缩列表(数据量小时候用) |
zset | skiplist | 跳表 |
6.五大数据类型底层实现
1)字符串对象(string):redis中的字符串是最常用的数据类型,所有的key都是字符串类型,
redis底层提供了三种不同的数据结构实现字符串对象,根据不同的数据自动选择合适的数据结构。
(1)int:当数据是long类型的整数字符串时,底层使用long类型的整数实现。
这个值会直接存储在字符串对象的ptr属性中,同时OBJECT ENCODING为int。
(2)raw:当数据为长度大于44字节的字符串时,底层使用简单动态字符串(SDS)实现。SDS有三个属性,free,len和buf。
struct sds {
char buf[]; // 字符数组,用于保存字符的二进制形式
int len; // 记录buf数组中已使用的长度,等于SDS所保存字符串的长度
int free; // 记录buf数组中未使用的长度
};
sds更新逻辑
a.sds通过len和free来动态控制和记录buf list
b.当存储的字符串大小小于1MB时,分配的free和len相等
c.当存储的字符串大小大于等于1MB时,free=1MB
d.当需要增加的字符串长度小于free时,直接添加,不会为free重新分配空间
e.注意会有结尾1 byte空字符
sds优点
a.字符串改变时减少重新分配内存空间,提高效率
b.惰性空间释放,字符串缩短并不会立即减小free,以备将来使用。
c.二进制安全,buf中存的时字符的二进制数组,因此避免了中间是空字符的影响。
(3)embstr:当数据为长度小于44字节的字符串时,底层使用embstr编码的简单动态字符串实现。
相比于raw,embstr内存分配只需要一次就可完成,分配的是一块连续的内存空间。
127.0.0.1:6379> set key1 1234
OK
127.0.0.1:6379> OBJECT ENCODING key1
"int"
127.0.0.1:6379> set key2 qwer
OK
127.0.0.1:6379> OBJECT ENCODING key2
"embstr"
127.0.0.1:6379> set key3 abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz
OK
127.0.0.1:6379> OBJECT ENCODING key3
"raw"
2)列表对象(list):redis中的列表对象经常被用作消息队列使用,底层采用ziplist和linkedlist实现。
(1)ziplist
列表对象使用ziplist编码需要满足两个要求,一是所有字符串长度都小于设定值值64字节(可以在配置文件中修改list-max-ziplist-value字段改变)。
二是所存元素量小于设定值512个(可以在配置文件中修改list-max-ziplist-entries字段改变)。ziplist类似与python中的list,
占用一段连续的内存地址,由此减小指针内存占用。
zlbytes:占内存总数
zltail:到尾部的偏移量
zllen:内部节点数
node:节点
zlend:尾部标识
previous_entry_length:前一节点的长度
encoding:数据类型
content:真实数据
遍历的时候会根据zlbytes和zltail直接找到尾部节点nodeN,然后根据每个节点的previous_entry_length反向遍历。
增加和删除节点会导致其他节点连锁更新,因为每个节点都存储了前一节点的长度。
(2)linkedlist
linkedlist有三个属性,head,tail和len。head指向链表的头部,tail指向链表的尾部,len为链表的长度。
3)哈希对象(hash)
redis中的hash对象底层有两种编码方式,ziplist和hashtable,当元素数量小于512个(可在配置文件中hash-max-ziplist-entries字段配置),
并且所有元素长度都小于64字节时(可在配置文件中hash-max-ziplist-value字段配置)底层才使用ziplist实现,其余时候都使用hashtable实现。
ziplist上文已经介绍过了,redis中的hashtable采用链地址法解决hash冲突,同时超过阈值0.75则扩容一倍渐进式rehash,具体步骤如下。
a.初始化一个hashtable ht1,长度为n,rehash标记位rehashidx=-1,设定阈值0.75(表示如果存储的数据超过初始值的75%,则将rehashidx置为0进行rehash)
b.依次向hash表中插入数,当达到75%时,初始化hashtable ht2=null,ht2的长度2n, rehashidx=0
c.当再次进行增加元素操作时,先将ht1中索引为rehashidx上的元素rehash(此时的hash函数改变,通过掩码)插入ht2,同时rehashidx+1,然后将新增加的元素添加到ht2
d.其余删改插入操作类似,直到ht1种的元素都转移到ht2中
e.ht2改为ht1,原来的ht1改为ht2,并置为null,rehashidx=-1
4)集合对象(set)
redis中的集合对象底层有两种实现方式,分别有整数集合和hashtable。当所有元素都是整数且元素数小于512(可在配置文件中set-max-intset-entries字段配置)
时采用整数集合实现,其余情况都采用hashtable实现。hashtable请移驾上文链接查阅,接下来介绍整数集合intset。intset有三个属性,
encoding:记录数字的类型,有int16,int32和int64等,length:记录集合的长度,content:存储具体数据。具体结构如下图:
5)有序集合对象(zset)
redis中的有序集合底层采用ziplist和skiplist跳表实现,当所有字符串长度都小于设定值值64字节(可以在配置文件中修改list-max-ziplist-value字段改变),
并且所存元素数量小于设定值512个(可以在配置文件中修改list-max-ziplist-entries字段改变)使用ziplist实现,其他情况均使用skiplist实现,
跳跃表的实现原理这里偷个懒,给大家推荐一篇写的非常好的博客(https://blog.csdn.net/u013709270/article/details/53470428)。
L1层的分数是有序的,相当于以分数为索引建立的
插入、删除、查找以及迭代输出有序序列这几个操作,红黑树也可以完成,时间复杂度跟跳表是一样的。
但是,按照区间来查找数据这个操作,红黑树的效率没有跳表高。
6.redis-HyperLogLog基数统计
redis在2.8.9版本添加了HyperLogLog数据结构,用于统计非重复数据个数,这个功能利用redis的set也能实现,但是为什么还要提供HyperLogLog呢?
原因在于如果有时我们只需要计算基数个数,不要存储具体的数据时,数据量很大,如果利用set会浪费很大的内存消耗,但是HyperLogLog的优点是,
他所需要的内存空间不会随着数据量的增大而增大,当统计元素很少时,HyperLogLog占用的内存空间也很小,
当统计的数据很大时,HyperLogLog最大只会占用12k的内存空间。
基数:不重复的元素
HyperLogLog提供三个函数:
PFADD:添加指定元素到 HyperLogLog 中
PFCOUNT:返回给定 HyperLogLog 的基数估算值
PFMERGE:将多个 HyperLogLog 合并为一个 HyperLogLog
PFADD和PFCOUNT演示:
127.0.0.1:6379> PFADD hyper 'red'
(integer) 1
127.0.0.1:6379> PFADD hyper 'bule'
(integer) 1
127.0.0.1:6379> PFADD hyper 'yellow'
(integer) 1
127.0.0.1:6379> PFADD hyper 'red'
(integer) 0
127.0.0.1:6379> PFCOUNT hyper
(integer) 3
PFMERGE演示:
127.0.0.1:6379> PFADD h1 'red'
(integer) 1
127.0.0.1:6379> PFADD h1 'blue'
(integer) 1
127.0.0.1:6379> PFADD h1 'red'
(integer) 0
127.0.0.1:6379> PFADD h2 'yellow'
(integer) 1
127.0.0.1:6379> PFADD h2 'black'
(integer) 1
127.0.0.1:6379> PFMERGE h3 h1 h2
OK
127.0.0.1:6379> PFCOUNT h3
(integer) 4
7.redis分布式锁方案
redis锁的使用方式一般有三种,INCR,SETNX,SET。
1)INCR:INCR命令会将key的值加一,如果key值不存在,则key值会被初始化为0,然后执行INCR操作。
127.0.0.1:6379> GET LOCK_1234
(nil)
127.0.0.1:6379> INCR LOCK_1234
(integer) 1
127.0.0.1:6379> GET LOCK_1234
"1"
结合python实际使用
(1)创建程序中需要加锁的key的关联key值,可以在原key_name前加特定字符实现。如要加锁的key_name为1234,
则关联key_name为LOCK_1234。
(2)客户端A执行INCR LOCK_1234操作,如果为1,则无其他客户端使用,先设置过期时间避免程序异常影响其他程序使用此key。
如果值不为1,则有其他客户端在占用key,等一会再次访问,直到值为0。
(3)客户端A处理完逻辑后删除LOCK_1234。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import redis
import time
rdb_host = '127.0.0.1'
rdb_port = 6379
rdb_pwd = 'root'
rdb_db = 13
db = redis.ConnectionPool(
host=rdb_host,
port=rdb_port,
password=rdb_pwd,
db=rdb_db)
rdb = redis.Redis(connection_pool=db)
def redis_lock(order_id):
lock_key = 'LOCK_' + str(order_id)
incr = rdb.incr(lock_key)
if incr == 1:
rdb.expire(lock_key, 10)
print 'lock success'
rdb.delete(lock_key)
else:
print 'current key has been occupied'
time.sleep(1)
redis_lock(order_id)
if __name__ == '__main__':
redis_lock(1234)
2)SETNX(SET is Not eXists):当key不存在时可以为key设置值,返回1,否则返回0。
127.0.0.1:6379> GET LOCK_2345
(nil)
127.0.0.1:6379> SETNX LOCK_2345 lock
(integer) 1
127.0.0.1:6379> SETNX LOCK_2345 lock
(integer) 0
127.0.0.1:6379> SETNX LOCK_2345 abc
(integer) 0
利用SETNX实现锁操作和上边的INCR类似,具体实现如下:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import redis
import time
rdb_host = '127.0.0.1'
rdb_port = 6379
rdb_pwd = 'root'
rdb_db = 13
db = redis.ConnectionPool(
host=rdb_host,
port=rdb_port,
password=rdb_pwd,
db=rdb_db)
rdb = redis.Redis(connection_pool=db)
def redis_lock_setnx(order_id):
lock_key = 'LOCK_' + str(order_id)
setnx = rdb.setnx(lock_key, 'lock')
if setnx == 1:
rdb.expire(lock_key, 10)
print 'lock success'
rdb.delete(lock_key)
else:
print 'current key has been occupied'
time.sleep(1)
redis_lock_setnx(order_id)
if __name__ == '__main__':
redis_lock_setnx(2345)
3)SET:Redis从2.6.12版本开始, SET命令的行为可以通过一系列参数来修改
(1)EX:设置键的过期时间为 second 秒。
(2)PX:设置键的过期时间为 millisecond 毫秒。
(3)NX:只在键不存在时,才对键进行设置操作。
(4)XX:只在键已经存在时,才对键进行设置操作。
可以通过NX参数和EX参数实现锁操作,代码如下:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import redis
import time
rdb_host = '127.0.0.1'
rdb_port = 6379
rdb_pwd = 'root'
rdb_db = 13
db = redis.ConnectionPool(
host=rdb_host,
port=rdb_port,
password=rdb_pwd,
db=rdb_db)
rdb = redis.Redis(connection_pool=db)
def redis_lock_set(order_id):
lock_key = 'LOCK_' + str(order_id)
set = rdb.set(lock_key, 'lock', ex=10, nx=True)
if set:
print 'lock success'
rdb.delete(lock_key)
else:
print 'current key has been occupied'
time.sleep(1)
redis_lock_set(order_id)
if __name__ == '__main__':
redis_lock_set(3456)
4)以上方式存在的问题
以上方式都设置了过期时间,原因在于如果程序由于某些bug以外退出了,不加过期时间的话,这个key会一直被锁定,无法更新。
但是加过期时间来处理就会有问题,如果客户端1在设置的过期时间内程序正常执行,但是没有处理完,这时过期时间失效了,然后这个key的锁被客户端2获取,
在客户端2还没处理完的时候客户端1处理完了,然后删除了客户端2的锁。
针对这个问题可以在给锁赋值的时候增加随机字符,然后删除的时候判断下是否为自己赋的值就可以了。具体实现如下:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import redis
import time
rdb_host = '127.0.0.1'
rdb_port = 6379
rdb_pwd = 'root'
rdb_db = 13
db = redis.ConnectionPool(
host=rdb_host,
port=rdb_port,
password=rdb_pwd,
db=rdb_db)
rdb = redis.Redis(connection_pool=db)
def redis_lock_set_random(order_id):
lock_key = 'LOCK_' + str(order_id)
lock_value = 'lock' + str(int(time.time()))
set = rdb.set(lock_key, lock_value, ex=10, nx=True)
if set:
print 'lock success'
if rdb.get(lock_key) == lock_value:
rdb.delete(lock_key)
else:
print 'current key has been occupied'
time.sleep(1)
redis_lock_set_random(order_id)
if __name__ == '__main__':
redis_lock_set_random(4567)
8.Memcache与Redis的区别
1)存储方式Memecache把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小。Redis有部份存在硬盘上,redis可以持久化其数据。
2)数据支持类型memcached所有的值均是简单的字符串,redis作为其替代者,支持更为丰富的数据类型,提供list,set,zset,hash等数据结构的存储。
3)value值大小不同:Redis最大可以达到512M;memcache只有1M。
4)redis的速度比memcached快很多。
5)Redis支持数据的备份,即master-slave模式的数据备份。
9.单线程的redis为什么这么快
1)纯内存操作
2)redis主要工作是单线程,避免了频繁的上下文切换,但并不是完全的单线程,4.0后有辅助线程:删除大key(UNLINK),flushdb
3)采用了非阻塞I/O多路复用机制
4)底层数据结构优化
5)持久化策略
10.高并发知识点
1)阻塞I/O:进程发起read后,如果kernel未准备好数据,进程就会被block,进入等待阶段;等待kernel将数据准备好,才会返回数据,解除阻塞;
2)非阻塞I/O:进程发起read请求,kernel未准备好数据,直接返回一个error,进程无需阻塞等待;
进程会轮询发送read请求,如此往复;一直到kernel准备好数据,才把数据拷贝并返回给进程,结束轮询;
3)I/O多路复用:进程调用select/poll/epoll,select/poll/epoll会将进程block起来;kernel会‘监视’所有select负责的socket;
当任何一个socket准备好数据,select就会返回(也就是图中的return medable);进程调用read(图中第二次system call),将数据从kernel拷贝到用户进程;
I/O多路复用主要的三种实现方式:[具体查看](http://note.youdao.com/s/JxcDG01S)
(1)select:不是线程安全;只返回数据,不会告知是哪个socket返回的,只能自己遍历去查找;只能监视1024个链接。
(2)poll:相对于select改进了:可监视无数个链接,缺点:仍旧不是线程安全。
(3)epoll:改进:线程安全,只有linux支持。
11.Redis事务
Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的
1)MULTI命令用于开启一个事务,它总是返回OK。 MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。
2)EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。 当操作被打断时,返回空值 nil 。
3)通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务,并且客户端会从事务状态中退出。
4)WATCH 命令可以为 Redis 事务提供 check-and-set (CAS)行为。 可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。
12.redis协议规范(RESP)
1)简单字符串 Simple Strings, 以 "+"加号 开头
eg: "+OK\r\n"
2)错误 Errors, 以"-"减号 开头
eg: "-Error unknow command 'foobar'\r\n"
3)整数型 Integer, 以 ":" 冒号开头
eg: ":1000\r\n"
4)大字符串类型 Bulk Strings, 以 "$"美元符号开头,长度限制512M
eg: "$6\r\nfoobar\r\n" 其中字符串为 foobar,而6就是foobar的字符长度
5)数组类型 Arrays,以 "*"星号开头
eg: "*2\r\n$2\r\nfoo\r\n$3\r\nbar\r\n" 数组包含2个元素,分别是字符串foo和bar
13.redis的持久化开启了RDB和AOF下重启服务是如何加载的?
1) AOF持久化开启且存在AOF文件时,优先加载AOF文件,
2) AOF关闭或者AOF文件不存在时,加载RDB文件,
3) 加载AOF/RDB文件成功后,Redis启动成功。
4) AOF/RDB文件存在错误时,Redis启动失败并打印错误信息
14.redis位操作:用于bitmap
1)setbit:将key的第几位设置为1或者0
127.0.0.1:6379> setbit hou 2 1
(integer) 0
127.0.0.1:6379> setbit hou 5 1
(integer) 0
127.0.0.1:6379> setbit hou 7 1
(integer) 0
# 此时hou=10100100
2)getbit:获取key的第几位的值
127.0.0.1:6379> getbit hou 7
(integer) 1
3)bitcount:统计key所有位共有几位为1
10.0.10.26:6379[12]> bitcount hou
(integer) 3
4)bitfield:统计key前n位值为1的个数,u表示无符号整形,i表示有符号
127.0.0.1:6379> bitfield hou get u6 1
(integer) 2
127.0.0.1:6379> bitfield hou get u8 1
(integer) 3
5)bitpos:获取key第一位为指定值的索引
127.0.0.1:6379> bitpos hou 0
(integer) 0
127.0.0.1:6379> bitpos hou 1
(integer) 2
6)bitop:将两个bitmap key进行逻辑运算,结果存入res中
127.0.0.1:6379> setbit hou 2 1
(integer) 0
127.0.0.1:6379> setbit hou 5 1
(integer) 0
127.0.0.1:6379> setbit hou 7 1
(integer) 0
127.0.0.1:6379> setbit yao 1 1
(integer) 0
127.0.0.1:6379> setbit yao 2 1
(integer) 0
127.0.0.1:6379> bitop and res hou yao
(integer) 1
127.0.0.1:6379> bitcount res
(integer) 1
# hou=10100100
# yao=00000110
# hou and yao = 00000100
15.10万用户一年365天的登录情况如何用redis存储,并快速检索任意时间窗内的活跃用户?
1)构造用户uid和bitmap偏移量的map
2)以日期天为单位作为key,第一步map中uid对应的偏移量为value建立bitmap
eg: setbit 20201204 24 1 (2020年12月4号,24号用户登陆了)
3)redis bitcount统计
eg: bitcount 20201204
4)key最多365个,每个bitmap最大长度10万
16.100万并发4G数据,10万并发400G数据,如何设计Redis存储方式?
1)前者用主从+哨兵进行高可用,加快读请求的速度,减轻单节点的压力。(数据全存redis)
2)后者用集群来均分这400G数据,或者redis存数据索引,数据物理地址。(数据索引存redis)
17.redis AKF/CAP如何实现和设计?
1)AKF:从x,y,z三个维度进行考虑
(1)x轴:在x轴方向上,做N个主机的全量镜像数据的副本,主redis与这些副本的关系为主从。主机可以对外提供read / write ,从机可以对外提供read(读写分离)。
结合高可用, 可以解决单点故障和容量瓶颈的问题,只是解决了 read 的压力,而没有解决 write 的压力。
(2)y轴:在y轴方向上,可以把之前一台redis中的数据按照业务功能来拆分成不同的redis实例存储,并且每个redis实例都可以再次做x轴的镜像副本进行读写分离,
当然,x轴和y轴之间不是必须要结合使用。y轴的拆分解决了容量瓶颈问题和数据访问压力的问题。
(3)z轴:如果y轴的某个redis实例过于臃肿,还可以把这个redis实例进行z轴的拆分,也就是把这个redis实例里面的数据按照一定规则查分。
比如:取模,优先级等规则再次查分成多个redis,使得不同的数据出现在固定的redis里
2)CAP:CAP理论指的是一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三项中的两项
(1)主备:Client只能访问master,不会访问slave。slave就是为了master挂了后接替master,使Client能够从新的master拿到数据,slave是不会参与业务的。
(2)主从:Client可以访问master,也可以访问slave。
18.redis哨兵模式(过半机制)
1)当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。
这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。
2)假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线。
当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover操作。
切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。这样对于客户端而言,一切都是透明的。
(1)定时任务:每个哨兵节点维护了3个定时任务。定时任务的功能分别如下:
通过向主从节点发送info命令获取最新的主从结构;
通过发布订阅功能获取其他哨兵节点的信息;
通过向其他节点发送ping命令进行心跳检测,判断是否下线。
(2)主观下线:在心跳检测的定时任务中,如果其他节点超过一定时间没有回复,哨兵节点就会将其进行主观下线。
顾名思义,主观下线的意思是一个哨兵节点“主观地”判断下线;与主观下线相对应的是客观下线。
(3)客观下线:哨兵节点在对主节点进行主观下线后,会通过sentinelis-master-down-by-addr命令询问其他哨兵节点该主节点的状态;
如果判断主节点下线的哨兵数量达到一定数值,则对该主节点进行客观下线。需要特别注意的是,客观下线是主节点才有的概念;
如果从节点和哨兵节点发生故障,被哨兵主观下线后,不会再有后续的客观下线和故障转移操作。
(4)选举领导者哨兵节点:当主节点被判断客观下线以后,各个哨兵节点会进行协商,选举出一个领导者哨兵节点,并由该领导者节点对其进行故障转移操作。
(5)故障转移:选举出的领导者哨兵,开始进行故障转移操作,该操作大体可以分为3个步骤:
(A)在从节点中选择新的主节点:选择的原则是,首先过滤掉不健康的从节点;然后选择优先级最高的从节点(由slave-priority指定);
如果优先级无法区分,则选择复制偏移量最大的从节点;如果仍无法区分,则选择runid最小的从节点。
(B)更新主从状态:通过slaveof no one命令,让选出来的从节点成为主节点;并通过slaveof命令让其他节点成为其从节点。
(C)将已经下线的主节点(即6379)设置为新的主节点的从节点,当6379重新上线后,它会成为新的主节点的从节点。
19.redis如果做集群该如何规划?
1)主从模式(包含主备,看17条第二点)
2)哨兵模式(看18条)
3)Cluster模式(hash slot算法)[详情点击](https://www.cnblogs.com/myseries/p/10959050.html)
(1)sentinel模式基本可以满足一般生产的需求,具备高可用性。但是当数据量过大到一台服务器存放不下的情况时,主从模式或sentinel模式就不能满足需求了,
这个时候需要对存储的数据进行分片,将数据存储到多个Redis实例中。cluster模式的出现就是为了解决单机Redis容量有限的问题,将Redis的数据根据一定的规则分配到多台机器。
(2)redis cluster集群是去中心化的,每个节点都是平等的,连接哪个节点都可以获取和设置数据。
(3)平等指的是master节点,因为slave节点根本不提供服务,只是作为对应master节点的一个备份。
(4)redis cluster有固定的16384个hash slot,
每个节点都会记录哪些槽指派给了自己,哪些槽指派给了其他节点。
客户端向节点发送键命令,节点要计算这个键属于哪个槽。
如果是自己负责这个槽,那么直接执行命令,如果不是,向客户端返回一个MOVED错误,指引客户端转向正确的节点。
4)一致性hash [详情点击](https://www.bilibili.com/video/BV1Hs411j73w?from=search&seid=17985925375220854636)
增加虚拟节点解决hash偏斜
5)数据一致性算法:raft
(1)leader,follower,condition
(2)从follower状态开始,每个节点维护一个随机的选举时间,如果超过时间还未收到leader的信息,则发起选举,如果收到信息,则时间重置
(3)采用半数通过机制选举leader
(4)数据同步,client-->leader-->follower
follower-->leader 确认
leader-->follower 确认
leader写入
6)数据一致性算法:paoxs
20.缓存一致性
[缓存一致性](https://www.cnblogs.com/liuqingzheng/p/11080680.html)
1)先更新数据库,再更新缓存:线程不安全
(1)线程A更新了数据库
(2)线程B更新了数据库
(3)线程B更新了缓存
(4)线程A更新了缓存
这就出现请求A更新缓存应该比请求B更新缓存早才对,但是因为网络等原因,B却比A更早更新了缓存。这就导致了脏数据,因此不考虑。
2)先删除缓存,再更新数据库
(1)请求A进行写操作,删除缓存
(2)请求B查询发现缓存不存在
(3)请求B去数据库查询得到旧值
(4)请求B将旧值写入缓存
(5)请求A将新值写入数据库
上述情况就会导致不一致的情形出现。而且,如果不采用给缓存设置过期时间策略,该数据永远都是脏数据。
3)先更新数据库,再删除缓存
(1)缓存刚好失效
(2)请求A查询数据库,得一个旧值
(3)请求B将新值写入数据库
(4)请求B删除缓存
(5)请求A将查到的旧值写入缓存
如果发生上述情况,确实是会发生脏数据。
所以,缓存一致性不可能100%达到,只能尽量保证,同时以上必须都配置缓存过期时间使用
21.lua
redis执行lua脚本为原子性,也是redisson底层实现的方式
22.redis 过期键删除策略
1)定时删除:在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即对键进行删除操作。
优点:对内存友好,通过定时器可以保证过期键过期键会尽可能快的删除,并释放过期键占用的空间。
缺点:
(1)cpu不友好,在过期键比较多的情况下,删除过期键可能会占用相当一部分cpu时间;
在内存不紧张cpu紧张的情况下,将cpu时间用在删除和当前任务无关的过期键上,无疑会对服务器响应时间和吞吐量造成影响。
(2)创建定时器需要Redis服务器中的时间事件,而现在时间事件的实现方式是无序链表,查找一个事件的时间复杂度为O(N),并不能高效的处理大量时间事件。
2)惰性删除:放任键过期不管,但每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键。如果没有过期,就返回该键。
优点:对cpu友好,程序只在取出键时才对建进行过期检查,删除的目标仅限于当前处理的键。
缺点:对内存不友好,当数据库中有大量的过期键,而这些键又没有被访问到,那么他们也许会永远不会被删除。
3)定期删除:每隔一段时间,程序对数据库进行一次检查,删除里面的过期键。至于删除多少过期键,以及检查多少数据库,有算法决定。
(1)如果删除操作太过频繁,或者执行时间太长,定期删除策略就会退化成定时删除策略。
(2)如果删除执行得太少,或者执行时间太短,定期删除策略又会和惰性删除策略一样,出现浪费内存现象
23.keys与scan命令使用的场景
问:假如Redis里面有10亿个key,其中有100w个key是以某个固定前缀开头的,如何将它们全部找出来?
答:使用keys指令可以扫出指定模式的key列表。
问:如果这个Redis正在给线上的业务提供服务,那使用keys指令会有什么问题?
答:由于Redis是单线程的,所以当执行keys命令后会导致线上服务停顿,直到指令执行完毕,服务才能恢复。
问:如何解决这个问题?
答:这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,
在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。
总结:在线系统一定不要使用keys命令。
24.在Redis中如何实现延时队列?
使用sortedset,拿时间戳作为score,消息内容作为key调用zadd来生产消息
10.0.10.26:6379[9]> ZRANGE orders 0 10 withscores
1) "order_id1"
2) "100"
3) "order_id2"
4) "200"
5) "order_id3"
6) "300"
1)消费前先对orders加锁,防止被重复消费,可用前边第七点讲的分布式锁方案加锁,也可以用redis事务解决
2)消费
10.0.10.26:6379[9]> ZRANGE orders 0 0 withscores
1) "order_id1"
2) "100"
10.0.10.26:6379[9]> ZREM orders 'order_id1'
(integer) 1
3)释放锁
25.安全队列:RPOPLPUSH
使用 RPOPLPUSH 获取消息时,RPOPLPUSH 会把消息返给客户端,同时把该消息放入一个备份消息列表,
并且这个过程是原子的,可以保证消息的安全。当客户端成功的处理了消息后,就可以把此消息从备份列表中移除了。
如果客户端因为崩溃的原因没有处理某个消息,那么就可以从备份列表destination中重新获取并处理这个消息。