问题1:Redis的基本介绍
1.Redis的特点
- 开源
- 多种数据结构

现在也有 BitMaps:位图、HyperLogLog:超小内存唯一值计数、GEO:地理信息定位(属于衍生)
-
基于键值的存储服务系统
-
高性能10W/s 读写,数据存在内存中、使用C语言、单线程(非阻塞IO,避免线程切换和竞态消耗)
-
功能丰富(发布订阅、事务、Lua脚本、pipeline)
-
持久化(Redis所有数据保持在内存中,对数据的更新将异步地保存到磁盘上。)
-
高可用、分布式
-
支持多种编辑语言
2.Redis的应用场景
- 缓存系统
- 排行榜
- 计数器
- 社交网络
- 消息队列系统
- 实时系统
3.Redis的文件说明
redis-server:Redis服务器
redis-cli:Redis命令行客户端
redis-benchmark:Redis性能测试
redis-check-aof:AОF文件修复工具
redis-check-dump:RDB文件检查工具
redis-sentinel:Sentinel服务器
4.Redis常用配置
daemonize:是否是守护进程(nolyes)
port:Redis对外端口号
logfile:Redis系统日志
dir:Redis工作目录
问题2:API的理解和使用
1.数据结构与内部编码

2.通用命令
keys #遍历所有key(支持正则表达式,一般不在生产环境使用) O(n)
dbsize #计算key的总数,O(1)
exists key #检查key是否存在
del key #删除指定key-value
expire key seconds #key在seconds秒后过期
ttl key #查看key剩的过А时间
(-2代表key已经不存在了)
persist key #去掉key的过期时间
-1(-1代表key存在,并且没有过期时间。)
type key #返回key的类型
3.字符串类型
缓存、分布式锁、计数器(单线程,天然成立)
1.get key#获取key对应的value
mget #批量获取(传输1次)
2.set key value#不管key是否存在,都设置
setnx key value#key不存在,才设置
set key value xx#key存在,才设置
mset #批量设置 O(n)
3.del key#删除key-value
4.incr key #key自增1,如果key不存在,自增后get(key)=1
decr key #key自减1,如果key不存在,自减后get(key)=-1
incrby key k #key自增k,如果key不存在,自增后get(key)-k
decr key k #key自减k,如果key不存在,自减后get(key)=-k
5.getset key newvalue #set key newvalue并返回旧的value
append key value #将value追加到旧的value
strlen key #返回字符串的长度(注意中文)
incrbyfloat key 3.5 #增加key对应的值3.5
getrange key start end #获取字符串指定下标所有的值
setrange key index value #设置指定下标所有对应的值
tips:计算机的字符与编码集
ASCII码 使用7个bits就可以完全表示ASCI码,包含95个可打印字符、33个不可打印字符(包括控制字符)
Extended ASCII码 使用8个bits,主要为了扩展不同地区的符号,如数学运算符
中文编码集--GB2312 一共收录了7445个字符包括,6763个汉字和682个其它符号,不符合国际的标准
中文编码集--GBK 向下兼容GB2312,向上支持国际ISO标准,收录了21003个汉字,支持全部中日韩汉字·,汉字占2个字节
兼容全球的字符集:Unicode Unicode:统一码、万国码、单一码Unicode定义了世界通用的符号集,UTF-*实现了编码,UTF-8以字节为单位对Unicode进行编码,(字符集定义了文字和二进制的对应关系,为字符分配了唯一的编号,而字符编码规定了如何将文字的编号存储到内存中。)UTF-8 使用一至四个字节为每个字符编码
有的编码方式采用 1~n 个字节存储,是变长的,例如 UTF-8、GB2312、GBK 等;如果一个字符使用了这种编码方式,我们就将它称为多字节字符,或者窄字符。
变长带来的问题就是怎么定位字符的起止:1) 一是从字符集本身下手,在设计字符集时,刻意让不同的字符编号有不同的特征。2) 二是从字符编号下手,可以设计一种转换方案,字符编号在存储之前先转换为有特征的、容易定位的编号,读取时再按照相反的过程转换成字符本来的编号。
有的编码方式是固定长度的,不管字符编号大小,始终采用 n 个字节存储,例如 UTF-32、UTF-16 等;如果一个字符使用了这种编码方式,我们就将它称为宽字符。
-
UFT-8:一种变长的编码方案,使用 1~6 个字节来存储;
UTF-8 的编码规则很简单:如果只有一个字节,那么最高的比特位为 0;如果有多个字节,那么第一个字节从最高位开始,连续有几个比特位的值为 1,就使用几个字节编码,剩下的字节均以 10 开头。只有 UTF-8 兼容 ASCII,UTF-32 和 UTF-16 都不兼容 ASCII,
-
UFT-32:一种固定长度的编码方案,不管字符编号大小,始终使用 4 个字节来存储;所以直接存储 Unicode 编号即可,不需要任何编码转换。浪费了空间,提高了效率
-
UTF-16:介于 UTF-8 和 UTF-32 之间,使用 2 个或者 4 个字节来存储,长度既固定又可变。
4.哈希类型
hget key field #获取hash key对应的field的value
hset key field value #设置hash key对应field的value
hdel key field #删除hash key对应field的value
hexists key field #判断hash key是否有field
hlen key #获取hash key field的数量
hexists key field #判断hash key是否有field
hlen key #获取hash key field的数量
hmget key field1 field2...fieldN #批量获取hash key的一批field对应的值
hmset key field1 value1 field2 value2...fieldN valueN #批量设置hash key的一批field value
hgetall key #返回hash key应所有的field和value
hvals key #返回hash key对应所有field的value
hkeys key #返回hash key对应所有field
hsetnx key field value #设置hash key对应field的value(如field已经存在,则失败)
hincrby key field intCounter #hash key对应的field的value自增intCounter
hincrbyfloat key field floatCounter #hincrby浮点数版

5.列表类型
增
rpush key valuel value2...valueN #从列表右端插入值(1-N个)
Ipush key valuel value2..valueN #从列表左端插入值(1-N个)
linsert key beforelafter value newValue #在list指定的值前后插入newValue
删
lpop key #从列表左侧弹出一个item
rpop key #从列表右侧弹出一个item
Irem key count value #根据count值,从列表中删除所有value相等的项 (1)count>0,从左到右,删除最多count个value相等的项 (2)count<0,从右到左,删除最多Math.abs(count)个value相等的项 (3)count-0,删除所有value相等的项
Itrim key start end #按照索引范围修剪列表
改
Iset key index newValue #设置列表指定索引值为newValue
查
Irange key start end(包含end) #获取列表指定索引范围所有item
lindex key index #获取列表指定索引的item
llen key #获取列表长度
阻塞
blpop key timeout #lpop阻塞版本,timeout是阻塞超时时间,timeout=0为永远不阻塞
brpop key timeout #rpop阻塞版本,timeout是阻塞超时时间,timeout-0为永远不阻塞
blpop key timeout #lpop阻塞版本,timeout是阻塞超时时间,timeout-0为永远不阻塞
LRUSH + LPOP = Stack
LPUSH + RPOP = Queue
LPUSH + LTRIM = Capped Collection
LPUSH + BRPOP = Message Queue
6.集合类型
sadd key element #向集合key添加element(如果element已经存在,添加失败)
srem key element #将集合key中的element移除掉
scard user:1:follow=4#计算集合大小
sismember user:1:follow it=1(存在)#判断it是否在集合中
srandmember user:1:follow count=his#从集合中随机挑count个元素
spop user:1:follow=sports#从集合中随机弹出一个元素
smembers user:1:follow=music his sports it#获取集合所有元素
sdiff user:1:follow user:2:follow =music his#差集
sinter user:1:follow user:2:follow =it sports#交集
sunion user:1:follow user:2:follow=it music his sports news ent#并集
sdiff|sinter|suion+store destkey..#将差集、交集、并集结果保存在destkey中

7.有序集合类型
add key score element可以是多对 #添加Score和element
zrem key element(可以是多个) #删除元素
zscore key element #返回元素的分数
zincrby key increScore element #增加或减少元素的分数
zcard key #返回元素的总个数
zrank player:rank ronaldo #获取排名(从0开始,从小到大)
zrange player:rank 0-1 withscores #带分数的范围查询
zcount key minScore maxScore #返回有序集合内在指定分数范围内的个数
zremrangebyrank key start end #删除指定排名内的升序元素
zremrangebyscore key minScore maxScore #删除指定分数内的升序元素
问题3:Jedis

public class JedisTest {
public static void main(String[] args) {
Jedis jedis = new Jedis("192.168.40.131",6382);
jedis.set("hello11", "world11");
String hello = jedis.get("hello11");
System.out.println(hello);
}
}
public class JedisTest {
public static void main(String[] args) {
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
JedisPool jedisPool = new JedisPool(poolConfig, "192.168.40.131", 6382);
Jedis jedis =null;
try {
jedis = jedisPool.getResource();
jedis.set("h111", "gsyzh");
} catch (Exception e) {
e.printStackTrace();
}
finally {
if (jedis != null) {
jedis.close();
}
}
}
}
问题4:Redis的功能
1.慢查询
Redis生命周期

特点
1.先进先出队列 2.固定长度 3.保存在内存内
配置
slowlog-max-len 慢查询阈值(单位:微秒)
slowlog-log-slower-than =0,记录所有命令
(支持动态配置)config set slowlog-max-len 1000、config set slowlog-log-slower-than 1000
命令
1.slowlog get[n]:获取慢查询队列 2.slowlog len:获取慢查询队列长度 3.slowlog reset:清空慢查询队列
运维经验
1.slowlog-max-len不要设置过大,默认10ms,通常设置1ms 2.slowlog-log-slower-than不要设置过小,通常设置1000左右。 3.定期持久化慢查询。
2.pipeline

public class JedisTest {
public static void main(String[] args) {
long a = System.currentTimeMillis();
Jedis jedis = new Jedis("192.168.40.131", 6382);
for (int i = 0; i < 10000; i++) {
jedis.hset("hashkey:"+i,"field"+i,"value"+i);
}
long b = System.currentTimeMillis();
System.out.println(b-a); //1115ms
}
}
public class JedisTest {
public static void main(String[] args) {
long a = System.currentTimeMillis();
Jedis jedis = new Jedis("192.168.40.131", 6382);
for (int i = 0; i < 100; i++) {
Pipeline pipelined = jedis.pipelined();
for (int j = i*100; j <(i+1)*100 ; j++) {
pipelined.hset("hashkey:" + j, "field" + j, "value" + j);
}
pipelined.syncAndReturnAll();
}
long b = System.currentTimeMillis();
System.out.println(b-a); //106ms
}
}
3.发布订阅
发布者(publisher)publish channel message
订阅者(subscriber)subscribe[channel]#订阅一个或多个 unsubscribe[channel]#取消一个或多个
频道(channel)
4.Bitmap
setbin、getbin、bitcount
bitop op destkey key[key..] 做多个Bitmap的and(交集),or(并集),not(非)、xor(异或)、操作并将结果保存在destkey中
5.HyperLogLog
极小空间完成独立数量统计。 pfadd key element[element..]:向hyperloglog添加元素
pfcount key[key..]计算hyperloglog的独立总数
pfmerge destkey sourcekey[sourcekey..:合并多个hyperloglog
1.是否能容忍错误?(错误率:0.81%)
2.是否需要单条数据?(不能取出来)
6.GEO
GEO(地理信息定位):存储经纬度,计算两地距离,范围计算等 使用zset实现 redis3.2以上
geo key longitude latitude member [longitude latitude member...] #增加地理位置信息
geopos key member[member...] #获取地理位置信息
geodist key member1 member2[unit] #获取两个地理位置的距离 #unit:m(米)、km(千米)、mi(英里),ft(尺)
问题5:Redis持久化的取舍和选择
什么是持久化 redis所有数据保持在内存中,对数据的更新将异步地保存到磁盘上。
快照(1.MySQL Dump、2.Redis RDB)
触发机制-主要三种方式 save(同步)、bgsave(异步)、自动


在这些情况下会自动保存:1.全量复制、2.debug reload、3.shutdown
写日志(1.MySQL Binlog、2.Hbase HLog、3.Redis AOF)

AOF是有重写功能的:减少硬盘占用量、加速恢复速度
bgrewriteaof命令


对比

一般的策略是关闭rdb,开启aof
常见的持久化问题
1.fork操作
与内存量息息相关:内存越大,耗时越长(与机器类型有关)info:latest_fork_usec
改善:
1.优先使用物理机或者高效支持fork操作的虚拟化技术 2.控制Redis实例最大可用内存:maxmemory 3.合理配置Linux内存分配策略:vm.overcommit-memory=1 4.降低fork频率:例如放宽AOF重写自动触发时机,不必要的全量复制
2.子进程开销和优化

3.AOF追加阻塞
和硬盘的优化有关
问题6:主从复制
复制的配置:可以使用slaveof命令或者config配置
全量复制和部分复制
全量复制的开销:
1.bgsave时间、2.RDB文件网络输时间、3.从节点清空数据时间
4.从节点加载RDB的时间、5.可能的AOF重写时间

部分复制:

故障处理
以一主两从为原型:


开发运维常见问题
1.读写分离
- 复制数据延迟
- 读到过期数据
- 从节点故障
2.主从配置不一致
- 例如maxmemory不一致:丢失数据
- 例如数据结构优化参数(例如hash-max-ziplist-entries):内存不一致
3.规避全量复制

4.规避复制风暴

问题7:Redis Sentinel
Redis Sentinel是Redis的高可用实现方案:故障发现、故障自动转移、配置中心、客户端通知。
主从复制的一些问题(手动故障转移、写能力和存储能力受限)
Redis Sentinel(默认端口26379)架构

故障转移的步骤(master宕机)
1.多个sentinel发现并确认master有问题。 2.选举出一个sentinel作为领导。 3.选出一slave作为master. 4.通知其余slave成为新的master的slave. 5.通知客户端主从变化 6.等待老的master复活成为新master的slave.
sentinel的三个定时任务
1.每10秒每个sentinel对master和slave执行info
- 发现slave节点
- 确认主从关系

2.每2秒每个sentinel通过master节点的channel交换信息(pub/sub)
- 通过_ _ sentinel _ _:hello频道交互
- 交互对节点的“看法”和自身信息

3.每1秒每个sentinel对其他sentinel和redis执行ping(心跳监测)

主观下线与客观下线---领导者选举---故障转移(选择合适的slave结点)
1选择slave-priority(slave节点优先级)最高的slave节点,如果存在则返回,不存在则继续。 2选择复制偏移量最大的slave节点(复制的最完整),如果存在则返回,不存在则继续。 3选择runld最小的slave节点。
常见的运维问题
1.节点运维(上下线)
- 机器过保质期下线
- 机器性能不足
- 节点服务不稳定
节点上线:主节点:sentinel failover进行替换。从节点:slaveof即可,sentinel节点可以感知。 sentinel节点:参考其他sentinel节点启动即可。
2 .高可用读写分离
sentinel只会对master进行故障转移
应该实现客户端对从节点的监测
+switch-master:切换主节点(从节点晋升主节点) +convert-to-slave:切换从节点(原主节点降为从节点) +sdown:主观下线。
问题8:Redis Cluster
1.为什么引入集群
并发量、数据量、网络流量问题
--使用分布式:加机器
Redis cluster数据分区规则采用虚拟槽方式(16384个槽),每个节点负责一部分槽和相关数据,实现数据和请求的负载均衡。
2.分布式数据库-数据分区
顺序分区
哈希分区(例如取余)

- 节点取余--扩容时会造成批量移动---建议使用成倍的扩容(50%)
- 一致性哈希--节点比较多的时候使用(客户端分片:哈希+顺时针)翻倍伸缩:保证最小迁移数据和负载均衡
- 虚拟槽分区

3.集群的安装
原生命令安装
1.配置开启节点 cluster-enabled yes 2.meet cluster meet ip port 3.指派槽 cluster addslots slot [slot ...] 4.设置主从 cluster replicate node-id
ruby安装
可视化部署
4.集群伸缩
伸缩原理:集群伸缩=槽和数据在节点之间的移动
扩容集群:准备新节点、加入集群(meet)、迁移槽和数据(如下图)

缩容集群:下线迁移槽、忘记节点、关闭节点
5.客户端路由
moved重定向 ask重定向(两者都是客户单重定向、moved:槽已经确定迁移、ask:槽还在迁移中)
smart客户端:使用直连,客户端是一个智能的客户端,客户端内部负责计算维护键->槽->节点的映射,用于快速定位到目标节点。
6.批量操作怎么实现?
mget mset必须在一个槽
1.串行mget 2.串行1O 3.并行10 4.hash_tag

7.开发运维常见问题
集群完整性:cluster-require-full-coverage默认为yes 带宽消耗:
避免"大"集群:避免多业务使用一个集群,大业务可以多集群。 尽量均匀分配到多机器上:保证高可用和带宽
Pub/Sub广播
publish在集群每个节点广播:加重带宽 可以使用单独“走"一套Redis Sentinel解决

数据倾斜
原因:节点和槽分配不均、不同槽对应键值数量差异较大、包含bigkey、内存相关配置不一致
请求倾斜
热点key:重要的key或者bigkey
优化:避免bigkey、热键不要用hash_tag、当一致性不高时,可以用本地缓存+ MQ
读写分离
只读连接:集群模式的从节点不接受任何读写请求。 (-重定向到负责槽的主节点、-readonly命令可以读:连接级别命令)
数据迁移
官方迁移工具:redis-trib.rb import
只能从单机迁移到集群 不支持在线迁移:source需要停写 不支持断点续传 单线程迁移:影响速度 在迁移过程中,使用redis,可能导致数据丢失
在线迁移:redis-migrate-tool、redis-port
集群vs单机
集群具有一定的限制:
- key批量操作支持有限:例如mget,mset必须在一个slot
- Key事务和Lua支持有限:操作的key必须在一个节点
- key是数据分区的最小粒度:不支持bigkey分区
- 不支持多个数据库:集群模式下只有一个db 0
- 复制只支持一层:不支持树形复制结构
问题9:缓存的使用与设计
1.缓存的受益与成本
**受益:**1.加速读写 2.降低后端负载
成本:
数据不一致:缓存层和数据层有时间窗口不一致,和更新策略有关。
代码维护成本:多了一层缓存逻辑。
运维成本:例如Redis Cluster
场景:
1.降低后端负载:对高消耗的SQL:join结果集/分组统计结果缓存。
2·加速请求响应:利用Redis/Memcache优化10响应时间
3·大量写合并为批量写:如计数器先Redis累加再批量写DB
2.缓存更新策略

3.缓存粒度控制
1.通用性:全量属性更好。 2.占用空间:部分属性更好。 3.代码维护:表面上全量属性更好。
4.缓存穿透问题
缓存穿透问题-大量请求不命中缓存
解决方法1-缓存空对象--问题:需要更多的键、缓存层和存储层数据“短期"不一致。
解决方法2-布隆过滤器拦截--问题:要自己编写

本质上布隆过滤器是一种数据结构,比较巧妙的概率型数据结构(probabilistic data structure),特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”。
相比于传统的 List、Set、Map 等数据结构,它更高效、占用空间更少,但是缺点是其返回的结果是概率性的,而不是确切的。
如果我们要映射一个值到布隆过滤器中,我们需要使用多个不同的哈希函数生成多个哈希值,并对每个生成的哈希值指向的 bit 位置 1,例如针对值 “baidu” 和三个不同的哈希函数分别生成了哈希值 1、4、7,则上图转变为:

Ok,我们现在再存一个值 “tencent”,如果哈希函数返回 3、4、8 的话,图继续变为:

值得注意的是,4 这个 bit 位由于两个值的哈希函数都返回了这个 bit 位,因此它被覆盖了。现在我们如果想查询 “dianping” 这个值是否存在,哈希函数返回了 1、5、8三个值,结果我们发现 5 这个 bit 位上的值为 0,说明没有任何一个值映射到这个 bit 位上,因此我们可以很确定地说 “dianping” 这个值不存在。而当我们需要查询 “baidu” 这个值是否存在的话,那么哈希函数必然会返回 1、4、7,然后我们检查发现这三个 bit 位上的值均为 1,那么我们可以说 “baidu” 存在了么?答案是不可以,只能是 “baidu” 这个值可能存在。
过小的布隆过滤器很快所有的 bit 位均为 1,那么查询任何值都会返回“可能存在”,起不到过滤的目的了。布隆过滤器的长度会直接影响误报率,布隆过滤器越长其误报率越小。

k 为哈希函数个数,m 为布隆过滤器长度,n 为插入的元素个数,p 为误报率。
5.缓存雪崩
缓存雪崩是指,缓存层出现了错误,不能正常工作了。于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。
2、解决方案
(1)redis高可用
这个思想的含义是,既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。
(2)限流降级
这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
(3)数据预热
数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
6.无底洞问题优化
机器增加--但是性能没有提高

优化IO的几种方法:
1.命令本身优化:例如慢查询keys.hgetall bigkey 2.减少网络通信次数 3.降低接入成本:例如客户端长连接/连接池、NIO等
7.热点key重建优化
1.三个目标:
减少重缓存的次数 数据尽可能一致 减少潜在危险--死锁
2.两个解决:
互斥锁(mutex key)

永远不过期(可能在一段时间使用老的值)
1.缓存层面:没有设置过期时间(没有用expire) 2.功能层面:为每个value添加逻辑过期时间,发现超过逻辑过期时间,会使用单独的线程去构建缓存。
