Redis——总结
Redis 与 Memcached的区别
Memcached从名字上看就能看出其实它也是一个高性能的内存缓存,那么它与Redis的区别在哪呢?
①Redis支持多数据类型,如String,List,Hash等,Memcached只支持String
②Redis能够周期性地更新数据写入磁盘或者将修改操作写入可记录的文件,也就是Redis有持久化操作,Memcached没有。
③Redis使用的是单线程+IO多路复用技术,而Memcached是使用多线程+锁的技术。
Redis的应用
缓存,分布式会话,分布式锁,排行榜。
Redis的5种数据类型(重要!!!)
①SDS(Simple Dynamic String)
SDS的结构:
redis的3.0版本:(int类型)已使用长度len,(int类型)未使用的长度free,字符数组buf(末尾会跟C一样加上“/0”,这样可以适配C语言的函数如strlen)。
这样会造成一个问题,假如我要存的字符串长度只有4个字节,而字符串头部已经耗了两个int类型变量,这样就有种浪费空间的感觉了。
redis的6.0版本:上述字段,把free字段改为alloc字段,代表总分配的数组长度,再加上一个char类型(1个字节,8位)的字段,用来表示字符串类型,前三位用于表示类型,后5位预留(后5位没用的意思)。所以,前3位可以表示5种类型,分别是5位,8位,16位,32位,64位。5位类型redis几乎不用。8位,16位等代表的是buf数组的长度2的8次方和2的16次方。
Redis为什么不采用字节对齐?
首先,SDS 的指针并不是指向 SDS 的起始位置(len位置),而是直接指向buf[],使得 SDS 可以直接使用 C 语言string.h库中的某些函数,做到了兼容。如8位类型的SDS:
如果不进行对齐填充,那么在获取当前 SDS 的类型时则只需要后退一步即可flagsPointer = ((unsigned char*)s)-1;相反,若进行对齐填充,由于 Padding 的存在,我们在不同的系统中不知道退多少才能获得flags,并且我们也不能将 sds 的指针指向flags,这样就无法兼容 C 语言的函数了,也不知道前进多少才能得到 buf[]。
SDS 是 redis 最基本的类型,你可以理解成与 Memcached 一模一样的类型,一个 key 对应一个 value。
相比于C语言的String类型,SDS的优势:
①SDS类型是二进制安全的。意思是 redis 的 SDS 可以包含任何数据,可以通过len属性。比如jpg图片或者序列化的对象。SDS 类型是 Redis 最基本的数据类型,SDS 类型的值最大能存储 512MB。
②能够O(1)获取字符串长度
③能够杜绝缓冲区溢出:缓冲区溢出是当计算机向缓冲区内填充数据位数时超过了缓冲区本身的容量溢出的数据覆盖在合法数据上,比如调用strcat()函数就有可能造成缓冲区溢出。而SDS通过自动扩容机制能避免这种情况发送。
自动扩容机制:当SDS API需要对SDS进行修改时,API会先检查 SDS 的空间是否满足修改所需的要求,如果不满足,API会自动将SDS的空间扩展至执行修改所需的大小,然后才执行实际的修改操作,所以使用 SDS 既不需要手动修改SDS的空间大小,也不会出现缓冲区溢出问题。
模板:set key value persist key(让该key称为一个永远不会过期的key)
适用场景:
1.缓存: 经典使用场景,把常用信息,字符串,图片或者视频等信息放到redis中。也可以用来缓存结构体的信息,以用户id为key,value为将bean用户信息json化再序列化成字符串。redis作为缓存层,mysql做持久化层,降低mysql的读写压力。
2.计数器:redis是单线程模型,一个命令执行完才会执行下一个,通过incr和decr命令进行自增和自减。
3.session:常见方案spring session + redis实现多服务器的session共享。
4.用作分布式锁
②Hash类型
是一个Map
③List
List是一个双向链表,能实现从链表的头部或尾部添加元素。
模板:lpush listname value
使用场景:
①用来作异步队列,将需要延后处理的任务结构体序列化成字符串塞进 Redis 的列表,另一个线程从这个列表中轮询数据进行处理。
②用于秒杀场景。在秒杀前将本场秒杀的商品放到list中,因为list的pop操作是原子性的,所以即使有多个用户同时请求,也是依次pop,list空了pop抛出异常就代表商品卖完了。
④Set
集合类型也是用来保存多个字符串的元素,但和列表不同的是集合中 1. 不允许有重复的元素,2.集合中的元素是无序的,不能通过索引下标获取元素,3.支持集合间的操作,可以取多个集合取交集、并集、差集。
模板:sadd setname value
使用场景:
①用于一些去重场景。比如用户只能秒杀一次某商品等。
⑤Zset
它类似于 Java 的 SortedSet 和 HashMap 的结合体,一方面它是一个 set,保证了内部 value 的唯一性,另一方面它可以给每个 value 赋予一个 score,代表这个 value 的排序权重。zset内部是通过跳跃列表这种数据结构来实现的。因为zset要支持随机的插入和删除,所以不能使用数组结构,而需要改成普通链表数据结构。zset需要根据score进行排序,所以每次插入或者删除值都需要进行先在链表上查找定位。
跳表结构:
跳表查找过程:假如我要找的是10号元素,先从1的二级索引开始找,找到第一个>= 10的元素的前一个位置,为7,再往下一层索引,找到第一个>=10的元素的前一个位置,为9,再往下一层,找到了。
使用场景:
1.各种热门排序场景(排行榜)。例如小说视频等网站需要对用户上传的小说视频做排行榜,榜单可以按照用户关注数,更新时间,字数等打分,做排行。
Redis事务
Redis事务机制主要由Mutil , Execu , discard 三个命令组成, 先输入mutil命令表示创建一个事务队列,不断往队列中加入命令,加完后执行exec命令对队列中每个命令进行执行,若其间命令有错误,则整个队列进行回滚。
悲观锁和乐观锁:
悲观锁 (Pessimistic Lock),顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会 block 直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
乐观锁 (Optimistic Lock):顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis 就是利用这种 check-and-set(CAS) 机制实现事务的。
在秒杀场景中,Redis就是利用乐观锁解决了超卖问题。
但是这样虽然解决了超卖问题,但是数据库中还有库存,如果我们想要达到正确的效果,实现库存清零的效果,可以利用lua脚本。
lua脚本具有原子性,redis会将整个脚本作为一个整体执行,中间不会被其他命令。通过 lua 脚本解决争抢问题,实际上是 redis 利用其单线程的特性,用任务队列的方式解决多任务并发问题。
Redis持久化(重要!!!)
Redis持久化的方式主要分为两种:RDB与AOF。
RDB (Redis DataBase):在指定的时间间隔内将内存中的数据集快照写入磁盘, 也就是行话讲的 Snapshot 快照,它恢复时是将快照文件直接读到内存里。
RDB的过程:RDB可以通过两种命令进行持久化:save与bgsave,save是主线程通过阻塞客户端命令,令其不能进行任何的IO操作,去生成一个快照文件。而bgsave是通过fork主线程生成一个新的线程专门异步用于生成快照文件,此方法的缺点是消耗内存空间,且最后一次持久化后的数据会丢失。
AOF(Append Only File):以日志的形式来记录每个写操作(增量保存),将 Redis 执行过的所有写指令记录下来 (读操作不记录), 只许追加文件但不可以改写文件,redis 启动之初会读取该文件重新构建数据,换言之,redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。
AOF流程:
①客户端的请求写命令会被 append 追加到 AOF 缓冲区内;
②AOF 缓冲区根据 AOF 持久化策略 [always/everysec/no] 将操作 sync 同步到磁盘的 AOF 文件中;
③AOF 文件大小超过重写策略或手动重写时,会对 AOF 文件 rewrite 重写,压缩 AOF 文件容量;
④Redis 服务重启时,会重新 load 加载 AOF 文件中的写操作达到数据恢复的目的。
RDB与AOF的选择:官网推荐两者都使用,如果对数据不敏感,允许数据部分出错,可以只使用RDB。而每次redis重启后,会优先使用AOF来恢复数据,因为AOF保存的数据比RDB更完整。
Redis主从复制
Redis主从复制的概念:指的是在多个服务器上,其中一个服务器为Master主服务器,主要是应对客户端的写操作;其余的服务器都为Slave从服务器,主要是应对客户端的读操作。这样布置的好处是:读写分离,能最大限度地发挥服务器的性能。在容灾后能快速恢复。
流程:当一个Slave服务器启动后,会向Master服务器发出连接请求。当成功连接后,会发送一个sync信号请求与主服务器进行一次全量复制。Master服务器收到请求后,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,master 将传送整个数据文件到 slave,以完成一次完全同步。此后,就可以用增量复制(主服务器把新收到的命令传给从服务器)来进行数据的同步。
一主多从,这就会涉及到一个问题,如果主服务器宕机了或者是主服务器所在的环境变差了,这就会导致整个Redis系统挂了或者是主从服务器之间通讯的效率和正确性降低。这里,就要引入哨兵系统了。
哨兵机制:即使用一个哨兵利用心跳机制不断检测主服务器是否还存活或者是通过发出去的包的数量和收回来的包的数量进行比较判断主服务器的网络环境情况,如果是未收到回信或者是收到的回信数量较少,就启动哨兵系统里的更多的哨兵去验证这一情况,如果情况属实,将会在众多的从服务器中重新选一个出来当选主服务器,旧的主服务器重启后就变为从服务器。而选择的条件是:①选择优先级最大的(可以在redis.conf里面设置从服务器的优先级)。
Redis集群模式
集群解决了当Redis数据库不断扩大,数据所占内存不断增多,进行AOF或者RDB的效率越来越慢的时候,我们可以把数据库进行分片,分治处理。这样既能最大化地利用服务器的性能,又能保证即使某一个服务器宕机了,其他部分区域的数据仍然可以使用的高可用性。
简单写一下集群: 既把一个Redis集群分为16384个插槽(hash-slot),每一个key都有其专属的存放槽,每一个服务器负责一定范围的槽。当客户端有值查询的时候,redis会计算出这个key值所属的槽,然后告诉客户端该槽属于哪个服务器,去那个服务器查询。
Redis的应用问题
缓存穿透
定义:用户需要查询的key不存在,每次针对该key的请求都在缓存中查不到,就打到数据库中。比如:黑客利用一个完全不存在的key去获取value,每次都会到数据库中访问。
解决办法:
①对空值进行缓存。对针对不正常的key值,查询数据库后返回的null进行储存,即<key,null>键值对。
②使用布隆过滤器过滤掉不正常的请求。可以先对缓存进行一个预热,再用布隆过滤器当作缓存的索引,如果key在布隆过滤器中,才会查询缓存,缓存中没有,才去查询数据库;而如果key不在布隆过滤器中,直接返回null。
缓存击穿
定义:当缓存中的某一个key过期后,大量的并发请求打在缓存上,查询不到该key后,大量请求打到了db上。
解决办法:
①进行数据预热
②设置key的过期时间永不过期或者是合理地设置过期时间。
③设置排它锁:当在缓存上查询不到某一个key时,设置一个排它锁(set key_lock 1 EX 1 Second),然后进入数据库进行获取值和设置key的缓存,释放排它锁(del key_lock)。别的线程想进入db中查询key值时得先拿到排它锁(成功设置排它锁的值),再进入,如果拿不到就让它睡眠一段时间,重新获取锁。
缓存雪崩
定义:缓存击穿的升级版,当存在大量的key过期后,大量的请求打在缓存上,进而打在数据库上,引发数据库宕机。
解决办法:
①在缓存过期时间上加一个随机值,让key的失效时间均匀分布
②使用多级缓存策略,nginx缓存+redis缓存+备用缓存,备用缓存设置key的过期时间久一点,这样当主缓存中的key失效时,可以去访问备用缓存。
③当是因为redis服务器整个宕机而导致大片缓存失效时,可以使用redis的主从复制机制来恢复缓存的可用性。
缓存一致性问题(重要!!!)
如果针对的是缓存一致性要求不高的场景,其实只需要设置缓存的过期时间就行。
现在应对redis缓存一致性问题的主要存在两种方案:
①先删除缓存,再更新数据库
②先更新数据库,再删除缓存
(补充:为什么是删除缓存而不是更新缓存?因为如果是更新缓存,意味着每一次数据库的更新,缓存都需要更新一次;而如果我在这期间只需要读一次该缓存,是浪费资源的。而如果是删除缓存,既我需要读该缓存的时候,我才去数据库读取,只需要做一次缓存删除)
针对方案①:
存在的问题:如果当前线程需要修改某个key的value值,于是把缓存删了,去更新数据库的途中没有时间片了,暂时挂起。此时另外一个线程过来,在缓存中查询不到key,去数据库中把旧值给取回来,设置到了缓存上,此时之前那个线程活了,把新的值放入数据库,这就出现了缓存与db数据不一致的情况。
解决方案:采用延时双删策略。即在更新完数据库后,线程sleep一段时间,再把缓存删了。
针对方案②:
存在的问题:如果我先更新了数据库,却删除缓存失败,这就会导致缓存的旧值与数据库的新值不一致。又或者是线程A去数据库读取某个key的value值,读取后,挂起;线程B来修改该key的value值,然后删除key的缓存,此时线程A唤醒,把旧值设置在缓冲中,造成数据不一致(这种情况概率比较低,因为读操作一般比写操作速度快)。
解决方案:强一致性的解决方案:通过redis订阅mysql的binlog日志来进行缓存的设置。流程:更新mysql数据库,产生一条binlog日志,发送至redis程序,放入消息队列中,进行对应的key的删除操作。(消息队列可以保证删除操作的成功性)。可以加以设置key的过期时间来辅助。
Redis分布式锁的实现
分布式锁的定义:Java的锁(如lock,synchronized)只能保证单机的时候有效,分布式集群环境就无能为力了,这个时候我们就需要用到分布式锁。当多个进程不在同一个系统中,用分布式锁控制多个进程对资源的访问。
具体流程:①SET key value NX EX max-lock-time (NX为if not exist , EX为expire 设置过期时间)
当某个线程进入redis后,会通过setex命令设置key为lock的值对应的value值为该线程的唯一标识,一般用uuid随机生成,且过期时间是多少。这两个步骤通过setex命令进行设置是原子性的。此时,如果有新的线程进入redis时,先通过set命令进行设置,发现key已存在,则返回false,sleep一段时间。然后,旧的线程设置完key为lock的值后,如何删除值(释放锁)呢?通过lua脚本(确保原子性)来执行判断当前线程是锁的持有者,再进行锁的删除。
②也可以使用Redission框架来实现分布式锁。
Redission实现分布式锁原理解析:
①加锁原理:通过lua脚本进行锁的设置,如 锁是Hash类型的,<“mylock”,<“123456789:52”,1>,mylock是锁的key,123456789是线程的guid,52是线程的id,1代表锁重入的次数。还有key的过期时间,初始化为30s。
②锁互斥原理:此时,如果当第二个线程进来时,先去判断是否存在mylock这个key,如果存在再去判断里面的线程id是否是自身(是否是可重入锁),如果不是,则返回该key还剩的ttl(存活时间)。
③WatchDog机制(锁延长机制):如果当前线程还没有执行完它的业务逻辑时,key为mylock的锁过期了,此时就会启动WatchDog机制判断当前线程是否还持有锁,自动延长锁的时间。(如果redis宕机了,WatchDog机制也会失效,锁同样会释放,此时另一个线程就可以拿到锁。)
④可重入锁原理:和互斥原理类似,只不过当判断到线程di是否是自身时,此时是自身,则让锁重入的次数+1,直接获取锁。
⑤释放锁原理:即通过lua脚本删除key,广播删除锁的消息,通知阻塞等待的线程,取消WatchDog机制。
存在的缺点:通过WatchDog机制那里我们就可知道,当某一个线程在一个Master服务器上进行操作时,宕机了,此时通过哨兵模式会自动更换Master节点,另一个线程就可以在新的Master节点中进行操作,导致了多个客户端对一个分布式锁进行加锁,从而产生了脏数据。
Redis 在内存不足时,淘汰机制是怎样的?
我们缓存空间一般不会把数据库中所有数据都缓存下来,所以,随着程序的进行,缓存的空间越来越大,甚至超过了其自身的容量。那么,此时,我们应该如何去淘汰旧数据呢?
Redis缓存空间一般设置多大?
“八二原理”:即80%的请求能在占20%空间的缓存中解决。所以我们的redis一般根据业务的需求设置在15%——30%之间。
淘汰策略
一般分为4种:随机删除,不删除,LRU,LFU,默认是不删除策略。
①随机删除:显然,随机删除不是一种很好的策略,万一把客户端请求频繁的数据删了,把不怎么访问的数据留了下来,这样是不高效的。
②不删除:等到redis空间满了,自动报错。
③LRU算法:最近最少使用算法:定义了一个双向链表,把最近使用过的数据使用头插法插入到链表头,当链表满了的时候,优先淘汰位于链表尾的数据。
④LFU算法:最不经常使用算法:它是基于LRU算法且给每个数据加了一个访问次数的字段,先根据访问次数做排序,再根据访问时间作排序。
为什么有了LRU算法还需要LFU算法?
假设有这样的场景,某个数据在一段时间内正被频繁访问,但有些数据被访问的次数非常少,甚至只会被访问一次,如果此时采用LRU算法,频繁访问的数据就有可能被缓存给淘汰,这是不合理的。
聊聊Redis的线程模型
Redis线程模型
关键:多路复用器(在Netty中是多线程的selector模型 ,在Redis中是单线程的文件事件处理器),这个组件是单线程的。
多路复用过程:
①在Redis启动初始化的时候,连接应答处理器会先去调用epoll_create()底层方法创建一个文件epoll,并往文件中调用epoll_ctl()方法往文件中注册一个连接事件;
②当有一个客户端发起连接时,会产生一个连接事件,然后触发回调函数将该socket 加入到就绪队列中,等待调用epoll_wait()方法。当调用epoll_wait()方法,会从队列中取出socket,根据其绑定的事件,分派到不同的处理器中进行处理。如绑定了连接事件,则分派到连接应答处理器处服务端与客户端建立连接,创建客户端对应的socket,同时将这个socket的AE_READABLE(可读)事件注册到epoll文件中。
③当客户端向Redis发生请求时(读或写),会使得该socket中产生可读事件,将该socket分派到命令请求处理器中,把该事件传入线程池中运行(读取客户端的命令内容,传给相关的程序去执行)。
④当Redis服务器准备好返回给客户端的响应数据后,会将客户端socket的可写事件注册到epoll文件中,当客户端准备好读取数据时,会在socket中产生一个可写事件,触发命令回复处理器将响应数据写入socket,供客户端读取。
⑤返回数据后,注销epoll文件中socket的可写事件。
(ps:Netty的多线程模型即每个处理器都有自己的一份epoll文件已经就绪队列,从而实现多线程并发)
Redis为什么是单线程的?
①因为Redis是基于内存进行操作的,不是基于数据库进行操作的,不需要进行IO处理,且不会产生阻塞,CPU执行的效率高。如果是多线程的话,进行频繁的线程切换会带来不必要的损耗。
②Redis采用的是单线程IO多路复用机制。
Redis的单线程模型为什么快?
①纯内存操作
②核心是基于非阻塞的IO多路复用机制
③单线程避免了多线程频繁上下文切换带来的性能问题。