Redis理论

Redis理论

Redis概念说明

1)REmote DIctionary Server(Redis) 是一个由Salvatore Sanfilippo写的key-value存储系统。是一个开源的使用ANSI C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。它通常被称为数据结构服务器,因为值可以是字符串、哈希、列表、集合和有序集合等类型。

2)Redis是一个key-value的NoSQL数据库,先存到内存中,会根据一定的策略持久化到磁盘,即使断电也不会丢失数据。主要用来做缓存数据库和web集群时当做中央缓存存放seesion。

3)Redis,英文全称是Remote Dictionary Server(远程字典服务),是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。与MySQL数据库不同的是,Redis的数据是存在内存中的。它的读写速度非常快,每秒可以处理超过10万次读写操作。因此redis被广泛应用于缓存,另外,Redis也经常用来做分布式锁。除此之外,Redis支持事务、持久化、LUA脚本、LRU驱动事件、多种集群方案。

4)Redis的出色之处不仅仅是性能,Redis最大的魅力是支持保存多种数据结构,此外单个value的最大限制是1GB,不像memcached只能保存1MB的数据,另外Redis也可以对存入的Key-Value设置expire时间。Redis的主要缺点是数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。

Redis使用场景

1)缓存
我们一提到redis,自然而然就想到缓存,国内外中大型的网站都离不开缓存。合理的利用缓存,比如缓存热点数据,不仅可以提升网站的访问速度,还可以降低数据库DB的压力。并且,Redis相比于memcached,还提供了丰富的数据结构,并且提供RDB和AOF等持久化机制,强的一批。

2)排行榜
当今互联网应用,有各种各样的排行榜,如电商网站的月度销量排行榜、社交APP的礼物排行榜、小程序的投票排行榜等等。Redis提供的zset数据类型能够实现这些复杂的排行榜。比如,用户每天上传视频,获得点赞的排行榜可以这样设计:
用户Jay上传一个视频,获得6个赞:
zadd user:ranking:2021-03-03 Jay 6
过了一段时间,再获得一个赞,可以这样:
zincrby user:ranking:2021-03-03 Jay 1
如果某个用户John作弊,需要删除该用户:
zrem user:ranking:2021-03-03 John
展示获取赞数最多的3个用户
zrevrangebyrank user:ranking:2021-03-03 0 2

3)计数器
各大网站、APP应用经常需要计数器的功能,如短视频的播放数、电商网站的浏览数。这些播放数、浏览数一般要求实时的,每一次播放和浏览都要做加1的操作,如果并发量很大对于传统关系型数据的性能是一种挑战。Redis天然支持计数功能而且计数的性能也非常好,可以说是计数器系统的重要选择。

4)共享Session、会话缓存(Session Cache)
如果一个分布式Web服务将用户的Session信息保存在各自服务器,用户刷新一次可能就需要重新登录了,这样显然有问题。实际上,可以使用Redis将用户的Session进行集中管理,每次用户更新或者查询登录信息都直接从Redis中集中获取。

5)分布式锁
几乎每个互联网公司中都使用了分布式部署,分布式服务下,就会遇到对同一个资源的并发访问的技术难题,如秒杀、下单减库存等场景。用synchronize或者reentrantlock本地锁肯定是不行的。如果是并发量不大话,使用数据库的悲观锁、乐观锁来实现没啥问题。但是在并发量高的场合中,利用数据库锁来控制资源的并发访问,会影响数据库的性能。实际上,可以用Redis的setnx来实现分布式的锁。除此之外,还可以使用官方提供的RedLock分布式锁实现。

6)社交网络
赞/踩、粉丝、共同好友/喜好、推送、下拉刷新等是社交网站的必备功能,由于社交网站访问量通常比较大,而且传统的关系型数据不太适保存这种类型的数据,Redis提供的数据结构可以相对比较容易地实现这些功能。

7)消息队列(发布、订阅)
消息队列是大型网站必用中间件,如ActiveMQ、RabbitMQ、Kafka等流行的消息队列中间件,主要用于业务解耦、流量削峰及异步处理实时性低的业务。Redis提供了发布/订阅及阻塞队列功能,能实现一个简单的消息队列系统。另外,这个不能和专业的消息中间件相比。

8)位操作
用于数据量上亿的场景下,例如几亿用户系统的签到,去重登录次数统计,某用户是否在线状态等等。腾讯10亿用户,要几个毫秒内查询到某个用户是否在线,能怎么做?千万别说给每个用户建立一个key,然后挨个记(你可以算一下需要的内存会很恐怖,而且这种类似的需求很多。这里要用到位操作(使用setbit、getbit、bitcount命令)。原理是:redis内构建一个足够长的数组,每个数组元素只能是0和1两个值,然后这个数组的下标index用来表示用户id(必须是数字),那么很显然,这个几亿长的大数组就能通过下标和元素值(0和1)来构建一个记忆系统。

9)全页缓存(FPC)
除基本的会话token之外,Redis还提供很简便的FPC平台。回到一致性问题,即使重启了Redis实例,因为有磁盘的持久化,用户也不会看到页面加载速度的下降,这是一个极大改进,类似PHP本地FPC。再次以Magento为例,Magento提供一个插件来使用Redis作为全页缓存后端。此外,对WordPress的用户来说,Pantheon有一个非常好的插件wp-redis,这个插件能帮助你以最快速度加载你曾浏览过的页面。

Redis实现消息队列

Redis中的订阅、发布实现了发布/订阅消息范式,发布者不是计划发送消息给特定的订阅者,而是发布消息到不同的频道,发布者不需要知道是哪些订阅者订阅了消息。订阅者对一个或多个频道感兴趣,只需接收感兴趣的消息,不需要知道是什么样的发布者发布的消息。这种发布者和订阅者的解耦合可以带来更大的扩展性和更加动态的网络拓扑。断开后的订阅者重新订阅后会丢失断开期间发布者发布的消息。在集群模式中,发布者发布消息后的返回值取决于订阅者与发布者在不在同一个节点上,发布者发布消息后返回值为与发布者相同节点当前订阅了该频道的客户端数量。也可以采用队列的形式发布到Redis,或者使用ZSet(sortedset)实现延时队列,拿时间戳作为score(分数),消息内容作为key,调用zadd来生产消息,消费者用zrangebyscore指令获取N秒之前的数据轮询进行处理。

在Redis的发布订阅模式中,有三个部分:
Publisher(发布者):发送消息到频道中,每次只能往一个频道发送一条消息。
Subscriber(订阅者):订阅频道,订阅者可以同时订阅多个频道。
Channel(频道):将发布者发布的消息转发给当前订阅此频道的订阅者。

=============================================================
发布订阅的命令如下:
发布消息到指定的频道
PUBLISH channel message

订阅给定的一个或多个频道的信息
SUBSCRIBE channel [channel ...]

订阅一个或多个符合给定模式的频道
PSUBSCRIBE pattern [pattern ...] 

指退订给定的频道
UNSUBSCRIBE [channel [channel ...]] 

退订所有给定模式的频道
PUNSUBSCRIBE [pattern [pattern ...]] 

查看订阅与发布系统状态
PUBSUB subcommand [argument [argument ...]]

Redis序列化器

1)普通的连接使用没有办法把Java对象直接存入Redis,而需要我们自己提供方案,如对象序列化,然后存入Redis,取回序列化内容后,转换为java对象。Spring模板中提供了封装的方案,在它内部提供了RedisSerializer接口和一些实现类。也可以自定义序列化器,实现RedisSerializer接口。常用的有:StringRedisSerializer、JdkSerializationRedisSerializer、GenericToStringSerializer等。

2)RedisTemplate默认的序列化类是JdkSerializationRedisSerializer,用JdkSerializationRedisSerializer序列化的话,被序列化的对象必须实现Serializable接口。在存储内容时,除了属性的内容外还存了其它内容在里面,总长度长,且不容易阅读。

3)我们要求是存储的数据可以方便查看,也方便反系列化,方便读取数据。Jackson2JsonRedisSerializer和GenericJackson2JsonRedisSerializer,两者都能序列化成json,但是后者会在json中加入@class属性,类的全路径包名,方便反序列化。前者如果存放了List则在反系列化的时候没指定TypeReference则会报错:java.util.LinkedHashMap cannot be cast to。

Redis默认内存大小

Redis内存大小的默认值取决于所使用的Redis版本。对于Redis2.4及更早的版本,内存最大值为64MB,在64位系统中可以达到3GB。但是,Redis2.6和更高版本内存默认为最大物理内存的一半(未证实),这是可以根据实际情况调整的。如果不设置maxmemory或者设置为0,64位系统不限制内存,32位系统最多使用3GB内存。

实际上,Redis内存大小不是固定的,可以通过配置文件或者命令行参数来进行调整。首先,通过配置文件调整Redis内存大小,需要找到redis.conf文件,并通过修改maxmemory参数来修改Redis内存大小。另外,还可以使用命令行参数来改变Redis的最大内存限制,例如向Redis服务器传递--maxmemory 1GB的参数,即将最大内存设置为1GB。

总而言之,Redis内存大小是可以根据实际情况来调整的,但是需要注意的是,不要将Redis的内存设置得过大,以免导致服务器出现内存溢出等问题。因此,在进行Redis内存设置时,需要仔细考虑,根据实际需要进行微调,以保证Redis服务器的运行稳定。

Redis数据结构类型

五种基本数据类型

1)String(字符串)
简介:String是Redis最基础的数据结构类型,它是二进制安全的,可以存储图片或者序列化的对象,值最大存储为512M。
简单使用举例:set key value、get key等。
应用场景:共享session、分布式锁,计数器、限流。
内部编码:有3种,int(8字节长整型)/embstr(小于等于39字节字符串)/raw(大于39个字节字符串)。

2)Hash(哈希)
简介:在Redis中,哈希类型是指v(值)本身又是一个键值对(k-v)结构
简单使用举例:hset key field value 、hget key field
内部编码:ziplist(压缩列表) 、hashtable(哈希表)
应用场景:缓存用户信息等。
注意点:如果开发使用hgetall,哈希元素比较多的话,可能导致Redis阻塞,可以使用hscan。而如果只是获取部分field,建议使用hmget。

3)List(列表)
简介:列表(list)类型是用来存储多个有序的字符串,一个列表最多可以存储2^32-1个元素。
简单实用举例:lpush key value [value ...] 、lrange key start end
内部编码:ziplist(压缩列表)、linkedlist(链表)
应用场景:消息队列,文章列表

4)Set(集合)
简介:集合(set)类型也是用来保存多个的字符串元素,但是不允许重复元素
简单使用举例:sadd key element [element ...]、smembers key
内部编码:intset(整数集合)、hashtable(哈希表)
注意点:smembers和lrange、hgetall都属于比较重的命令,如果元素过多存在阻塞Redis的可能性,可以使用sscan来完成。
应用场景:用户标签,生成随机数抽奖、社交需求。

5)有序集合(zset)
简介:已排序的字符串集合,同时元素不能重复
简单格式举例:zadd key score member [score member ...],zrank key member
底层内部编码:ziplist(压缩列表)、skiplist(跳跃表)
应用场景:排行榜,社交需求(如用户点赞)。

三种特殊数据类型

1)Geo:Redis3.2推出的,地理位置定位,用于存储地理位置信息,并对存储的信息进行操作。
2)HyperLogLog:用来做基数统计算法的数据结构,如统计网站的UV。
3)Bitmaps:用一个比特位来映射某个元素的状态,在Redis中,它的底层是基于字符串类型实现的,可以把bitmaps成作一个以比特位为单位的数组

Redis问题解决方案

缓存穿透

20200710100844985

布隆过滤器示意图:

20200710104118632

缓存穿透解决方案:

问题说明(Redis和数据库都没有数据):当查询Redis中没有的数据时,该查询会下沉到数据库层,同时数据库层也没有该数据,当这种情况大量出现或被恶意攻击时,接口的访问全部透过Redis访问数据库,而数据库中也没有这些数据,我们称这种现象为"缓存穿透"。缓存穿透会穿透Redis的保护,提升底层数据库的负载压力,同时这类穿透查询没有数据返回也造成了网络和计算资源的浪费。

产生原因:
1)业务不合理的设计,比如大多数用户都没开守护,但是你的每个请求都去缓存,查询某个userid查询有没有守护。
2)业务/运维/开发失误的操作,比如缓存和数据库的数据都被误删除了。
3)黑客非法请求攻击,比如黑客故意捏造大量非法请求,以读取不存在的业务数据。

解决方案:
1)在接口访问层对用户做校验,如接口传参、登陆状态、n秒内访问接口的次数。

2)利用布隆过滤器(优先校验数据库是否存在,存在才查询),将数据库层有的数据key存储在位数组中,以判断访问的key在底层数据库中是否存在;布隆过滤器可以判断key一定不在集合内以及key极有可能在集合内。基于布隆过滤器,我们可以先将数据库中数据的key存储在布隆过滤器的位数组中,每次客户端查询数据时先访问Redis:如果Redis内不存在该数据,则通过布隆过滤器判断数据是否在底层数据库内;如果布隆过滤器告诉我们该key在底层库内不存在,则直接返回null给客户端即可,避免了查询底层数据库的动作;如果布隆过滤器告诉我们该key极有可能在底层数据库内存在,那么将查询下推到底层数据库即可。

布隆过滤器原理:它由初始值为0的位图数组和N个哈希函数组成。一个对一个key进行N个hash算法获取N个值,在比特数组中将这N个值散列后设定为1,然后查的时候如果特定的这几个位置都为1,那么布隆过滤器判断该key存在。

注意:布隆过滤器有误判率,虽然不能完全避免数据穿透的现象,但已经可以将99.99%的穿透查询给屏蔽在Redis层了,极大的降低了底层数据库的压力,减少了资源浪费。Bloom-Filter一般用于在大数据量的集合中判定某元素是否存在。

3)另外也有一个更为简单粗暴的方法,如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。通过这个直接设置的默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库,这种办法最简单粗暴。

缓存击穿

20200710115525279

互斥锁示意图:

20200710134906630

缓存击穿解决方案:

问题说明(Redis没有数据,数据库有数据,热点KEY失效):缓存击穿和缓存穿透从名词上可能很难区分开来,它们的区别是:穿透表示底层数据库没有数据且缓存内也没有数据,击穿表示底层数据库有数据而缓存内没有数据。当热点数据key从缓存内失效时,大量访问同时请求这个数据,就会将查询下沉到数据库层,此时数据库层的负载压力会骤增,我们称这种现象为"缓存击穿"。

解决方案:
1)延长热点key的过期时间或者设置永不过期,如排行榜,首页等一定会有高并发的接口。

2)利用互斥锁保证同一时刻只有一个客户端可以查询底层数据库的这个数据,一旦查到数据就缓存至Redis内,避免其他大量请求同时穿过Redis访问底层数据库。

在使用互斥锁的时候需要避免出现死锁或者锁过期的情况:
1)使用lua脚本或事务将获取锁和设置过期时间作为一个原子性操作(如:set kk vv nx px 30000),以避免出现某个客户端获取锁之后宕机导致的锁不被释放造成死锁现象。
2)另起一个线程监控获取锁的线程的查询状态,快到锁过期时间时还没查询结束则延长锁的过期时间,避免多次查询多次锁过期造成计算资源的浪费。

缓存雪崩

img

分散失效示意图:

img

分散请求时间示意图:

img

缓存雪崩解决方案:

问题说明(Redis没有数据,数据库有数据,大量KEY失效):缓存雪崩是缓存击穿的"大面积"版,缓存击穿是数据库缓存到Redis内的热点数据失效导致大量并发查询穿过Redis直接击打到底层数据库,而缓存雪崩是指Redis中大量的key几乎同时过期,然后大量并发查询穿过Redis击打到底层数据库上,此时数据库层的负载压力会骤增,我们称这种现象为"缓存雪崩"。事实上缓存雪崩相比于缓存击穿更容易发生,对于大多数公司来讲,同时超大并发量访问同一个过时key的场景的确太少见了,而大量key同时过期,大量用户访问这些key的几率相比缓存击穿来说明显更大。

解决方案:
1)在可接受的时间范围内随机设置key的过期时间,分散key的过期时间,以防止大量的key在同一时刻过期。

2)对于一定要在固定时间让key失效的场景(例如每日12点准时更新所有最新排名),可以在固定的失效时间在接口服务端设置随机延时,将请求的时间打散,让一部分查询先将数据缓存起来。

3)延长热点key的过期时间或者设置永不过期,这一点和缓存击穿中的方案一样。

4)大多数系统设计者考虑用加锁(最多的解决方案)或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。

缓存预热

缓存预热如字面意思,当系统上线时,缓存内还没有数据,如果直接提供给用户使用,每个请求都会穿过缓存去访问底层数据库,如果并发大的话,很有可能在上线当天就会宕机,因此我们需要在上线前先将数据库内的热点数据缓存至Redis内再提供出去使用,这种操作就成为"缓存预热"。缓存预热的实现方式有很多,比较通用的方式是写个批任务,在启动项目时或定时去触发将底层数据库内的热点数据加载到缓存内。或者直接写个缓存刷新页面,上线时手工操作下,或者定时刷新缓存。

缓存更新

img

缓存更新解决方案:

缓存服务(Redis)和数据服务(底层数据库)是相互独立且异构的系统,在更新缓存或更新数据的时候无法做到原子性的同时更新两边的数据,因此在并发读写或第二步操作异常(第二步操作异常:缓存和数据的操作顺序中,第二个动作报错。如数据库被更新,此时失效缓存的时候出错,缓存内数据仍是旧版本)时会遇到各种数据不一致的问题。如何解决并发场景下更新操作的双写一致是缓存系统的一个重要知识点。缓存更新的设计模式有四种:
1)Cache aside(延时双删,推荐)
查询操作:先查缓存,缓存没有就查数据库,然后加载至缓存内。
更新操作:先更新数据库,然后让缓存失效,或者先失效缓存然后更新数据库。实现起来较简单,但需要维护两个数据存储,一个是缓存(Cache),一个是数据库(Repository)。为了避免在并发场景下,多个请求同时更新同一个缓存导致脏数据,因此不能直接更新缓存而是令缓存失效。推荐使用先失效缓存,后更新数据库,配合延迟失效来更新缓存的模式。

先更新数据库后失效缓存:并发场景下,推荐使用延迟失效(写请求完成后给缓存设置1s过期时间),在读请求缓存数据时若redis内已有该数据(其他写请求还未结束)则不更新。当redis内没有该数据的时候(其他写请求已令该缓存失效),读请求才会更新redis内的数据。这里的读请求缓存数据可以加上失效时间,以防第二步操作异常导致的不一致情况。

先失效缓存后更新数据库:并发场景下,推荐使用延迟失效(写请求开始前给缓存设置1s过期时间),在写请求失效缓存时设置一个1s延迟时间,然后再去更新数据库的数据,此时其他读请求仍然可以读到缓存内的数据,当数据库端更新完成后,缓存内的数据已失效,之后的读请求会将数据库端最新的数据加载至缓存内保证缓存和数据库端数据一致性;在这种方案下,第二步操作异常不会引起数据不一致,例如设置了缓存1s后失效,然后在更新数据库时报错,即使缓存失效,之后的读请求仍然会把更新前的数据重新加载到缓存内。

2)Read through(读取数据时更新,失效才重新缓存)
在查询操作中更新缓存,即当缓存失效时,Cache Aside模式是由调用方负责把数据加载入缓存,而Read Through则用缓存服务自己来加载。

3)Write through(更新数据时,只更新一处,后面通过其他方式同步)
在更新数据时发生。当有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后由缓存自己更新数据库。和Read through类似,只需要维护一个数据存储(缓存),但是实现起来要复杂一些。

4)Write behind caching(更新数据时,只更新缓存,不更新数据库,后面异步同步数据)
俗称write back,在更新数据的时候,只更新缓存,不更新数据库,缓存会异步地定时批量更新数据库。与Read/Write Through类似,区别是Write Behind Caching的数据持久化操作是异步的,但是Read/Write Through更新模式的数据持久化操作是同步的。优点是直接操作内存速度快,多次操作可以合并持久化到数据库。缺点是数据可能会丢失,例如系统断电等。

总结:
1)延时双删
更新数据库时,先删除缓存,然后更新数据库,这时延迟1秒再次删除缓存(防止读的时候重新缓存数据)。延迟时间 = 读业务逻辑数据的耗时 + 几百毫秒

2)删除缓存重试机制
因为延时双删可能会存在第二步的删除缓存失败,导致的数据不一致问题。可以使用这个方案优化,删除失败就多删除几次,保证删除缓存成功就可以了,所以可以引入删除缓存重试机制。失败时将缓存KEY放到消息队列,然后重试删除操作。

3)读取biglog异步删除缓存
重试删除缓存机制还可以吧,就是会造成好多业务代码入侵。其实,还可以这样优化:通过数据库的binlog来异步淘汰key。可以使用阿里的canal将binlog日志采集发送到MQ队列里面,然后通过ACK机制确认处理这条更新消息,删除缓存,保证数据缓存一致性。

缓存降级

缓存降级是指当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,即使是有损部分其他服务,仍然需要保证主服务可用。可以将其他次要服务的数据进行缓存降级,从而提升主服务的稳定性。降级的目的是保证核心服务可用,即使是有损的。如去年双十一的时候淘宝购物车无法修改地址只能使用默认地址,这个服务就是被降级了,这里阿里保证了订单可以正常提交和付款,但修改地址的服务可以在服务器压力降低,并发量相对减少的时候再恢复。降级可以根据实时的监控数据进行自动降级也可以配置开关人工降级。是否需要降级,哪些服务需要降级,在什么情况下再降级,取决于大家对于系统功能的取舍。例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。

模糊查询大量Key

假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,使用keys指令可以扫出指定模式的key列表。由于Redis的单线程的。keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。

Redis持久化方式

Redis持久化方式之RDB

RDB持久化,是指在指定的时间间隔内,执行指定次数的写操作,将内存中的数据集快照写入磁盘中,它是Redis默认的持久化方式。执行完操作后,在指定目录下会生成一个dump.rdb文件,Redis 重启的时候,通过加载dump.rdb文件来恢复数据。

RDB触发机制主要有以下几种:
1)save命令,同步,会阻塞当前Redis服务器,直到持久化完成,线上应该禁止使用。
2)bgsave命令,异步,该触发方式会fork一个子进程,由子进程负责持久化过程,因此阻塞只会发生在fork子进程的时候。
3)自动触发:save m n,在m秒内存在n次修改,自动触发bgsave。

=============================================================
在redis.windows.conf文件中有如下配置:
# 如果想要禁用RDB配置,只需要在save的最后一行写上:save ""
# 在900秒(15分钟)之后,如果至少有1个key发生变化,则dump内存快照
save 900 1
# 在300秒(5分钟)之后,如果至少有10个key发生变化,则dump内存快照
save 300 10
# 在60秒(1分钟)之后,如果至少有10000个key发生变化,则dump内存快照
save 60 10000

# 文件名称
dbfilename dump.rdb

# 文件保存路径
dir /home/work/app/redis/data/

# 如果持久化出错,主进程是否停止写入
stop-writes-on-bgsave-error yes

# 是否压缩
rdbcompression yes

# 导入时是否检查
rdbchecksum yes

Redis持久化方式AOF

AOF(append only file)持久化,采用日志的形式来记录每个写操作,追加到文件中,重启时再重新执行AOF文件中的命令来恢复数据。它主要解决数据持久化的实时性问题。默认是不开启的。通过AOF每秒钟将对数据库操作的命令保存到磁盘上,性能慢,完整性高。开启了AOF之后,RDB就默认不使用了。会在当前reids目录下生成一个aof文件。

手动触发方式:bgrewriteaof命令和rewriteaof命令。

RDB与AOF对比:
不论是RDB还是AOF都是先写入一个临时文件,然后通过rename完成文件的替换工作。如果实时写入磁盘会带来非常高的磁盘IO,影响整体性能。快照类似将当前的内容拍照保存到本地,因为有时间间隔,所以时间段内的数据不能保存;日志文件保存的话,由于保存了对redis的所有操作,同时也将不必要的数据也保存下来,增加了文件的大小。

=============================================================
在redis.windows.conf文件中有如下配置:
# 改为appendonly yes则开启aof存储
appendonly no

# 文件名称
appendfilename "appendonly.aof"

# 同步方式,3选1
appendfsync always (每次操作都持久化)
appendfsync everysec (每隔一秒持久化)
appendfsync no (不进行持久化)

# aof重写期间是否同步
no-appendfsync-on-rewrite no

# 重写触发配置
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

# 加载aof时如果有错如何处理,如果该配置启用,在加载时发现aof尾部不正确时,会向客户端写入一个log,但是会继续执行,如果设置为no ,发现错误就会停止,必须修复后才能重新加载。
aof-load-truncated yes

# 文件重写策略
aof-rewrite-incremental-fsync yes

Redis持久化方式之RDB+AOF

使用RDB持久化会有数据丢失的风险,但是恢复速度快,而使用AOF持久化可以保证数据完整性,但恢复数据的时候会很慢。于是从Redis4之后新增了混合AOF和RDB的模式,先使用RDB进行快照存储,然后使用AOF持久化记录所有的写操作,当重写策略满足或手动触发重写的时候,将最新的数据存储为新的RDB记录。这样的话,重启服务的时候会从RDB和AOF两部分恢复数据,即保证了数据完整性,又提高了恢复的性能。在redis.windows.conf文件中有如下配置:
aof-use-rdb-preamble yes

开启混合模式后,每当bgrewriteaof命令之后会在AOF文件中以RDB格式写入当前最新的数据,之后的新的写操作继续以AOF的追加形式追加写命令。当redis重启的时候,加载aof文件进行恢复数据:先加载rdb的部分再加载剩余的aof部分。

Redis数据恢复原理

1500187847-5b70e0fd040ac_fix732

恢复原理说明:

启动时会先检查AOF文件是否存在,如果不存在就尝试加载RDB。优先加载AOF是因为AOF保存的数据更完整,通过上面的分析我们知道AOF基本上最多损失1s的数据。最新的Redis是通过是否开启AOF来加载文件,若开启了AOF,则使用AOF持久化文件恢复数据,否则使用RDB持久化文件恢复数据。

Redis生产持久化方案

1)如果Redis中的数据并不是特别敏感或者可以通过其它方式重写生成数据,可以关闭持久化,如果丢失数据可以通过其它途径补回。
2)自己制定策略定期检查Redis的情况,然后可以手动触发备份、重写数据。
3)单机如果部署多个实例,要防止多个机器同时运行持久化、重写操作,防止出现内存、CPU、IO资源竞争,让持久化变为串行。
4)可以加入主从机器,利用一台从机器进行备份处理,其它机器正常响应客户端的命令。
5)RDB持久化与AOF持久化可以同时存在,配合使用。

Redis过期删除策略

在Redis中过期的key不会立刻从内存中删除,而是会同时以惰性删除和定期删除策略进行删除:
1)惰性删除
当key被访问时检查该key的过期时间,若已过期则删除;已过期未被访问的数据仍保持在内存中,消耗内存资源。

2)定期删除
每隔一段时间,随机检查设置了过期的key并删除已过期的key;维护定时器会消耗CPU资源;Redis每100ms进行一次过期扫描(1秒10次):
1、随机取20个设置了过期策略的key
2、检查20个key中过期时间中已过期的key并删除
3、如果有超过25%的key已过期则重复第一步。
这种循环随机操作会持续到过期key可能仅占全部key的25%以下时,并且为了保证不会出现循环过多的情况,默认扫描时间不会超过25ms。

3)定时删除
每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。

Redis中同时使用了惰性过期和定期过期两种过期策略:
假设Redis当前存放30万个key,并且都设置了过期时间,如果你每隔100ms就去检查这全部的key,CPU负载会特别高,最后可能会挂掉。因此,redis采取的是定期过期,每隔100ms就随机抽取一定数量的key来检查和删除的。但是呢,最后可能会有很多已经过期的key没被删除。这时候,Redis采用惰性删除。在你获取某个key的时候,Redis会检查一下,这个key如果设置了过期时间并且已经过期了,此时就会删除。

总结:数据持久化时,四个持久化命令都不会将过期key持久化到RDB文件或AOF文件中,可以保证重启服务时不会将过期key载入Redis。为了保证一致性,在AOF持久化模式中,当key过期时候,会同时发送DEL命令给AOF文件和所有节点;从节点不会主动的删除过期key除非它升级为主节点或收到主节点发来的DEL命令。如果定期删除没删除key,然后你也没即时去请求key,也就是说惰性删除也没生效。这样,redis的内存会越来越高,那么就应该采用内存淘汰机制。

Redis数据淘汰机制

在Redis中,允许用户设置最大使用内存大小server.maxmemory,在内存限定的情况下是很有用的。譬如,在一台8G机子上部署了4个redis服务点,每一个服务点分配1.5G的内存大小,减少内存紧张的情况,由此获取更为稳健的服务。Redis内存数据集大小上升到一定大小的时候,就会实行数据淘汰策略。

=============================================================
# 设置内存大小,下面写法均合法
maxmemory 1024000
maxmemory 1GB
maxmemory 1G
maxmemory 1024kb
maxmemory 1024k
maxmemory 1024MB

# 设置淘汰策略
maxmemory-policy noeviction
=============================================================

旧版Redis提供6种数据淘汰策略:
1)volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰。
2)volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰。
3)volatile-random:从已设置过期时间的数据集中任意选择数据淘汰。
4)allkeys-lru:从数据集中挑选最近最少使用的数据淘汰。
5)allkeys-random:从数据集中任意选择数据淘汰。
6)no-enviction(默认策略):禁止驱逐数据

新版Redis提供8种数据淘汰策略:
1)volatile-lru:设置了过期时间的key使用LRU算法淘汰,LRU算法实现:新加数据放在链表头部 ,链表中的数据被访问就移动到链头,链表满的时候从链表尾部移出数据。
2)allkeys-lru:所有key使用LRU算法淘汰。
3)volatile-lfu(4.0版本新增):设置了过期时间的key使用LFU算法淘汰,LFU算法实现:新数据放在链表尾部,链表中的数据按照被访问次数降序排列,访问次数相同的按最近访问时间降序排列,链表满的时候从链表尾部移出数据。
4)allkeys-lfu(4.0版本新增):所有key使用LFU算法淘汰。
5)volatile-random:设置了过期时间的key使用随机淘汰。
6)allkeys-random:所有key使用随机淘汰。
7)volatile-ttl:设置了过期时间的key根据过期时间淘汰,越早过期越早淘汰。
8)noeviction(默认策略):当内存达到设置的最大值时,所有申请内存的操作(增删改)都会报错,只读操作可以正常执行。

Redis事务处理方式

Redis事务处理机制

Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH四个原语实现的,Redis事务相当于将多个命令添加到一个执行的集合内,multi为begin,exec为commit,discard相当于rollback,watch相当于锁,对象若在事务执行前被修改则事务被打断。因此Redis的事务机制为乐观锁,如果在高并发场景下,如果多个客户端同时对一个key进行了watch,只要有一个客户端提交成功,其他客户端的操作都是无效的,因此Redis事务不适合在高并发场景下使用,使用lua脚本可以更好的解决事务解决不了的场景。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。简言之,Redis事务就是顺序性、一次性、排他性的执行一个队列中的一系列命令。

1)MULTI(begin)
用于开启一个事务,它总是返回OK。MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。
2)EXEC(commit)
执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。当操作被打断时,返回空值。
3)DISCARD(rollback)
客户端可以清空事务队列,并放弃执行事务,并且客户端会从事务状态中退出。
4)WATCH(锁)
可以为Redis事务提供check-and-set(CAS)行为。 可以监控一个或多个键,一旦其中有一个键被修改或删除,之后的事务就不会执行,监控一直持续到EXEC命令。

Redis事务可以一次执行多个命令,并且带有以下三个重要的保证:
1)批量操作在发送EXEC命令前被放入队列缓存。
2)收到EXEC命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行。
3)在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中。

如果事务中出现错误:
1)还未exec就报错:如果事务中出现语法错误,则事务会成功回滚,整个事务中的命令都不会提交。
2)成功执行exec后才报错:如果事务中出现的不是语法错误,而是执行错误,不会触发回滚,该事务中错误的命令不会提交,其他命令依旧会继续提交,因此这里事务和我们通常理解的数据库事务完全不一样。

使用LUA脚本处理Redis事务

Redis2.6之后新增的功能,我们可以在Redis中通过lua脚本操作Redis。与事务不同的是事务是将多个命令添加到一个执行的集合,执行的时候仍然是多个命令,会受到其他客户端的影响,而脚本会将多个命令和操作当成一个命令在Redis中执行,也就是说该脚本在执行的过程中,不会被任何其他脚本或命令打断干扰。正是因此这种原子性,lua脚本才可以代替multi和exec的事务功能。同时也是因此,在lua脚本中不宜进行过大的开销操作,避免影响后续的其他请求的正常执行。

Redis脚本使用eval命令执行lua脚本,其中numkeys表示lua script里有多少个key参数,redis脚本根据该数字从后面的key和arg中取前n个作为key参数,之后的都作为arg参数:
eval script numkeys key [key ...] arg [arg ...]

lua案例:
1)利用hash记录所有登录的IP次数,key参数的数量必须和numkey一致,使用key或者argv可以实现一样的效果。如下面第一个命令里用了三个key,代表后面的三个参数分别对应脚本里的key1、key2、key3,第二个命令里用了一个key,代表了后面第一个参数对应脚本里的key1,后面第二和第三个参数对应脚本里的argv1和argv2:
eval "return redis.call('hincrby', KEYS[1], KEYS[2], KEYS[3])" 3 h_host host_192.168.145.1 1
eval "return redis.call('hincrby', KEYS[1], ARGV[1], ARGV[2])" 1 h_host host_192.168.145.1 1

2)当10秒内请求3次后拒绝访问,给访问ip的key递增,判断该访问次数若为首次登录则设置过期时间10秒,若不是首次登录则判断是否大于3次,若大于则返回0,否则返回1。
eval "local request_times = redis.call('incr',KEYS[1]);if request_times == 1 then redis.call('expire',KEYS[1], ARGV[1]) end;if request_times > tonumber(ARGV[2]) then return 0 end return 1;" 1 test_127.0.0.1 10 3

通过上面的例子也可以看出,我们可以在Redis里使用eval命令调用lua脚本,且该脚本在Redis里作为单条命令去执行不会受到其余命令的影响,非常适用于高并发场景下的事务处理。同样我们可以在lua脚本里实现任何想要实现的功能,迭代,循环,判断,赋值都是可以的。Redis脚本也支持将脚本进行持久化,这样的话,下次再使用就不用输入那么长的lua脚本了。事实上使用eval执行的时候也会缓存,eval与load不同的是eval会将lua脚本执行并缓存,而load只会将脚本缓存。相同点是它们都使用sha算法进行缓存,因此只要lua脚本内容相同,eval与load缓存的sha码就是一样的。而缓存后的脚本,我们可以使用evalsha命令直接调用,极大的简化了我们的代码量,不用重复的将lua脚本写出来。

=============================================================
eval执行脚本并缓存
eval script numkeys key [key ...] arg [arg ...]
 
load缓存lua脚本
SCRIPT LOAD script
 
使用缓存的脚本sha码调用脚本
EVALSHA sha1 numkeys key [key ...] arg [arg ...] 
 
使用sha码判断脚本是否已缓存
SCRIPT EXISTS sha1 [sha1 ...]
 
清空所有缓存的脚本
SCRIPT FLUSH
 
杀死当前正在执行的所有lua脚本
SCRIPT KILL

缓存LUA脚本至Redis:

img

清空LUA脚本:

img

posted @ 2019-03-06 19:33  肖德子裕  阅读(445)  评论(0编辑  收藏  举报