Redis应用场景:
1、点赞数,点击数,评论数(hash)
2、帖子Id排序,快速现实帖子列表(zset)
3、帖子标题、摘要、作者和封面信息,用户列表页展现(hash)
4、点赞用户Id列表,评论Id列表,用来显示和去重计数(zset)
5、缓存近期热帖内容(帖子的内容和空间占用比例大),减少数据库压力(hash)
6、记录帖子相关文章Id,根据内容推荐相关帖子(list)
7、如果帖子Id是整数自增的,可用Redis来分配帖子Id(计数器)
8、收藏集和帖子之间的关系(zset)
9、记录热榜帖子Id列表,总热榜和分类热榜(zset)
10、缓存用户行为历史,过滤恶意行为(zset、hash)
5种基础数据结构:
1、String字符串
打个比方:用户信息结构体使用Json序列化为字符串,是一个可修改的字符串,内部结构是实现类似于ArrayList,采用预分配冗余空间的形式来减少内存的频繁分配。当字符串长度小于1MB时,扩容为倍增。如果字符串长度超过1MB,扩容时一次只会扩1MB的空间。字符串最大长度为512MB。
2、list列表
Redis的列表相当于Java里的LinkedList,意味着list的插入和删除操作非常快,时间复杂度为O(1),索引位置就很慢,时间复杂度为O(0)。
Redis列表结构常用来做异步队列使用。
右边进,左边出,队列
rpush
lpop
右边进,右边出,栈
rpush
rpop
【快速列表】
quicklist = n*ziplist = ziplist+ziplist+…+ziplist
在列表元素较少的情况下,会使用一块连续的内存存储,这个结构是ziplist,即压缩列表。它将所有的元素彼此紧挨着一起存储,分配的是一块连续的存储空间。当数据量较多的时候才会改为quicklist,因为普通的链表需要的附加指针空间太大,浪费空间,会加重内存的碎片化。
3、hash(字典)
Redis字典相当于Java里的HashMap,无序字典,内部存储了很多键值对,但是Redis的hash字典只能是字符串。
当hash移除了最有一个元素后,该数据结构被自动删除,内存被回收。
hash缺点:hash结构的存储消耗要高于单个字符串。
4、set
Redis的set相当于Java里的HashSet,无序且唯一,有去重功能。
当set中最后一个元素被移除后,数据结构被自动删除,内存被回收。
5、zset【重点】
具有set属性,无序且唯一,zset:value,score,score表示value的排序权重。
zset中最后一个value被移除后,数据结构被自动删除,内存被回收。
【跳跃列表】
zset内部的排序功能是通过“跳跃列表”数据结构实现的。因为zset要支持随机的插入和删除,不宜使用数组来表示。
zset根据score值进行排序,具体5.5节
分布式锁:【重点】
一、分布式锁的意义
功能:使用分布式锁来限制程序的并发执行。
目标:占坑
指令:setnx(set if not exists)指令,只允许被一个客户端占坑。先来先占,用结束后使用del释放坑。
会发生的问题:del指令没吊用,陷入死锁,所永远不能释放。所以,在拿到锁之后,再给锁加上一个过期时间。
setnx name true
expire name 5
del name
但同样造成死锁情况:如果setnx之后服务器进程挂了,那么又成了死锁
所以要保证setnx和expire的原子性:
set name true ex 5 nx
set xxxxxxxxxxxxx nx
xxxxxxxxxxxxx = name true ex 5
二、超时问题
Redis的分布式锁不能解决超时问题:第一个线程持有的锁过期了,临界区的逻辑没有执行完,第二个线程提前持有了这把锁,导致临界区代码不能得到串行执行。为了避免这个问题,Redis分布式锁不能用较长时间的任务。
方案:(不完美)set指令的value参数设置为一个随机数,释放锁时先匹配随机数是否一致,然后再删除key,保证当前线程占有的锁不会被其他线程释放,除非这个锁是因为过期了而被服务器自动释放的。
延时队列
一、介绍
Redis消息队列不是专业的消息队里,消息不是十分可靠。
二、异步消息队列-list
list,右进左出,左进右出,rpush→lpop,lpush→rpop
三、列队空了怎么办
客户端会陷入pop死循环,不停pop,没有数据,继续pop……空轮询
空轮询:拉高客户端的CPU消耗,Redis的QPS也会被拉高,如果空轮询的客户端有几十个,Redis的慢查询会显著增多。
通常使用sleep来解决,让线程睡1s,就是1000毫秒,Thread.sleep(1000),CPU消耗会降低,Redis的QPS也会降低。
但是,sleep一秒会导致一个问题:阻塞读
四、阻塞读
睡眠会导致消息的延迟增大,如果有多个消费者,这个延迟会有所下降,因为每个消费之的睡眠时间是岔开的。阻塞读 blocking -》 blpop / brpop
阻塞读在队列没有数据的时候,会立即进入休眠状态【阻塞了】,一旦数据到了,就会醒过来。消息的延迟几乎为0。用blpop 和 brpop完美解决了上面的问题。
上面 的方法仍旧存在问题:空闲连接的问题
五、空闲连接问题–》造成异常
如果线程一直阻塞在那里,Redis的客户端连接就成了闲置连接,服务器很少主动断开连接,占用资源将减少。使用blpop和brpop会抛出异常。
六、客户端加锁失败问题–3种策略
1、抛出异常,稍后重试
2、sleep一会儿,再重试
3、请求转至延时队列,稍后重试
1–》本质是对当前请求的放弃,由用户决定是否发起新的请求
2–》会造成死锁
3–》适合异步消息处理,将当前冲突的请求扔到另一个对队列后处理以避开冲突
七、延时队列的实现
可通过Redis的zset(有序列表)来实现,将消息序列号为一个字符串作为zset的value,将消息的到期处理时间作为score,然后用多个线程轮询zset获取到期的任务进行处理。
多线程是为了保证可用性,万一一个线程挂了,其他线程还可以继续处理。正因为有多个线程,所以需要考虑并发争抢任务,确保任务不会被多次执行。
此处应有代码敲打
位图
位图类似于数组,可用get/set直接获取设置整个位图的内容,也可使用getbit/setbit等将byte数组看成“位数组”来处理
- 基本用法:零存整取、整存零取
- 统计和查找
- 指令bitfield:饱和阶段sat、失败不执行fail
HyperLogLog
1、定义:
提供不精确的去重计数方案,虽然不精确,标准误差0.81%,是redis的高级数据结构。
2、方法:
pfadd:增加计数
pfcount:获取计数
pfmerge:将多个pf计数累加在一起形成一个新的pf值
3、pfadd中的pf是什么?数据结构发明人Philippe Flajolet的首字母缩写!
4、注意事项
每个HyperLogLog都占12kb存储空间,当有上亿个用户,所占空间会很大。
但是redis对HyperLogLog进行了优化,在计数较小时,它的存储空间采用稀疏矩阵存储,空间所占很小,只有当计数慢慢变大超越了阈值时候,才会一次转变成稠密矩阵,占用12kb空间。
布隆过滤器
1、用途:解决去重问题
解决去重问题,空间上节省90%。
2、判断对象是否存在:
过滤器:数值存在
真实:数据可能不存在
过滤器:数值不存在
真实:数据不存在
3、基本用法
bf.add:添加元素
bf.exists:判断一个元素是否存在
bf.madd:添加多个元素
bf.mexists:判断多个元素是否存在
bf.add name1 value1
bf.exists name1 value1
bf.madd name1 value1 value2 value3
bf mexists name1 value1 value2 value3 value4
布隆过滤器对于已经见过的元素不会发生误判,只会误判没有见过的元素。
4、注意事项
布隆过滤器的initial_size设置的过大,会浪费存储空间,设置的过小,会影响准确率。
布隆过滤器的error_rate越小,需要的存储空间就越大。
Redis-Cell
限流使用漏斗限流算法。
指令cl.throttle
应用场景:用户回复行为频率每60秒30次,漏斗初始容量为15,然后收到漏水速度的影响。
cl.throttle userzz:reply 15 30 60 1
或
cl.throttle userzz:reply 15 30 60
指令会返回指令状态(0或1),漏斗容量,漏斗剩余空间,拒绝重试时间,漏斗空耗费时间。
可以直接返回数组的第四个值进行sleep,若不想阻塞线程,可以异步定时任务了重试。
GeoHash算法(粗略)地理位置Geo模块
其实就是zset结构
Geo指令–6个
- geoadd 添加
- geodist 距离
- geopos 获取元素位置
- geohash 获取元素hash
- georadiusmumber 查询元素附近的元素
- georadius 根据坐标值查询附近的元素
Keys&Scan(粗略)
1、keys的缺点:
keys算法是遍历算法,时间复杂度是O(n),如果实例中有千万级别以上的key,这个指令会导致Redis服务卡顿,所有读写Redis的其他指令都会被蔓延后甚至会超时报错,因为Redis是单线程程序,所有读写Redis的其他指令都必须等到keys执行完了才可以继续。
2、scan:
- 复杂度也是O(n),但是通过游标分步进行的,不会阻塞线程
- 提供limit参数,返回控制结果最大值
- 提供模式匹配功能
- 服务器不需要游标状态,scan的游标以整数给服务器
- scan不提供去重,服务器端去重
- 遍历过程中数据修改,不会被遍历到修改
- 单词返回的结果为空不意味着遍历结束,要看游标值是否为0
3、scan的遍历:高位进位加法
4、渐进式rehash
为啥有rehash?
Java的HashMap在扩容时会一次性将旧数据下挂接的元素全部转移到新的数组下。如果HashMap中元素特别多,线程就会出现卡顿。
rehash的功能:
同时保留旧数组和新数组,在定时任务中以及后续hash的指令操作渐渐将就数据挂接的元素迁移到新数组中。【这意味着rehash的字典需要同时访问新旧两个数据结构。如果在旧数组下面找不到元素,还需要去新数组下面寻找】
zscan遍历zset集合元素
hscan遍历hash字典元素
sscan遍历set集合元素
业务开发中尽量避免key的使用。