一、Redis基础知识
1. Redis简介
Redis 是一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存、分布式、可选持久性的键值对(Key-Value)存储数据库,并提供多种语言的 API。
Redis 通常被称为数据结构服务器,因为值(value)、字符串(String)、哈希(Hash)、列表(list)、集合(sets)和有序集合(sorted sets)等5种类型,相比memcache的String单一类型丰富很多。这5种类型分别应对不同的需求,丰富了redis的使用场景和功能。
【存储方案】
Redis内存存储,运行效率远超过硬盘的存储访问。同时支持持久化方案(在磁盘格式方面他们是紧凑的以追加的方式产生的,因为他们并不需要进行随机访问), 提供了AOF和RDB两种方式。RDB快照的方式可以实现快速恢复,AOF可以保证数据的实时行和完整性。
【Redis其他特性】
- 原子性 – Redis的所有操作都是原子性的,同时Redis还支持对几个操作全并后的原子性执行。
- 支持发布\订阅 - 可以做为消息中心使用,支持通知, 消息订阅等场景
- 性能极高 – 内存存储加上单线程原子性操作(不止于此),让Redis具备很高的处理性能,读的速度可以达到110000次/s,写的速度可以达到81000次/s ,性能略低于memcache,但功能要强大很多
2. Redis的线程模型
都说Redis是单线程的,避免了线程切换带来的额外成本,降低了系统复杂度。但在Redis6.0引入了多线程,其实也是主要是在请求接受端引入了多路复用,而命令处理部分仍然使用的单线程的处理模式(如上图)
1. 客户端的请求进来后,会进入一个处理线程池,由多路复用器分配处理;
2. 多路复用器分配好处理线程后会将请求交由文件事件分配器;
3. 文件事件分配器会根据请求的类型(读/写)选择对应的应答处理器,并将请求交由命令处理器排队处理;
4. 然后由对应的回复处理器将处理结果返回给对应的多路复用线程池中的线程;
5. 处理结果通过server socket返回给client端
3. Redis数据结构
Redis支持丰富的数据结构,Key值为String,Value支持 String ,Hash,List,set,SortSet,分别针对不同的使用场景,这里介绍下没种数据类型的原理和使用场景
(1)Key - String 类型
Redis使用了Hash表作为底层实现,原因不外乎高效且实现简单。Redis底层key的存储结构就是类似于HashMap那样数组+链表的结构,Redis的key查找算法复杂度是O(n)
(2) Value -String 类型
String 是 Redis 最简单最常用的数据结构,也是 Memcached 唯一的数据结构,日常使用最频繁。
【底层实现】
如果一个字符串对象保存的是整数值, 并且这个整数值可以用 long 类型来表示, 那么字符串对象会将整数值保存在字符串对象结构的 ptr 属性里面(将 void* 转换成 long ), 并将字符串对象的编码设置为 int 。
如果字符串对象保存的是一个字符串值, 并且这个字符串值的长度大于 39 字节, 那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串值, 并将对象的编码设置为 raw。
如果字符串对象保存的是一个字符串值, 并且这个字符串值的长度小于等于 39 字节, 那么字符串对象将使用 embstr 编码的方式来保存这个字符串值。
【应用场景】
- 缓存:string 最常用的就是缓存功能,会将一些更新不频繁但是查询频繁的数据缓存起来,以此来减轻 DB 的压力。
- 计数器:可以用来计数,通过 incr 和decr 操作,如统计网站的访问量、文章访问量等。
3. Value- List(列表)
list 是有序可重复列表,和 Java 的 LinkedList 比较像,可以通过索引查询;插入删除速度快。
【底层实现】
使用quicklist,它是一个双向链表,而且是一个基于ziplist的双向链表,quicklist的每个节点都是一个ziplist,结合了双向链表和ziplist的优点。
【使用场景】
- 消息队列:Redis 的 list 是有序的列表结构,可以实现阻塞队列,使用左进右出的方式。Lpush 用来生产 从左侧插入数据,Brpop 用来消费,用来从右侧 阻塞的消费数据。
- 数据的分页展示: lrange 命令需要两个索引来获取数据,这个就可以用来实现分页,可以在代码中计算两个索引值,然后来 redis 中取数据。
- 可以用来实现粉丝列表以及最新消息排行等功能。
4. Value - Hash (散列)
【基本概念】
Redis 散列可以存储多个键值对之间的映射。和字符串一样,散列存储的值既可以是字符串又可以是数值,并且用户同样可以对散列存储的数字值执行自增或自减操作。这个和 Java 的 HashMap 很像,每个 HashMap 有自己的名字,同时可以存储多个 k/v 对。
【底层实现】
哈希对象的编码可以是 ziplist 或者 hashtable 。
哈希对象保存的所有键值对的键和值的字符串长度都小于 64 字节并且保存的键值对数量小于 512 个,使用ziplist 编码;否则使用 hashtable;
【应用场景】
- Hash 更适合存储结构化的数据,比如 Java 中的对象;其实 Java 中的对象也可以用 string 进行存储,只需要将 对象 序列化成 json 串就可以,但是如果这个对象的某个属性更新比较频繁的话,那么每次就需要重新将整个对象序列化存储,这样消耗开销比较大。可如果用 hash 来存储 对象的每个属性,那么每次只需要更新要更新的属性就可以。
- 购物车场景:可以以用户的 id 为 key ,商品的 id 为存储的 field ,商品数量为键值对的value,这样就构成了购物车的三个要素。
5. Value - Set(集合)
【基本概念】
Redis 的 set 和 list 都可以存储多个字符串,他们之间的不同之处在于,list是有序可重复,而set是无序不可重复。
【底层实现】
集合对象的编码可以是 intset 或者 hashtable 。
如果集合对象保存的所有元素都是整数值并且保存的元素数量不超过 512 个,则使用 intset 编码;否则使用 hashtable;
【应用场景】
标签:可以将博客网站每个人的标签用 set 集合存储,然后还按每个标签 将用户进行归并。
存储好友/粉丝:set 具有去重功能;还可以利用set并集功能得到共同好友之类的功能。
6. Value - zset(有序集合)
【基本概念】
有序集合和散列一样,都用于存储键值对:其中有序集合的每个键称为成员(member),都是独一无二的,而有序集合的每个值称为分值(score),都必须是浮点数。可以根据分数进行排序,有序集合是Redis里面唯一既可以根据成员访问元素(这一点和散列一样),又可以根据分值以及分值的排列顺序来访问元素的结构。和Redis的其他结构一样,用户可以对有序集合执行添加、移除和获取等操作。
【底层实现】
有序集合的编码可以是 ziplist 或者 skiplist
如果有序集合保存的元素数量小于 128 个并且保存的所有元素成员的长度都小于 64 字节,用 ziplist 编码;否则使用skiplist;
当ziplist作为zset的底层存储结构时候,每个集合元素使用两个紧挨在一起的 ziplist 节点来保存,第一个节点保存元素的成员,第二个元素保存元素的分值。
当skiplist作为zset的底层存储结构的时候,使用skiplist按序保存元素及分值,使用dict来保存元素和分值的映射关系。
【应用场景】
排行榜:有序集合最常用的场景。如新闻网站对热点新闻排序,比如根据点击量、点赞量等。
带权重的消息队列:重要的消息 score 大一些,普通消息 score 小一些,可以实现优先级高的任务先执行。
7. Value - HyperLogLog
【基本概念】
Redis 在 2.8.9 版本添加了 HyperLogLog 结构,HyperLogLog 是用来做基数统计的算法,所谓基数,也就是不重复的元素。
- 优点:在输入元素的数量或者体积非常大时,计算基数所需的空间总是固定的、并且是很小的。在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。
- 缺点:因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。
估算的值,可能存在误差
【应用场景】
- 网页统计UV (浏览用户数量,同一天同一个ip多次访问算一次访问,目的是计数,而不是保存用户)
- 传统的方式是使用set保存用户的id,可以统计set中元素数量作为标准判断。但如果这种方式保存大量用户id,会占用大量内存,我们的目的是为了计数,而不是去保存id。
8. Value - GEO
【基本概念】
在 Redis 3.2 版本中新增了一种叫 geo 的数据结构,它主要用来存储地理位置信息,并对存储的信息进行操作。
【应用场景】
用于存储地理信息以及对地理信息作操作的场景。例如:查看附近的人、微信位置共享、地图上直线距离的展示
9. Value - BloomFilter(布隆过滤器)
【基本概念】
一种数据结构,是由一串很长的二进制向量组成,可以将其看成一个二进制数组。既然是二进制,那么里面存放的不是0,就是1,但是初始默认值都是0。
【主要作用】
判断一个元素是否在某个集合中。比如说,我想判断20亿的号码中是否存在某个号码,如果直接插DB,那么数据量太大时间会很慢;如果将20亿数据放到缓存中,缓存也装不下。这个时候用布隆过滤器最合适.
布隆过滤器是用于判断一个元素是否在集合中。通过一个位数组和N个hash函数实现。
- 优点:空间效率高,所占空间小,查询时间短。
- 缺点:元素添加到集合中后,不能被删除,有一定的误判率
【底层实现】
Redis提供的Bitmaps这个“数据结构”可以实现对位的操作。Bitmaps本身不是一种数据结构,实际上就是字符串,但是它可以对字符串的位进行操作。
可以把Bitmaps想象成一个以位为单位数组,数组中的每个单元只能存0或者1,数组的下标在bitmaps中叫做偏移量。单个bitmaps的最大长度是512MB,即2^32个比特位。
【应用场景】
- 数据库防止穿库
- 业务场景中判断用户是否阅读过某视频或文章
- 缓存宕机、缓存击穿场景,一般判断用户是否在缓存中,如果在则直接返回结果,不在则查询db
- WEB拦截器,如果相同请求则拦截,防止重复被攻击
二、Redis部署方案
1. 单机模式
这也是最基本的部署方式,只需要一台机器,负责读写,一般只用于开发人员自己测试。
2.主从复制
在主从复制这种集群部署模式中,我们会将数据库分为两类,第一种称为主数据库(master),另一种称为从数据库(slave)。主数据库会负责我们整个系统中的读写操作,从数据库会负责我们整个数据库中的读操作。其中在职场开发中的真实情况是,我们会让主数据库只负责写操作,让从数据库只负责读操作,就是为了读写分离,减轻服务器的压力。
3. 哨兵模式
哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立运行的进程,其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。它具备自动故障转移、集群监控、消息通知等功能。
4. cluster集群模式
在redis3.0版本中支持了cluster集群部署的方式,这种集群部署的方式能自动将数据进行分片,每个master上放一部分数据,提供了内置的高可用服务,即使某个master挂了,服务还可以正常地提供。
三、Redis典型应用场景
1. 分布式锁(string)
利用value为String的setnx方法能力,当key不存在时,将 key 的值设为 value ,返回1,Key存在则setnx不做任何动作,返回0。
整体思路: 当setnx返回1时,表示获取锁,做完操作以后del key,表示释放锁,如果setnx返回0表示获取锁失败。
2. 计数器(string)
利用value为String且值为数字时 incr/decr的能力,通过redis的单线程处理以及高性能保证计数的准确和实效性。比如博客中文章的浏览数量功能
set key 0
incr key // incr readcount::{帖子id} 每阅读一次
get key // get readcount::{帖子id} 获取阅读量
3. 分布式全局唯一id(string)
Redis可以实现全局唯一id的生成功能,利用的也是Value为String且值为数字时 incrBy的能力,通过redis每次分配一个id段,利用本地的缓存慢慢累加,既可以解决全局id生成的单点性能问题 ,也可以保证全局唯一,但服务宕机只会消耗一段id而已
set userId 0
incr usrId //返回1
incrby userId 1000 //返回10001
4. 消息队列(list)
利用list队列的
在list里面一边进,一边出即可
## key这个list有元素时,直接弹出,没有元素被阻塞,直到等待超时或发现可弹出元素为止,上面例子超时时间为10s
lpush key value //一直往list左边放
brpop key value 10
5. 新浪/Twitter用户消息列表(list)
加入说小编li关注了2个微博a和b,a发了一条微博(编号为100)就执行如下命令
lpush msg::li 100
b发了一条微博(编号为200)就执行如下命令:
lpush msg::li 200
假如想拿最近的10条消息就可以执行如下命令(最新的消息一定在list的最左边):
lrange msg::li 0 9 //下标从0开始,[start,stop]是闭区间,都包含
6. 抽奖活动(set)
利用set的集合能力、去重能力实现以及redis本身的单线程、高性能,实现抽奖活动的瞬时高并发业务场景。
sadd key {userId} // 参加抽奖活动
smembers key //获取所有抽奖用户,大轮盘转起来
spop key count //抽取count名中奖者,并从抽奖活动中移除
srandmember key count //抽取count名中奖者,不从抽奖活动中移除
7,实现点赞,签到,like等功能
// 1001用户给8001帖子点赞
sadd like::8001 1001
srem like::8001 1001 //取消点赞
sismember like::8001 1001 //检查用户是否点过赞
smembers like::8001 //获取点赞的用户列表
scard like::8001 //获取点赞用户数
8. 实现关注模型,可能认识的人(set)
seven关注的人
sevenSub -> {qing, mic, james}
青山关注的人
qingSub->{seven,jack,mic,james}
Mic关注的人
MicSub->{seven,james,qing,jack,tom}
//返回sevenSub和qingSub的交集,即seven和青山的共同关注
sinter sevenSub qingSub -> {mic,james}
// 我关注的人也关注他,下面例子中我是seven
// qing在micSub中返回1,否则返回0
sismember micSub qing
sismember jamesSub qing
// 我可能认识的人,下面例子中我是seven
// 求qingSub和sevenSub的差集,并存在sevenMayKnow集合中
sdiffstore sevenMayKnow qingSub sevenSub -> {seven,jack}
9. 电商商品筛选(set)
每个商品入库的时候即会建立他的静态标签列表如,品牌,尺寸,处理器,内存
// 将拯救者y700P-001和ThinkPad-T480这两个元素放到集合brand::lenovo
sadd brand::lenovo 拯救者y700P-001 ThinkPad-T480
sadd screenSize::15.6 拯救者y700P-001 机械革命Z2AIR
sadd processor::i7 拯救者y700P-001 机械革命X8TIPlus
// 获取品牌为联想,屏幕尺寸为15.6,并且处理器为i7的电脑品牌(sinter为获取集合的交集)
sinter brand::lenovo screenSize::15.6 processor::i7 -> 拯救者y700P-001
10. 排行版(zset)
redis的zset天生是用来做排行榜的、好友列表, 去重, 历史记录等业务需求
// user1的用户分数为 10
zadd ranking 10 user1
zadd ranking 20 user2
// 取分数最高的3个用户
zrevrange ranking 0 2 withscores
参考文档:
Redis的线程模型_Escape2022的博客-CSDN博客_redis线程模型
Redis 的部署模式_李歘歘的博客-CSDN博客_redis部署方式