关于Redis的整体认识

257 阅读9分钟

方法论

  • 学习redis的过程中,每个操作都要想想,会不会造成阻塞。
  • 要读懂一下内容,需要了解以下术语:内存、线程、进程、阻塞、哈希表、压缩表、跳表、幂等性、缓存命中、缓存缺失
  • 分布系统中,两个组件要基于消息队列进行通信
  • 计算机功能在开发时是要调用的,所以遇到技术问题,就要想想怎么调用。

为什么redis快

redis能够支持万次的并发量。
为啥快?因为

  • 是内存DB,操作都在内存完成
  • 得益于它的数据结构

Redis数据类型&数据结构对应关系

string: 简单动态字符串 List: 双向链表、压缩列表 Hash: 压缩列表、哈希表 Sorted Set: 压缩列表、跳表 Set: 哈希表、整数数组
为了实现从键到值的快速访问,Redis使用哈希表保存所有键值对;
对于值是string,直接操作即可;对于压缩表则根据zlbytes、zltail、zllen等找到数据;对于跳表则根据多个索引跳着查找

哈希冲突,同桶下的数据越来越多

哈希冲突链解决冲突问题,但是数据多了,链就多了,因此查询就满了。所以redis会对哈希表做rehash(扩hash2,hash1数据转到hash2,has1留着之后rehash)。但是,一次性完成rehash或造成线程阻塞,所以要渐进式rehash(随着每个客户端请求,带走一个索引的所有entry)

实际使用中要避开高复杂度操作

什么是高复杂操作?可以查下redsi官方手册参看操作设计的数据结构的复杂度。

  • 单元素操作是基础
  • 范围操作非常耗时
  • 统计操作通常高效
  • 例外情况只有几个(LPOP/RPOP/LPUSH/RPUSH)

Redis单线程

Redis单线程指Redis的网络I耦合键值对读写是由一个线程来完成的,但持久化、异步删除、集群数据同步是由额外线程执行的。

为啥redis用单线程?
避免多线程模式绵连的共享资源的并发访问控制问题。

为啥Redis用单线程都这么快?

  • 内存DB
  • 采用多路复用机制
    redis网络框架采用epoll机制。socket网络模型本身支持非阻塞模式,具体体现在3个函数(为什么说函数?康康之前自己写的网络是如何运行的吧)调用上:socket() listen() accept()。调用时,客户端没反应可不等,不会阻塞,因为用了多路复用机制。Redis使用Linux的select/epoll机制:一个线程处理多个IO流,改机制语序内核中同时存在多个监听套接字和已连接套接字,一旦有请求到达就交给redis线程处理。epoll机制,不用操作系统有不同的实现。这里只是拿Linux环境下做例子。监听套接字来了,就事件入队,redis给事件注册回调函数,事件出队则自动执行事件回调函数。

数据在内存,宕机怎么办

宕机的话,数据会丢失,所以数据需要持久化,有两个机制可实现持久化:AOF日志、RDB快照

AOF

AOF好处:

  • 避免出现记录错误命令的情况(因为AOF是append only file,先执行命令再记日志)
  • 不会阻塞当前的写操作 AOF坏处:
  • 执行完命令就宕机的话,命令、数据都丢失
  • 给下一个操作带来阻塞的风险 基于AOF的坏处,就出现了3中写回磁盘(写回DB)的策略:AOF配置项appendfsync的三个可选值:Always/Everysec/No。自己按照业务对高性能、高可靠的需求度来判断用哪个策略。另一方面,命令多了,AOF会越来越大,IO则越麻烦,多疑出现AOF重写机制,可减少日志文件大小。注意,AOF日志由主线程写回,重写过程由后台子进程bgrewriteaof子进程完成。

RDB快照

是某一个时刻的状态。生成RDB文件的两个命令:save(阻塞),bgsave(不会阻塞)。
快照时数据能够被修改,但不能影响redis效率,所以redis借助操作系统提供的写时复制技术(copy-on-write ,COW),在执行快照的同事正常处理写操作。
是不是连拍就好呢?不好。因为虽不导致阻塞单会给磁盘很大压力,而且fork这个创建过程本身会阻塞线程
那,做增量快照吧。
但是会增加额外的空间存元数据信息(什么时候、增了啥,之类的)。
所以,redis4.0提议混合使用AOF日志和内存快照的方法:内存快照以一定的评论执行,在两次快照之间使用AOF日志记录这区间的所有命令操作。注意,在第二次做全量时,AOF要清空,因为此时磁盘数据是最新的了。

redis具有高可靠性

高可靠性指的是:

  • 数据尽量少丢失(AOF/快照)
  • 服务尽量少中断(可以用读写分离的主从库实现)
    那,主从库如何进行第一次同步?replicaof命令(slaveof命令)。

redis消息队列

幂等性:对于同一条消息,消费者受到一次的处理结果和收到多次的处理结果都一样。
对消息队列的要求:消息保序、重复消息处理、消息可靠性保证。
方案:

  • 基于List。用BRPOP,节省CPU开销;BRPOPLPUSH(备份list,针对消息可靠性保证)
  • 基于Streams。XADD SREAD SREADGROUP XPENDING XACK。这个实现比基于list的专业,更靠近KFAKA什么的。

应对变慢的redis

首先查下redis的响应延迟,看是不是redis在延迟。应该基于当前环境下的redis基线性能做判断。基线性能,指的是一个系统在低压力、无干扰下的基本性能,由当前的软硬件配置决定。从2.8.7起,redis-cli提供-intrinsil-latency选项,如果观察到的redis运行时延迟是其基线性能的2倍及以上,就可以认为redis变慢了。想要了解网络对redis性能的影响?用iPerf工具。如果延迟了,要调整网络的流量分配。
那,为啥会慢?

  • redis自身操作特性:慢查询命令(latency manitar工具;解决方案:慢查询命令可用其他命令胎体)、过期key操作(解决方案:在客户端执行排序、交集、并集,不要用SORT/SUNION/SINTER)
  • 文件系统(AOF模式)
  • 操作系统的swap。触发swap的原因:物理机器内存不足
  • 操作系统内存大页。怎么做?查看是否使用内存大页(某个配置参数)->判断->禁用

redis做缓存

在用redis做缓存,要在应用程序中显式调用。单计算机系统的LCC、pagec cache则不用显式调用,因为在构建计算机硬件系统时,已经把LCC和page cache用程序的数据访问路径上了,应用程序访问数据时直接能用上缓存。redis缓存有只读缓存、读写缓存。读写缓存分同步直写和异步回写(等到redis中缓存快被淘汰再写回DB)

那,缓存满了怎么办?
使用缓存数据的淘汰机制(缓存替换机制)。那么要设置多大的缓存容量呢?建议把缓存容量设置为总数据量的15%~30%。兼顾访问性能和内存空间开销。

缓存策略:

  • 不进行数据淘汰:noeviction
  • 进行数据淘汰:
    • 设置了过期时间的数据中进行淘汰:volatile-random volatile-ttl volatile-lru volatile-lfu
    • 在所有数据范围内进行淘汰:allkeys-lru allkeys-random allkeys-lfu

使用redis缓存时,如果data被修改,记得在数据修改时就将它写回DB

缓存异常

  • 缓存中的数据与数据库中的数据不一致:一般产生在“删改数据”中。解决方法:
    • 重试机制(一个线程时)
    • 如果是有其他线程并发操作时:如果是先删除缓存再更新数据库。在线程A更新完db值后,让它sleep一小段时间,再缓存删除操作;如果是先更新db值再删除缓存值,对业务影响小,不讨论~
  • 缓存雪崩:大量的应用请求无法在redis缓存中进行处理,紧接着,应用将大量请求发送到数据库层,导致数据库层压力激增。
    原因一:缓存中有大量数据同时过期。
    解决:设置不同的过期时间

原因二:redis缓存示例故障宕机。
解决:在业务系统中实现服务熔断,请求限流机制;构建Redis缓存高可靠集群

  • 缓存击穿:针对某个访问非常频繁的热点数据的请求,无法再缓存中进行处理,然后,访问该数据的大量请求一下子发给数据库。
    原因:热点数据过期。
    解决:对热点数据不设过期时间

  • 缓存穿透:要访问的数据既不在redsi缓存中也不在数据库中。此时,应用也无法从数据库中读数据再写入缓存,来服务后续请求。这样,缓存变成摆设。这时,缓存和数据库都有很大压力。
    原因:业务层错误操作;恶意攻击
    解决: 针对查询的数据再redis中缓存一个空值或者和业务层协商确定的缺省值;使用布隆过滤器快速判断data是否存在;在请求入口的前端进行请求检测。

redis如何应对高并发访问

但命令操作、LUA脚本。枷锁也能实现临界区代码的互斥执行。只是如果有多个客户端加锁时,需要分布式锁。

redis能实现ACID属性吗

MULTI:开启一个事务

EXEC:提交事务

PISCARD:放弃一个事务,清空命令队列

WATCH:检测一个或多个键的值在事务执行期间是否发生变化,如果发生变化则当前事务放弃执行。

秒杀场景

  • 秒杀前:把商品详情页的页面元素静态化,然后使用CDN或是浏览器把这些静态化的元素缓存起来
  • 秒杀开始:在redis终进行库存扣减。当库存查验完成后,一旦库存有余量,立即在redis扣减库存 怎么做?
    用hash。
    key:itemID
    value:{total:N, ordered:M}。total总库存,ordered已秒杀。
    库存查验、库存扣减要保证原子性,所以用LUA脚本来完成。或者分布式锁。

下节思考问题

数据库、MYSQL、PHP如何检测性能?