Redis从基础到实战
1 Redis数据类型
CLI命令:
object encoding xxx - 查看保存当前Key值的数据结构
Redis命令参考:
http://redisdoc.com/index
1.1 String类型
最常用的数据类型,底层编码:int、enbstr、row
CLI命令:set、get、incr、incrby、mset、mget
1.2 hash类型 数组+链表
CLI命令:hget、hmget、hset、hmset、hkeys、hvals、hgetall、hdel、hlen
1.2.1 存储结构:ziplist
ziplist是使用连续的内存空间存储数据,读写效率更高。
使用条件:
一个hash对象保存的filed数量<512个
一个hash对象中所有的field和value的字符串长度都<64byte
超过ziplist使用条件的hash类型自动使用hashtable存储结构存储数据。
1.2.2 存储结构:hashtable
dict-dictht-dictEntry[]-dictEntry
dictht下的dictEntry中的链表长度平均大于5,则扩容dictht
Hash可以存储 String、对象 类型
1.3 list列表(有序字符串)
存储有序的字符串,元素可以重复
lpush、rpush、lpop、rpop、lindex、lrange、blpop、brpop(等待)
1.3.1 ziplist存储结构(早期3.2版本之前)
1.3.2 linkedlist存储结构(早期3.2版本之前)
1.3.3 quicklist(新版结构)
本质是一个数组+链表的数据结构
应用场景:
可以用作队列、栈
可以用作一些有序的内容,比如消息列表、文章列表、评论列表、公告列表、活动列表
1.4 set集合(无序的不重复)
最大存储数量2^32-1(40亿左右)
CLI命令:
sadd、smembers(查询)、scard(统计元素长度)、srandmember(随机拿一个元素)、spop(弹出来一个元素)、srem(删除某个元素)、sismember(是否存在某个元素)
取差集sdiff、取交集sinter、取并集sunion
1.4.1 存储结构:intset
存储整形的数组
1.4.2 存储结构:hashtable
默认超过512(redis.conf配置)个元素的时候使用,存储非纯数字的字符串
应用场景:
抽奖spop xxx、去重、 商品文章评论点赞、商品贴标签
商品条件的筛选(取交集)、用户关注和推荐的模型、两个账单对账(可以查出你有我没有的数据、差集)
1.5 zset有序链表(有序不重复)
CLI命令:
zadd、zrange(有序输出)、zrevrange(倒序排序)、zrangebyscore(范围输出)、zrem、zcard、zincrby(步增)、zcount(统计元素个数)、zrank(元素排名)、zscore(根据分数获取元素)
1.5.1 存储结构:ziplist
使用条件:元素数量<128,所有元素长度小于64bytes
根据分值排序决定元素放在哪个位置
1.5.2 存储结构:skiplist+dict
分层(跳表):增加指针,level是随机的
应用场景:存在动态变化的列表、学生成绩、热搜
1.6 其他数据结构
1.6.1 Bitmap(String)
CLI命令:
getbit、setbit、get
1.6.2 GeoSpatial
经纬度位置 geoadd(添加位置)、geopos(获取位置)、计算距离、获取范围内地点集合
1.6.3 Hyperloglogs:
基数统计,非精准计数(节省内存)
1.6.4 Streams
流数据处理
2 Redis高级特性
2.1 发布订阅
模式:publisher - Redis Channel - subscriber
订阅频道:可以一次订阅多个(channel不需要提前创建):
CLI命令:
subscribe channel-1 channel-2 channel-3
向指定频道发布消息:
CLI命令:
publish channel-1 hello
取消订阅:
CLI命令:
unsubscribe channel-1
按规则(pattern)订阅频道:
CLI命令:
psubscribe *sport
缺点:非专业MQ服务器,并非性能,延迟,持久化不好
Redis消息发布订阅主要是redis内部功能使用,或非常简单的消息服务
2.2 事务功能
定义:一组命令一起执行。先入队先执行,执行命令时不会存在其他客户端干扰,不存在事务的嵌套。
CLI命令:
multi //开启事务,将下面的命令放到待执行的队列中
—xxxx— // Redis命令
exec //执行事务命令
其他命令:
discard //取消事务的命令
watch //监视key是否被修改,如果修改了,就不能提交成功了。没有修改才可以exec成功
2.3 Lua脚本
在Redis中执行Lua脚本:
CLI命令:
eval lua-script key-num key[] val[]
在Lua脚本中执行Redis命令:
Lua脚本命令:
redis.call(command, key[],val[])
用执行lua脚本的语法,执行lua脚本来调用redis的命令:
Linux命令:
eval “return redis.call(‘set’,KEY[1],ARGV[1])” 1 hello world
调用Lua脚本文件:
Linux命令:
redis-cli --eval 脚本名称 参数个数 参数1 参数2 参数3…
3 Redis原理
3.1 Redis为什么这么快?
1、纯内存KV 2、请求单线程 3、同步非阻塞I/O–多路复用
Redis QPS性能测试:
Linux命令:
redis-benchmark -t set,lpush -n 100000 -q
为什么使用单线程?
1、多线程创建销毁的性能消耗
2、多线程的上下文切换
3、多线程竞争问题、锁的问题
redis.conf中有参数maxmemory设置Redis的最大内存:
32位系统:2^32 = 4GB,应用程序通过虚拟地址访问内存,内核空间1GB,实际用户空间大小3GB
64位系统:2^64 = 1024*1024TB,应用程序通过虚拟地址访问内存,内核空间256TB,实际用户空间大小256TB,其他空间Redis未定义
6.0的多线程 指的是 I/O多线程。说的是多线程处理I/O,实际4.0之后就已经有其他线程了。
我们一直说的单线程是处理请求是单线程的,多线程指的是从内核空间复制到用户空间是多线程。
虚拟内存 - 多路复用
3.2 过期策略
- 立即过期-占用大量cpu资源
- 惰性过期-访问的时候判断是否过期,过期的话就删掉,不占用大量cpu资源,但占用内存
- 定时过期
3.3 Redis持久化策略
3.3.1 RDB: Redis DataBase, 记录快照
redis.conf中的配置:
save 900 1
save 300 10
save 60 10000
…
rdbcompression yes # 是否开启压缩算法,LZF算法
rdbchecksum yes # 校验RDB完整性
rdbfilename dump.rdb # 指定的rdb的名字
rdb-del-sync-files no
dir /usr/local/soft/redis-6.0.9 # 指定rdb的路径
RDB手动触发刷盘:
- save:前台保存快照,会注册redis服务
- bgsave:后台保存快照,会folk一个子进程生成快照,不会长时间阻塞redis提供服务
另外Redis客户端的命令:shutdown(正常关机)和flushall(删库)也会刷rdb文件
优势:
- 内存紧凑,适合备份和灾难回复
- 生成文件过程不影响主进程
- 大数据集恢复速度快
不足:
- 不能实时持久化,可能丢失数据,可靠性没有那么强
3.3.2 AOF: Append Only File, 记录命令日志
(如果两个策略都开启的话,优先AOF)
redis.conf中的配置:
appendonly no # 开关 yes开启AOF
appendfilename “appendonly.aof” # 文件名
appendfsync everysec # no表示不执行fsync,由操作系统保证数据同步到磁盘;always表示每次从写入都执行fsync,以保证数据同步到磁盘;everysec(默认)表示每秒执行一次fsync,可能会丢失这1s的数据
auto-aof-rewrite-percentage 100 # 文件大小生长的比例达到,100表示100%,也就是翻倍的时候自动触发重写AOF文件
auto-aof-rewrite-min-size 64mb # 触发重写最小文件的大小,64mb表示如果文件小于64mb,则不触发重写
bgrewriterafo:重写AOF文件,从内存中(不是硬盘中的aof文件)的数据重新生成AOF,多其中的命令进行压缩
选哪个:
可以同时开启,rdb比较节省空间,aof可靠性更好。
4 Redis分布式
4.1 主从复制
主从复制需要协调维护master节点、slave节点 的关系。Redis主从节点架构中,只允许主节点写入数据,从节点自动同步
redis.conf中的配置:
replicaof [masterip] [masterport] # 指定当前节点的master的ip和port,则当前节点会成为master的从节点
也可以不通过redis配置文件,通过执行CLI命令的方式加入主从架构:
Linux命令:
–slaveof IP port # 启动redis时变成某个主节点的从节点
CLI命令:
slaveof host port # 在redis客户端中将当前已经启动的节点变成某个主节点的从节点
CLI命令:
info replication # 查看当前节点的主从关系
slaveof no one # 脱离主从架构
主从复制的实现:从节点后台定时任务线程每隔1s钟跑一次
全量复制: 读取RDB文件,生成新RDB文件
增量复制: 命令同步阶段,跟上主节点的步伐,从节点记录上次同步到了哪个位置offset。
6.0中主从复制的无盘复制:repl-diskless-sync no ; 不借助磁盘,直接通过网络发送RDB文件。2.8版本中这个功能已经出现
不足:
没有解决高可用问题,master节点挂掉的时候,存在单点故障。需要手动切换主节点。
4.2 可用性保证
4.2.1 Sentinel哨兵机制
目标:
- 服务端:自动切换主从
- 客户端:自动发现路由
Sentinel是一个特殊状态的Redis服务,主要用来监控master/slave,并且互相监控
Sentinel启动方式:
Linux命令:进入src目录
1、 ./redis-sentinel …/sentinel.conf
2、 ./redis-server …/sentinel.conf --sentinel
哨兵的选举基于RAFT算法:
先到先得,少数服从多数。(Raft算法在Consul、RocketMQ里面都有应用)
Redis Master选主规则:
- 如果与哨兵链接断开的比较久,超过了某个阈值,就直接失去了选举权。
- 如果拥有选举权,那就看谁的优先级高,这个在配置文件里可以设置(replica-priority 100),数值越小优先级越高。
- 如果优先级相同,就看谁从master中复制的数据最多(offset复制偏移量最大),选最多的那个。
- 如果复制数量也相同,就选择进程id最小的那个。
4.3 数据分片方案
4.3.1 客户端实现路由选择
Jedis自带分片方案:Jedis提供普通连接池、哨兵连接池、分片连接池
4.3.2 路由选择的逻辑抽取出来,做成一个服务
Twemproxy、Codis、Redis Cluster
Redis Cluster
创建一个Cluster就完成了集群的搭建:
Linux命令:
redis-cli --cluster create host[]:port[]
Redis Cluster特点:
- 无中心的集群架构
- 数据分布是根据slot来动态分布。slot可以用命令调整,可以将性能好的机器的槽分配的多一点
- 可扩展到1000个Redis Group(官方不推荐超过1000个),节点可以动态的添加或者删除,数据会重新分布。
- 高可用,部分节点不可用,集群仍然可用,可以实现自动的故障转移。master挂掉后,slave可以通过投票机制自动完成选举成为Master
- 降低了运维成本。提高系统的扩展性和可用性。
- 包括了sentinel的功能
Redis Cluster解决以下问题:
-
怎么均匀分片
将内存分成16384个slot槽,再将槽分配到对应的节点上,通过CRC16 计算Hash的方式将key模以16384得到slot槽,从而得到对应的节点。 -
怎么访问到相应的节点
CLI命令:
cluster keyslot key # 可以用这个命令得到这个key分布在哪个slot槽里面
当需要将一批有业务关联的数据放在一个节点上时,可以通过{}来指定只对key中相同标识的一部分key做CRC16计算,从而让其保存在同一个节点。
CLI命令:
set a{key}1 1 # 只用大括号包起来的key取模,通过{}中相同的key控制他落到的节点
SmartJedis,在客户端本地计算key对应的槽,并从本地客户端中group和slot的对应表,选择对应的redis服务端进行选择。
- 重写分片,怎么正常服务
节点的加入分两步:
① 通过命令将节点加入Redis Cluster
新增节点时会马上生成一个reshard计划,进行新节点对应slot的数据迁移。
新增节点命令:redis-cli --cluster reshard 目标节点(IP端口号)
② 通过命令分配slot给新加入的节点(slot可以动态的调整,旧数据还能自动迁移)
Redis Cluster故障转移
- 当Slave发现自己的Master变成FAIL了
- Slave将自己记录的集群currentEpoch加1,并广播FAILOVER_AUTH_REQUEST信息
- 其他节点收到该信息,只有Master响应,判断请求者的合法性,并发送FAILOVER_AUTH_ACK,对每一个epoch只发送一次ack
- 尝试failover的slave收集FAILOVER_AUTH_ACK
- 当投票超过半数后变成新的Master
- 广播Pong通知其他集群节点
4.3.3 服务端自带分片存储,路由选择。
5 Redis实战
用抓包工具抓包可以看到redis客户端的请求信息是怎样的。
抓包命令:ip.dst==192.168.125.1 and tcp.port in {6379}
5.1 客户端介绍
RedisTemplate、Jedis、Lettuce、Redisson
5.1.1 Jedis
Jedis实现了包含了几乎所有的Redis命令
Pipeline:批量打包发送给Redis
5.1.2 Lettuce
Lettuce时线程安全的Redis客户端,不存在Jedis那种多个客户端共享一个实例的时候的线程安全的问题。
-
提供了同步的API: RedisClient(创建客户端)、StatefulRedisConnection(线程安全的长连接)、RedisCommands(get、set方法)
-
提供了异步的API: RedisClient、StatefulRedisConnection、RedisAsyncCommands(异步执行命令set、get)、RedisFuture(异步get)
通常SpringBoot 2.x以后只需要用RedisTemplate,就是默认使用上述API实现的。
5.1.2 Redisson
Redisson诞生的定位是:Java在分布式环境中可扩展的数据结构,分布式的缓存。它不支持事务处理,但可以通过Lua脚本来实现事务。
Map、SortedSet
支持分布式锁:
加锁:RLock.tryLock
解锁:RLock.unlock
锁到期后,redisson内部提供了一个看门狗watchDog,不断监视这个锁,如果当前任务没执行完,自动进行续命
5.2 数据一致性
可以使用Redis作为缓存来进行数据一致性处理。
缓存的使用场景:
- 命中缓存-缓存返回
- 缓存不存在-数据库存在(加锁)-写入缓存-缓存返回
缓存一致性的原则:
- 以数据库的数据为准。
- 考虑实时一致性和最终一致性。
1、当缓存中不存在
2、数据库中的值发生变化的时候
一致性是在性能和准确性的权衡
先更新数据库再更新缓存失败: MQ保证缓存更新成功
先删除缓存再更新数据库: 双删保证缓存最新
6 Redis高并发
6.1 热点数据发现
怎么操作记录热点数据
1、客户端
2、代理层Proxy
3、服务端
客户端通过:Jedis.monitor(new JedisMonitor(){}),进行key的监听。
会影响服务端的性能,且一个monitor只能监听一个节点。如果要监听多个redis节点的数据,则需要再客户端创建多个monitor。
redis-faina:专门用来监听monitor的工具
4、机器
ELK提供packetbeat,可以对TCP的协议进行抓包,可以分析网络流量,抓到redis命令的数据,就可以解析它,再出报表热点数据。
6.2 缓存雪崩:大量的热点数据同时过期
原因:数据是批量添加到redis,且过期时间是一样的。过期以后就都打到数据库了,数据库就扛不住了。
解决方案:
1、加锁
2、预更新 - 提前更新过期值
3、ttl加随机数 - expire属性加一个随机数的时间,
4、永不过期 - 超热点数据就不用过期了
6.3 缓存击穿
热点key过期,影响没有雪崩大
6.4 缓存穿透
不断的查询数据库中不存在的值,Redis失去了作用,没起到保护数据库的作用
解决方案:
1、即使key不存在,也set一个值为无效数据的key到Redis中,设置过期时间expire。有可能导致Redis内存不够。
2、增加布隆过滤器,实时快速的判断数据库中是否存在对应key。
如何在海量元素中(比如有十亿数据,不定长、不重复),快速判断一个元素是否存在?
①Java中的Map、Set,时间复杂度是O(1),但是这种方式,10亿的大数据,一般服务器扛不住。
②BitMap(位图) BitSet,来标识某一个元素在Long[]数组中对应下标是否存在。提前将元素和下表对应起来,提前将不固定的长度转换成相同长度的输出,提前利用hash散列算法将数据分布均匀,当出现哈希碰撞的时候就可以利用链表或者向后移1位。
③布隆过滤器的本质:
1)位数组(二进制向量)
2)一系列随机映射函数(哈希)
布隆过滤器能在查询数据库之前就判断数据库中是否存在数据,如果不存在就不查询,起到保护数据库的作用。
实现:Guava的BloomFilter(只支持put,不支持删除元素)、baqend的带计数器的CountingBloomFilter(支持add,支持remove)…