以下内容大多是学习链接,他人整理,个人收藏以便复习,同时归纳分享出来(如有不妥,原作者可随时联系本人删除,感谢!)
八、Redis 缓存
1、Redis命令参考大全
2、Redis分布式锁的实现原理
Redis分布式锁的正确实现方式 - Ruthless - 博客园(当master宕机以后,slave还没有同步过来,那么这种锁也是有问题的)
通过 Redlock 实现分布式锁 | Brickgao's
Redis分布式锁-这一篇全了解(Redission实现分布式锁完美方案)_Coder-优快云博客_redission实现分布式锁
Redisson 分布式锁源码 09:RedLock 红锁的故事 - 程序员小航 - 博客园
新来个技术总监,这Redis分布式锁设计的真漂亮! (qq.com)
3、每秒上千订单场景下的分布式锁高并发优化实践!【石杉的架构笔记】
每秒上千订单场景下的分布式锁高并发优化实践!【石杉的架构笔记】
4、分布式信号量:
探秘分布式解决方案: 分布式限流——Redis版分布式信号量原理 (附RedisTemplate具体实现代码) | 编码妙♂妙♂屋
5、限流:Redis + lua分布式限流
限流(三)Redis + lua分布式限流 - __lay - 博客园
我司用了 6 年的 Redis 分布式限流器,可以说是非常厉害了!
https://www.iteye.com/blog/jinnianshilongnian-2305117(张开涛)
(限流的几种做法,
a、ngnix在网关层限流
ngx_http_limit_req_module
http { limit_req_zone $binary_remote_addr zone=myRateLimit:10m rate=10r/s; }
- zone:定义共享内存区来存储访问信息, myRateLimit:10m 表示一个大小为10M,名字为myRateLimit的内存区域。1M能存储16000 IP地址的访问信息,10M可以存储16W IP地址访问信息。
- rate 用于设置最大访问速率,rate=10r/s 表示每秒最多处理10个请求。Nginx 实际上以毫秒为粒度来跟踪请求信息,因此 10r/s 实际上是限制:每100毫秒处理一个请求。这意味着,自上一个请求处理完后,若后续100毫秒内又有请求到达,将拒绝处理该请求。
b、Google 的 Guava RateLimiter限流: 使用Guava RateLimiter限流以及源码解析 - 简书
c、ngnix+lua分布式全局限流 :限流(三)Redis + lua分布式限流 - __lay - 博客园
)
6、布隆过滤器原理:
(布隆过滤器应用场景:
(1)、在缓存穿透问题上,使用布隆过滤器判断数据是否存在,不存在直接返回
在缓存之前在加一层 BloomFilter ,在查询的时候先去 BloomFilter 去查询 key 是否存在,如果不存在就直接返回,存在再走查缓存 -> 查 DB。
(2)、海量数据去重:爬虫系统中对成千上万的url的去重等
(3)、邮箱系统的垃圾邮件过滤功能
(4)、黑名单过滤功能
(5)、判断一个数字是否在包含大量数字的数字集中
)
7、接口幂等性
例:
A应用,发过来两个请求到B应用,请求扣款,都是同样的一个单号123,
B应用,需要针对 t_account 账户表,帐号abc去扣款,t_account表 怎么去识别这个流水号是否已经扣过款了呢?
需要先去 t_trans_detail 交易记录表,查询一下,有没有这条扣款流水记录,状态status是支付成功的;
(1)、第一次支付成功后之后,t_trans_detail流水表,订单状态status更改为PAYED(已支付),并记录支付流水号123;
(2)、其它请求,发起对该订单的发货请求deliverOrder,讲该order实体状态更改为DELIVERED;
(3)、同样的流水号123扣款请求再次到达,这时候查询支付记录表,发现该流水号123已经支付过了,遂返回步骤时的支付结果和状态(返回第1步的状态码:支付成功
因为幂等性,要求的就是幂等,相同业务请求,每次结果返回一样)
8、单位时间内,redis限制IP访问次数的方法(例如用来限制,用户账户,多少时间内登录错误次数)
9、zookeeper实现分布式锁:
基于zookeeper实现分布式锁_sunfeizhi的专栏-优快云博客_zookeeper实现分布式锁
Zookeeper 分布式锁 - 图解 - 秒懂_架构师尼恩-优快云博客_zookeeper分布式锁
10、redis有序集合ZUNIONSTORE命令
11、如何使用redis使用排行榜的问题
Redis实现排行榜功能(实战)_俊俊的小熊饼干的博客-优快云博客_redis实现排行榜
https://segmentfault.com/a/1190000002694239(同上篇文章思路相同)
12、ZUNIONSTORE 和 ZINTERSTORE 是用于有序集(sorted set)区别:
根据文档的说明:
-
ZUNIONSTORE
用于计算给定的一个或多个有序集的并集。 -
而
ZINTERSTORE
则用于计算给定的一个或多个有序集的交集。
参考:Redis 的 ZUNIONSTORE 命令和 ZINTERSTORE 命令的另一种用法 — blog.huangz.me
13、redis序列化问题:
redis序列化的问题_yuyeqianhen的博客-优快云博客_redis序列化
redis序列化方式对比:
(1)、redis的默认方式是JdkSerializationRedisSerializer
JdkSerializationRedisSerializer: 使用JDK提供的序列化功能。
优点是反序列化时不需要提供类型信息(class),但缺点是需要实现Serializable接口,还有序列化后的结果非常庞大,是JSON格式的5倍左右,这样就会消耗redis服务器的大量内存。
(2)、Jackson2JsonRedisSerializer: 使用Jackson库将对象序列化为JSON字符串。
优点是速度快,序列化后的字符串短小精悍,不需要实现Serializable接口。
但缺点也非常致命,那就是此类的构造函数中有一个类型参数,必须提供要序列化对象的类型信息(.class对象)。 通过查看源代码,发现其只在反序列化过程中用到了类型信息。
问题:使用默认的JDK序列化方式,在RDM工具中查看k-v值时会出现“乱码”,不方便查看。
14、redis防止用户重复提交的操作(防止接口B重复操作提交),参考: 防止表单重复提交(springboot,redis) - 简书
个人理解笔记:系统通过redis防止重复请求_小哥骑单车-优快云博客_redis重复请求
15、redis解决单点登录问题:
为什么要单点登录
在平常写案例的时候,如果只有一个web工程。如果要访问用户相关信息,那么我们通常会写个拦截器,从session中看能不能取到用户信息,如果不能那么需要返回到登录页面,用户登录后将用户信息保存到session中,那么在此访问用户中心就没问题了。然后这种做法在一个web工程中是没问题的。
如果系统是分布式的情况呢,比如拿商城来说,它是一个分布式的,什么会员模块,商品管理模块,购物车模块,订单模块等。如果还用上面那张验证方式的话,访问a模块的话发现没登录,然后跳转页面登录了,信息存入session中,如果下次访问的是b模块,由于模块都是存在于不同的服务器session中肯定没有登录用户信息,那么肯定是访问不通过要求重新登录。而且,为了解决高并发还得进行集群,即使是两次访问同一模块,也有可能访问的是集群中的另外一台服务器,这样就存在多次要求登录的问题。
解决:我们可以整个Session服务器(SSO系统)专门用来处理登录问题,这样用户每次访问用户中心的时候都来该服务器判断用户有没有登录,如果登录了放行,没有登录就跳转到登录页面,登录后将用户信息保存到Session服务器中,我们需要用redis来模拟从前的session,以前用户信息存入redis中,这里我们就将用户信息存入redis中。
那么怎么存,用什么做key什么做value呢?在一个web工程的时候,用户信息存入session是这样设置的session.setArrtribute(“admin”,user),获取则是session.getAttribute(“admin”);
很自然的想到用用户id做为key,但是不要这样做,因为这样是不能区分不同的连接的,比如你在A电脑登录了tom这个账号,信息存入redis中了且key为tom的id。然后换个电脑B,但是不直接登录却直接访问用户中心(比如订单结算),那么能不能访问呢?是可以访问的,因为redis中存了这么个键值对啊,然后就能取出来用户信息说明已经登录了,所以肯定给访问。但是这样是不合理的,不应该换电脑登录还能直接访问用户中心。
用一个web工程使用tomcat服务器的时候,tomcat是怎么区分不同连接的呢,实际上每次获取session的时候tomcat会生成一个JSESSIONID的作为标识,然后返回给浏览器存到Cookie里面,下次再访问的时候会带着这个JSESSIONID来访问,然后tomcat拿到这个标识会去寻找对应的session,再从中取出用户信息,这样如果换电脑B了,B浏览器里面是没有这个JSESSIONID的,那么在服务端也找不到对应的session更别说取到用户信息了,所以要求重新登录。
既然是打算用redis来模拟session,那么也可以这样做。用户每次登录的时候都会生成一个唯一的表示token,用它来作为key,用户信息作为value,然后将token存到Cookie里面返给浏览器。用户下次
访问用户中心的时候,从Cookie里面取token,再用token从redis中取用户信息,来判断是否允许访问用户中心。
这样做,只要用户换电脑登录了,那么Cookie里面就没有这个token就查不到用户信息所以必须要重新登录了,分析到这基本上也能做了。
redis实现单点登录系统_成都往右的博客-优快云博客_redis单点登录
16、使用redis-cli无法登录redis的时候,可以使用telnet:
使用telnet连接redis - John_2011 - 博客园
17、redis的scan问题:
SCAN 命令用于迭代当前数据库中的数据库键。SSCAN 命令用于迭代集合键中的元素。HSCAN 命令用于迭代哈希键中的键值对。ZSCAN 命令用于迭代有序集合中的元素(包括元素成员和元素分值) ;
SSCAN 命令、 HSCAN 命令和 ZSCAN 命令的第一个参数总是一个数据库键。
而 SCAN 命令则不需要在第一个参数提供任何数据库键 —— 因为它迭代的是当前数据库中的所有数据库键。
18、使用redis进行自动补全功能
19、redis性能方面注意事项:
20、redis数据结构之quicklist:
quickList 是 zipList 和 linkedList 的混合体,它将 linkedList 按段切分,每一段使用 zipList 来紧凑存储,多个 zipList 之间使用双向指针串接起来。
每个quicklist节点上的ziplist越短,则内存碎片越多。内存碎片多了,有可能在内存中产生很多无法被利用的小碎片,从而降低存储效率。这种情况的极端是每个quicklist节点上的ziplist只包含一个数据项,这就蜕化成一个普通的双向链表了。
每个quicklist节点上的ziplist越长,则为ziplist分配大块连续内存空间的难度就越大。有可能出现内存里有很多小块的空闲空间(它们加起来很多),但却找不到一块足够大的空闲空间分配给ziplist的情况。这同样会降低存储效率。这种情况的极端是整个quicklist只有一个节点,所有的数据项都分配在这仅有的一个节点的ziplist里面。这其实蜕化成一个ziplist了。
配置参数:list-max-ziplist-size -2
21、redis跳表:
跳跃表结构在 Redis 中的运用场景只有一个,那就是作为有序列表 (Zset) 的使用。跳跃表的性能可以保证在查找,删除,添加等操作的时候在对数期望时间内完成,这个性能是可以和平衡树来相比较的,而且在实现方面比平衡树要优雅,这就是跳跃表的长处。跳跃表的缺点就是需要的存储空间比较大,属于利用空间来换取时间的数据结构。
Redis使用跳跃表作为有序集合键的底层实现之一,若一个有序集合包含的元素数量比较多,或者有序集合中的成员是比较长的字符串时,Redis就会使用跳跃表来作为有序集合键的底层实现。
Redis的跳跃表实现跟WilliamPugh在"Skip Lists: A Probabilistic Alternative to Balanced Trees"中描述的跳跃表算法类似,只是有三点不同:
a、允许重复分数;
b、排序不止根据分数,还可能根据成员对象(当分数score相同时,是根据member排序的);
c、有一个前继指针,因此在第1层,就形成了一个双向链表,从而可以方便的从表尾向表头遍历,用于ZREVRANGE命令的实现。
跳表java代码实现:面试准备 -- Redis 跳跃表_LuckyToMeet-Dian叶-优快云博客_redis跳表
扩展知识点:对应juc包中的集合使用的跳表:
那么当我们需要多线程并发存取<Key, Value>数据并且希望保证数据有序时,我们需要怎么做呢?也许,我们可以选择ConcurrentTreeMap。不好意思,JDK没有提供这么好的数据结构给我们。当然,我们可以自己添加lock来实现ConcurrentTreeMap,但是随着并发量的提升,lock带来的性能开销也随之增大。JDK6里面引入的ConcurrentSkipListMap也许可以满足我们的需求:
juc的ConcurrentSkipListMap源码:Java多线程系列--“JUC集合”05之 ConcurrentSkipListMap - 如果天空不死 - 博客园
22、redis渐进式哈希:
当以下条件中的任意一个被满足时, 程序会自动开始对哈希表执行扩展操作:
服务器目前没有在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 1;
服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 5;
从该函数中,我们并没有看到真正执行哈希表rehash的相关操作,只是分配了一个新的哈希表就结束了。我们知道哈希表rehash需要遍历原有的整个哈希表,对原有的所有key进行重新hash,存放到新的哈希槽;
在redis的实现中,没有集中的将原有的key重新rehash到新的槽中,而是分解到各个命令的执行中,以及周期函数中;
(1)操作辅助哈希:
在redis中每一个增删改查命令中都会判断数据库字典中的哈希表是否正在进行渐进式rehash,如果是则帮助执行一次;
(2)定时辅助rehash:
虽然redis实现了在读写操作时,辅助服务器进行渐进式rehash操作,但是如果服务器比较空闲,redis数据库将很长时间内都一直使用两个哈希表。所以在redis周期函数中,如果发现有字典正在进行渐进式rehash操作,则会花费1毫秒的时间,帮助一起进行渐进式rehash操作
优点:
渐进式 rehash 的好处在于它采取分而治之的方式, 将 rehash 键值对所需的计算工作均滩到对字典的每个添加、删除、查找和更新操作上, 从而避免了集中式 rehash 而带来的庞大计算量。式rehash避免了redis阻塞;
缺点:
渐进,可以说非常完美,但是由于在rehash时,需要分配一个新的hash表,在rehash期间,同时有两个hash表在使用,会使得redis内存使用量瞬间突增,在Redis 满容状态下由于Rehash会导致大量Key驱逐。
参考帖:redis渐进式rehash机制 - 割肉机 - 博客园
23、分布式环境下如何保证数据库和缓存的双写一致性:
【分布式】分布式环境下如何保证数据库和缓存的双写一致性?看完我明白了!!_冰河的专栏-优快云博客
缓存与数据库一致性系列-序言 - Kido的博客 | Kido's Blog (推荐)
分布式环境下如何保证数据库和缓存的双写一致性_小哥骑单车-优快云博客(同上同一篇,属个人转载文章)
24、redis分配key的一致性哈希算法:
Redis Cluster在设计中没有使用一致性哈希(Consistency Hashing),而是使用数据分片引入哈希槽(hash slot)来实现;
一个 Redis Cluster包含16384(0~16383)个哈希槽,存储在Redis Cluster中的所有键都会被映射到这些slot中,
集群中的每个键都属于这16384个哈希槽中的一个,集群使用公式slot=CRC16(key)/16384来计算key属于哪个槽,其中CRC16(key)语句用于计算key的CRC16 校验和。
参考链接:一致性Hash原理与实现 - 简书
25、redis为什么单线程那么快:
https://zhuanlan.zhihu.com/p/58038188
26、简单来说,Redis在4.0之前使用单线程的模式是因为以下三个原因:
(1)使用单线程模式的Redis,其开发和维护会更简单,因为单线程模型方便开发和调试。
(2)即使使用单线程模型也能够并发地处理多客户端的请求,主要是因为Redis内部使用了基于epoll的多路复用(后面会说)。
(3)对于Redis来说,主要的性能瓶颈是内存或者网络带宽,而并非CPU。
但Redis在4.0以及之后的版本中引入了惰性删除(也叫异步删除),意思就是我们可以使用异步的方式对Redis中的数据进行删除操作了,
例如:unlink key、flushdb async、flushall async,但是此版本的多线程只能用于大数据量的异步删除,对于非删除操作的意义并不是很大。
如果我们使用Redis多线程就可以分摊Redis同步读写I/O的压力,以及充分利用多核CPU资源,并且可以有效地提升Redis的QPS。在Redis中虽然使用了I/O多路复用,并且是基于非阻塞I/O进行操作的,但是I/O的读和写本身是阻塞的。比如当socket中有数据时,Redis会先将数据从内核态空间拷贝到用户态空间,然后再进行相关操作,而这个拷贝过程是阻塞的,并且当数据量越大时拷贝所需要的时间就越多,而这些操作都是基于单线程完成的。
因此在Redis6.0中新增了多线程的功能来提高I/O的读写性能,它的主要实现思路是将主线程的I/O读写任务拆分给一组独立的线程去执行,这样就可以使多个socket的读写并行化了,但Redis的命令依旧是由主线程串行执行的。
但是注意:Redis6.0是默认禁用多线程的,但可以通过配置文件redis.conf中的io-threads-do-reads等于true来开启,完整配置为io-threads-do-reads true。但是还不够,除此之外我们还需要设置线程的数量才能正确地开启多线程的功能,同样是修改Redis的配置,例如设置io-threads 4,表示开启4个线程。
关于线程数的设置,官方的建议是如果为4核的CPU,那么设置线程数为2或3;如果为8核的CPU,那么设置线程数为6。总之线程数一定要小于机器的CPU核数,线程数并不是越大越好。
redis多线程模型讲解:https://segmentfault.com/a/1190000039223696(篇幅较长,有一定难度,选择了解)
IO多路复用源码分析:Strike Freedom(篇幅较长,有一定难度,选择了解)
27、Redis事务:
redis操作是原子的,但是到事务这就不是了,事务可以是很多命令进一个队列依次执行,中间某个执行失败是不影响后续命令执行的;
大概的意思是,redis作者不支持事务回滚的原因有以下两个:
他认为 Redis 事务的执行时,错误通常都是编程错误造成的,这种错误通常只会出现在开发环境中,而很少会在实际的生产环境中出现,所以他认为没有必要为 Redis 开发事务回滚功能;
不支持事务回滚是因为这种复杂的功能和 Redis 追求的简单高效的设计主旨不符合。
这里不支持事务回滚,指的是不支持运行时错误的事务回滚。
参考帖:Redis之坑:理解Redis事务_洛丹伦的夏天-优快云博客
28、Redis集群方案对比:
Redis集群方案总结 - kaleidoscopic - 博客园
Redis集群化方案对比:Codis、Twemproxy、Redis Cluster | Kaito's Blog
29、为什么要用Redis?
30、Redis为什么这么快
而之所以Redis能够有这么高的性能,不仅仅和采用多路复用技术和单线程有关,此外还有以下几个原因:
(1)、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。
(2)、数据结构简单,对数据操作也简单,如哈希表、跳表都有很高的性能。
(3)、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU
(4)、使用多路I/O复用模型
为什么说Redis是单线程的以及Redis为什么这么快!_徐刘根的博客-优快云博客_redis为什么快
31、Redis不是一直号称单线程效率也很高吗,为什么又采用多线程了?
Redis不是一直号称单线程效率也很高吗,为什么又采用多线程了? - 知乎
redisTemplate.opsForHash().putIfAbsent(hashName, hashCol, hashColVal); (此命令是否原子性,有待商榷,可以用redis+lua脚本实现原子性)
redis生成分布式id方案 - HackerVirus - 博客园 (cnblogs.com)
33、雪花算法分布式id,时钟回拨的问题:
简单有效地解决SnowFlake的时钟回拨问题
简单有效地解决SnowFlake的时钟回拨问题_timestatic的博客-优快云博客_linux时钟回拨
34、redis(主从)脑裂及解决方案
脑裂主要是一主多从,或者哨兵模式下的问题!
集群模式下,如果你这个分片是一主多从,也有可能发生脑裂问题!
下面的解答是,只有主从,没有集群:
redis集群(主从)脑裂及解决方案_霁云HYY的博客-优快云博客_redis脑裂
35、redis数据丢失的情况:
Redis数据"丢失"讨论及规避和解决的几点总结_shangyuanlang的博客-优快云博客
36、redis命令大全: