Redis学习
文章目录
Redis学习
支持原创
Redis介绍
使用场景
- 热点数据缓存
- 数据库二级缓存
- 分布式session
- 排行榜
- 计数器
- 分布式锁
- 秒杀&红包系统
- 社区动态Feed流
- 消息队列
基本命令
字符串 String
- 特点: 存储所有的字符和字符串
- 应用场景: 做缓存使用
- 常用命令
- 赋值:set key value
- 取值: get key
- 删除: del key [key …]
- 数值增减:incr key decr key
- 拼接value值: append key value
散列 hash
- 特点: 相当于java中hashMap集合
- 应用场景: 可以存储javaBean对象,此种使用场景不多,可被String替代
- 常用命令
- 赋值:hset key field value
- 批量赋值:hmset key field value [field value …]
- 取值:hget key field
- 批量取值:hmget key field [field …]
- 取所有值:hgetall key
- 获取所有的key: hkeys key
- 获取所有的value: hvals key
- 获取key中所包含的field的数量: hlen key
- 删除:hdel key field [field …]
- 某字段是否存在:hexists key field
- 数值增减:hincrby key field increment
列表 list
- 特点: 相当于java中linkList, 是一个双向链表的结构(可以从头部或者尾部增删数据,增删块,查找慢)
- 应用场景: 做消息队列
在java中客户端提供了线程安全获取集合数据的方式 - 常用命令
-
从左侧(头)添加: lpush key value [value …]
-
从右侧(尾)添加: rpush key value [value …]
-
从左侧弹出:lpop key
-
从右侧弹出:rpop key
-
查看列表:lrange key start end(0 -1表示从头到尾)
-
获取列表中元素的个数:llen key
-
lset key index value:
设置链表中的index的脚标的元素值,0代表链表的头元素,-1代表链表的尾元素。index不存在则抛异常 -
删除count个值为value的元素:lrem key count value:
无序集合 set
- 特点: 唯一, 无序(通过hash表实现的,so添加、删除、查找复杂度都是o(1))
- 应用场景: 集合运算(例如去重的操作)
- 常用命令
- 添加:sadd key member [member …]
- 删除:srem key member [member …]
- 获取所有的元素:smembers key
- 判断元素是否存在:sismember key member
- 集合的差集运算:sdiff key [key …]
- 集合的交集运算:sinter key [key …]
- 集合的并集运算:sunion key [key …]
- 获取set中成员的数量:scard key(随机返回set中的成员)
- 返回count个不重复key的个数:srandmember key [count]
- 将key1、key2相差的成员存储在destination上:
sdiffstore destination key [key …] - 将返回的交集存储在destination上:
sinterstore destination key [key …] - 将返回的并集存储在destination上:
sunionstore destination key [key …]
有序集合:sorted set
- 特点:唯一, 有序
- 应用场景: 一般用来做排行榜
- 常用命令
- 添加数据:zadd key score member [score member …]
- 获得元素:zscore key member: 返回指定元素的值
- 获取集合中的成员数量:zcard key
- 删除元素:zrem key member [member …]
- 范围查询:zrange key start end [withscores]:
- 按照分数大小顺序返回数据:zrevrange key start stop [withscores]
- 按照排名范围删除元素:zremrangebyrank key start stop
- 按照分数范围删除元素:zremrangebyscore key min max
- 返回分数在[min,max]的成员并按照分数从低到高排序:zrangebyscore key min max [withscores] [limit offset count]:
。[withscores]:显示分数;[limit offset count]:offset,表明从脚标为offset的元素开始并返回count个成员a
-设置指定成员的增加的分数。返回值是更改后的分数:zincrby key increment member - 获取分数在[min,max]之间的成员:zcount key min max:
- 返回成员在集合中的排名。(从小到大:zrank key member:
- 返回成员在集合中的排名。(从大到小):zrevrank key member
发布和订阅
-
介绍:发布订阅(pub/sub)是一种消息通信模式,其中发送者pub发送消息,订阅者sub接收消息。Redis客户端可以订阅任意数量的频道
-
常用命令
- subscribe channel [channel …]
- publish channel message
- unsubscribe [channel [channel …]]
其他命令
- 排序命令
- sort key [by pattern] [limit offset count] [get pattern] [get pattern …] [asc] [desc] [alpha] [store destination]
- 过期命令
- expire key seconds
- expireat key timestamp
- persist key
- ttl key
- 事务命令
- multi
- exec
- discard
- watch key [key …]
- unwatch
持久化
RDB方式
- 根据配置进行自动快照
- 用户执行save或bgsave命令
- 执行flushall命令
- 执行复制(replication)时
AOF方式
- 默认未开启,需要手动配置
- 参数启用:appendonly yes
- 默认文件名:appendonly.aof
集群方案
复制方式
哨兵模式
集群模式
分布式锁分析
同进程锁
分布式锁
Redis分布式锁方案
- 实现原理:利用setnx命令,该命令时原子性操作,只有在key不存在的情况下,才能set成功
- 涉及命令
- 加锁命令:setnx key [seconds] value
- 解锁命令:del key
- 锁超时命令:expire key seconds
- 代码实现
if(setnx(key,1)==1){ ①
expire(key,30) ②
try{
//需要加锁的代码块
......
}finally{
del(key) ③
}
}
- 问题1分析:当①成功,②失败时,该key锁就相当于没有设置过期时间,永不释放锁
- 解决方案:使用set(key,1,30,NX)命令代替setnx,这样①,②是原子性操作…
- 问题2分析:当③导致误删情况(A线程设置超时30s,but超过30s都没执行完,到30s则锁过期自动释放锁,被B线程获得锁,之后过了会A执行完了,然后接着执行del指令,A线程实际删除的是B线程的锁)
- 解决方案:可以在加锁的时候把当前线程的ID当做value,并且在删除之前验证key对应的value是不是自己线程的ID
加锁时伪代码
String threadId=Thread.currentThread().getId();
set(key,threadId,30,NX);
解锁时伪代码
if(threadId.equals(redisClient.get(key))){
del(key);
}
红包案例分析
业务介绍
业务难点
- 安全级别高,不能超发或者少发
- 海量高并发资源抢夺严重
技术设计
基础组件概念
- list结构存放预先生成的红包金额
- hash结构存放红包领取记录
- list结构存放红包异步发放队列
红包拆分方案
- 实时拆分
- 过程:枪红包时,实时计算每个红包的余额,以实现红包拆分的过程;拆—抢---转账(异步操作)
- 最后的入账操作一致性保证:最后一次的take all操作+对账
- 要求:系统性能、拆分算法要求高
- 举例:拆分过程需要保证后续拆分红包的金额不为空,而且不容易做到拆分红包的金额服从正态分布
- 正态分布
- 解释:正态分布像一只倒扣的钟。两头低,中间高,左右对称。大部分数据集中在平均值,小部分在两端。实际上人的身高、手臂长度、成绩都是符合正态分布
- 预先拆分
- 过程:红包在开抢之前已经完成了红包的拆分工作,抢红包的过程实际上是依次取出拆分好的红包金额
- 要求:对拆分算法要求较低,可以拆分出随机性比较好的红包金额,通常结合队列来使用
拆分算法
- 二倍均值算法
- 算法介绍:假设拆分金额未M,剩余拆分红包个数为N,红包最小金额为0.01,并且金额需要有一定趋势的正态分布
- 算法公式:m=rand(0,floor(M/N*2))
- 算法解释:floor为向下取整;rand(min,max)表示从(min,max)区间随机取一个一个值;( M/N2)表示剩余平均金额的2倍;因为N是大于1的整数,所以(M/N2) <M的,表示一定能保证后续红包能拆分到金额
- 举例:
假设10人,红包金额100元
第一人:100/10*2=20,随机范围(0,20),平均可以抢到10元
第二人:90/9*2=20,随机范围(0,20),平均可以抢到10元
第三人:80/8*2=20,随机范围(0,20),平均可以抢到10元
以此类推,每次随机范围的均值是相等的
第一次平均抢到M/N元,第二次抢到(M-M/N)/(N-1)=M/N,以此类推,每个人的均值都一样。
缺点:除了最后一次,任何一次抢到的金额都不会超过人均金额的两倍,并不是任意的随机所以风险规避者应该先抢,风险偏爱者应该后抢(有风险^-^)
- 过程图解
发红包
抢红包
过期红包退还
- 线段分割法
- 算法介绍:把红包总金额想象成一条很长的线段,而每个人抢到的金额,则是这条主线段所拆分出的若干子线段(N段),
- 当N个人一起抢总金额为M的红包时,我们需要做N-1次随机运算,以此确定N-1个切割点。随机的范围区间是(1, M)
- 注意点:
(1)当随机切割点出现重复,如何处理 — 重复了就重新切呗
(2)如何尽可能降低时间复杂度和空间复杂度 — 可以用链表,牺牲时间换取空间(排了个序),也可以牺牲空间节省时间(大数组)
对比:与二倍均值法相比,改善了缺点。越大金额抢到的人数越少。
深层思考
- 抢红包过程中如何更高效的进行事务操作
- 如果更新账户余额失败怎么办,如何做到最终一致性
- 如何快速扩容,通过set化实现
Feed流案例作业
业务介绍
- Feed喂养的意思,想吃什么就喂你什么
- 典型的场景:微博、知乎、朋友圈、以及各个聚合类咨讯app的订阅号(你想看啥给你啥,而不是所以东西全给你(瀑布流))
- 此系统中,我可以关注别人,别人可以关注我,另外自己有一个首页,以时间轴的形式显示关注人和自己的动态信息
业务难点
- 如何解决大V问题
- 如何解决内存消耗问题
技术设计
- 集中模式:将所有用户的动态汇集到统一的流,不同用户拉取各自关注信息筛选处理
- 性能要求和复杂度非常高(不考虑)
- 推模式:写扩散。每当用户发帖,对所有粉丝推送一条该用户的动态消息记录
- 优点:查看动态的读取场景下效率最高
- 缺点:对关注列表的变动不符合需求
- 拉模式:读扩散。每当请求好友动态接口,拉取用户所有关注者的最近动态,然后汇总排序
- 优点:对关注列表的变化能实时体现
- 缺点:拉取所有关注者的数据,做汇总排序分页的代价较大
- 推拉模式:即读+写扩散,上面两者的结合
- 根据业务需求对推模式做补充,在开销较小的部分使用拉模式
Redis面试’刁难’问题
-
Redis有哪些数据结构
- 基本:字符串String、字典Hash、列表List、集合Set、有序集合SortedSet
- 高级:HyperLogLog、Geo、Pub/Sub
- 扩展:还玩过Redis Module、BloomFilter,RedisSearch,Redis-ML
-
使用过Redis分布式锁吗,具体怎么使用的
- 使用:setnx来加锁、再用expire给锁加一个过期时间防止忘记释放
- 问题:
- 问题1:在setnx之后执行expire之前进程意外crash或者要重启维护了,这个锁永不释放了
- 问题1解决:用set指令代替setnx,可以把setnx和expire合成一条指令来用的
-
Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如果将它们全部找出来?
- 可以使用keys指令可以扫出指定模式的key列表
- 问题:
- 问题1:假如redis正在给线上业务服务,使用keys指令会造成线程阻塞一段时间,线上服务会暂停,直至keys指令执行完毕
- 问题1解决:可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长
-
使用过Redis做异步队列么,怎么用的?
- 使用:一般使用list结构作为队列,rpush生产消息,lpop消费消息。当lpop没有消息的时候,要适当sleep一会再重试
- 问题:
- 问题1:有没有办法不用sleep呢
- 问题1解决:list还有个指令叫blpop,在没有消息的时候,它会阻塞住直到消息到来
- 问题2:能不能生产一次消费多次呢?
- 问题2解决:使用pub/sub主题订阅者模式,可以实现1:N的消息队列
- 问题3:pub/sub有什么缺点?在消费者下线的情况下,生产的消息会丢失
- 问题3解决:使用专业的消息队列如rabbitmq
-
redis如何实现延时队列
- 使用sortedset,拿时间戳作为score,消息内容作为key调用zadd来生产消息,消费者用zrangebyscore指令获取N秒之前的数据轮询进行处理。
-
如果有大量的key需要设置同一时间过期,一般需要注意什么?
- 大量的key过期时间设置的过于集中,到过期的那个时间点,redis可能会出现短暂的卡顿现象。一般需要在时间上加一个随机值,使得过期时间分散一些
-
Redis如何做持久化的?
- bgsave做镜像全量持久化,aof做增量持久化。因为bgsave会耗费较长时间,不够实时,在停机的时候会导致大量丢失数据,所以需要aof来配合使用。在redis实例重启时,优先使用aof来恢复内存的状态,如果没有aof日志,就会使用rdb文件来恢复
-
aof文件过大恢复时间过长怎么办?
- Redis会定期做aof重写,压缩aof文件日志大小
- Redis4.0之后有了混合持久化的功能,将bgsave的全量和aof的增量做了融合处理,这样既保证了恢复的效率又兼顾了数据的安全性(杀手锏答案)
-
突然机器掉电会怎样?
- 取决于aof日志sync属性的配置,如果不要求性能,在每条写指令时都sync一下磁盘,就不会丢失数据。但是在高性能的要求下每次都sync是不现实的,一般都使用定时sync,比如1s1次,这个时候最多就会丢失1s的数据
-
Pipeline有什么好处,为什么要用pipeline?
- 可以将多次IO往返的时间缩减为一次,前提是pipeline执行的指令之间没有因果相关性。使用redis-benchmark进行压测的时候可以发现影响redis的QPS峰值的一个重要因素是pipeline批次指令的数目
-
Redis的同步机制了解么?
- Redis可以使用主从同步,从从同步。第一次同步时,主节点做一次bgsave,并同时将后续修改操作记录到内存buffer,待完成后将rdb文件全量同步到复制节点,复制节点接受完成后将rdb镜像加载到内存。加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程
-
是否使用过Redis集群,集群的原理是什么?
- Redis Sentinal着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务
- Redis Cluster着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储