1、Redis简介

Redis是一个使用C语言开发的数据库,不过与传统的数据库不同的是Redis的数据是存在内存中的,是内存数据库,读写速度非常快,被广泛用于缓存方向。此外,Redis除了做缓存之外,也经常用来做分布式锁,甚至是消息队列。Redis提供了多种数据类型来支持不同的业务场景。Redis还支持事务、持久化、Lua脚本、多种集群方案。

2、分布式缓存

分布式缓存由一个服务端实现管理和控制,有多个客户端节点存储数据,可以进一步提高数据的读取速率;通过客户端的一致性哈希算法确定数据的存储和读取节点。原理如下图:

redis spring 分布式缓存 redis分布式缓存原理_Redis

分布式缓存主要解决的是单机缓存的容量受服务器限制并且无法保存通用的信息。因为本地缓存只在当前服务里有效,如果你部署了两个相同的服务,它们两者之间的缓存数据无法同步。

分布式缓存常见的技术选型:

1、Memcached

memcached采用的是预先分配的原则,这种方式是拿空间换时间的方式来提高它的速度,这种方式会造成不能很高效的利用内存空间,但是memcached采用了Slab Allocation机制来解决内存碎片的问题,Slab Allocation的基本原理就是按照预先规定的大小,将分配的内存分割成特定长度的块,并把尺寸相同的块分成组(chunk的集合),如图

redis spring 分布式缓存 redis分布式缓存原理_缓存_02

memcached会针对客户端发送的数据选择slab并缓存到chunk中,这样就有一个弊端那就是比如要缓存的数据大小是50个字节,如果被分配到如上图88字节的chunk中的时候就造成了33个字节的浪费,虽然在内存中不会存在碎片,但是也造成了内存的浪费,这也是我上面说的拿空间换时间的原因,不过memcached对于分配到的内存不会释放,而是重复利用。

memcached的分布式完全就是依靠客户端的一致哈希算法来达到分布式的存储,因为本身各个memcached的服务器之间没办法通信,并不存在副本集或者主从的概念,它的分布式算法主要是先求出每一个memcached的服务器节点的哈希值,并将它们分配到2的32次方的圆上,然后根据存储的key的哈希值来映射到这个圆上,属于哪个区间顺时针找到的节点就存到这个服务器节点上。

2、Redis

3、Redis和Memcached的区别和共同点

redis spring 分布式缓存 redis分布式缓存原理_缓存_03

使用Redis:高性能(从内存中获取数据,速度快)、高并发(直接操作缓存能承受的数据库请求数量远远大于直接访问数据库,通过将部分数据转移到缓存中,提高系统整体的并发)。

3、Redis常见数据结构及使用场景

string

redis spring 分布式缓存 redis分布式缓存原理_数据_04

list

redis spring 分布式缓存 redis分布式缓存原理_Redis_05

hash

redis spring 分布式缓存 redis分布式缓存原理_redis spring 分布式缓存_06

set

redis spring 分布式缓存 redis分布式缓存原理_Redis_07

sorted set

redis spring 分布式缓存 redis分布式缓存原理_redis spring 分布式缓存_08

bitmap

redis spring 分布式缓存 redis分布式缓存原理_缓存_09

 4、Redis单线程模型

Redis 基于 Reactor 模式开发了自己的网络事件处理器:这个处理器被称为文件事件处理器(file event handler)。文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字,并根据 套接字目前执行的任务来为套接字关联不同的事件处理器。

当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关 闭(close)等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。

redis spring 分布式缓存 redis分布式缓存原理_Redis_10

不使用多线程原因:单线程编程容易并且更容易维护;Redis性能瓶颈不在CPU主要在内存和网络;多线程就会存在死锁、线程上下文切换等问题,甚至会影响性能。 

后面引入多线程主要是为了提高网络IO读写性能。Redis的多线程只是在网络数据的读写这类好事操作上使用了,执行命令仍然是单线程顺序执行。因此不需要担心线程安全问题。

5、Redis给缓存设置过期时间

作用:缓解内存的消耗;很多时候,我们的业务场景就是需要某个数据只在某一时间段内存在,比如我们的短信验证码可能只在 1 分钟内有效,用户登录的 token 可能只在 1 天内有效。

如何判断过期?

Redis 通过一个叫做过期字典(可以看作是 hash 表)来保存数据过期的时间。过期字典的键指向 Redis 数据库中的某个 key(键),过期字典的值是一个 long long 类型的整数,这个整数保存了 key 所指向的数据库键的过期时间(毫秒精度的 UNIX 时间戳)。

过期数据删除策略:

redis spring 分布式缓存 redis分布式缓存原理_数据_11

 6、内存淘汰机制(包括手撕算法时的LRU哦)

过期删除策略可能存在定期删除和惰性删除漏掉了很多过期 key 的情况。这样就导致大量过期 key 堆积在内存里,然后就 Out of memory 了。解决方案是内存淘汰机制。

redis spring 分布式缓存 redis分布式缓存原理_redis spring 分布式缓存_12

7、Redis持久化机制

 快照(snapshotting)持久化(RDB)

Redis 可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。Redis 创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能),还可以将快照留在原地以便重启服务器的时候使用。

AOF(append-only file)持久化

AOF会保存服务器执行的所有写操作到日志文件中,在服务重启以后,会执行这些命令来恢复数据。与快照持久化相比,AOF 持久化 的实时性更好,因此已成为主流的持久化方案。开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入硬盘中的 AOF 文件。AOF 文件的保存位置和 RDB 文件的位置相同,都是通过 dir 参数设置的,默认的文件名是 appendonly.aof。

Redis4.0对于持久化机制的优化:

Redis 4.0 开始支持 RDB 和 AOF 的混合持久化。如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的 RDB 部分是压缩格式不再是 AOF 格式,可读性较差。

8、Redis事务

Redis 可以通过 MULTI,EXEC,DISCARD 和 WATCH 等命令来实现事务(transaction)功能。使用 MULTI命令后可以输入多个命令。Redis 不会立即执行这些命令,而是将它们放到队列,当调用了EXEC命令将执行所有命令。

Redis 是不支持 roll back 的,因而不满足原子性的(而且不满足持久性)。

Redis 事务提供了一种将多个命令请求打包的功能。然后,再按顺序执行打包的所有命令,并且不会被中途打断。

9、缓存穿透

缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层。

解决办法:

最基本的就是首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。比如查询的数据库 id 不能小于 0、传入的邮箱格式不对的时候直接返回错误消息给客户端等等。

1)缓存无效key:如果缓存和数据库都查不到某个 key 的数据就写一个到 Redis 中去并设置过期时间,具体命令如下: SET key value EX 10086 。这种方式可以解决请求的 key 变化不频繁的情况,如果黑客恶意攻击,每次构建不同的请求 key,会导致 Redis 中缓存大量无效的 key 。很明显,这种方案并不能从根本上解决此问题。如果非要用这种方式来解决穿透问题的话,尽量将无效的 key 的过期时间设置短一点比如 1 分钟。

2)布隆过滤器:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话再在缓存中和数据库中查找数据。布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。

元素加入布隆过滤器:使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值(有几个哈希函数得到几个哈希值);根据得到的哈希值,在位数组中把对应下标的值置为 1。

判断一个元素是否存在于布隆过滤器:对给定元素再次进行相同的哈希计算;得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。

10、缓存雪崩

缓存在同一时间大面积的失效,后面的请求都直接落到了数据库上,造成数据库短时间内承受大量请求。或者是有一些被大量访问数据(热点缓存)在某一时刻大面积失效,导致对应的请求直接落到了数据库上。举例系统的缓存模块出了问题比如宕机导致不可用,造成系统的所有访问,都要走数据库。

解决办法:

redis spring 分布式缓存 redis分布式缓存原理_redis spring 分布式缓存_13

11、如何保证缓存和数据库数据的一致性

Cache Aside Pattern(旁路缓存模式):遇到写请求,更新DB,然后直接删除cache.

如果更新数据库成功,而删除缓存这一步失败的话,解决方案:1、缓存失效时间变短,这样的话缓存就会从数据库中加载数据。对于先操作缓存后操作数据库的场景不可用;2、增加cache更新重试机制,如果cache服务当前不可用导致缓存删除失败的话,我们就隔一段时间进行重试。如果多次重试还是失败,就把当前更新失败的key存入队列中,等缓存服务可用之后,再将缓存中对应的key删除即可。

12、缓存常用的读写策略

Cache Aside Pattern(旁路缓存模式):

读:先更新 DB,然后直接删除 cache 。

写:从 cache 中读取数据,读取到就直接返回cache中;读取不到的话,就从 DB 中读取数据返回,再把数据放到 cache 中。

在写数据的过程中,如果先删除cache,后更新DB问题:数据库缓存不一致,如请求1先把cache中的A数据删除 -> 请求2从DB中读取数据->请求1再把DB中的A数据更新。如果先更新DB,后删除cache问题:很小概率出现数据不一致,因为缓存的写入速度比 数据库的写入速度快,如请求1从DB读数据A->请求2写更新数据 A 到数据库并把删除cache中的A数据->请求1将数据A写入cache。

Read/Write Through Pattern(读写穿透):

读:从cache中读取数据,读取到就直接返回;读取不到的话,先从DB加载,写入到cache后返回相应。

写:先查cache,cache不存在,直接更新DB;cache中存在,则先更新cache,然后cache服务自己更新DB。

Write Behind Pattern(异步缓存写入):

和读写穿透很相似。不同点是Read/Write Through 是同步更新 cache 和 DB,而 Write Behind Caching 则是只更新缓存,不直接更新 DB,而是改为异步批量的方式来更新 DB。

项目中的使用:

本节内容主要是redis在springboot工程中的使用。

首先导入相关依赖:spring-boot-starter-data-redis;然后再.properties文件里面配置使用的数据库,以及IP和端口号;然后针对我们的需求,设置RedisSerializer,即设置序列化的方式,如果不设置,redis会将我们存入的数据自己加上一串字符,会导致我们根据之前传入的key找不到值,所以要自己设置;RedisTmplate默认使用JdkSerializationRedisSerializer,在使用时自己根据情况选择序列化方式,key和hashKey: 推荐使用StringRedisSerializer: 简单的字符串序列化。hashValue: 推荐使用 GenericJackson2JsonRedisSerializer:类似Jackson2JsonRedisSerializer,但使用时构造函数不用特定的类。存入元素时主要设置key、value、过期时间,采用编程式事务。

常见的序列化方式:

1,用StringRedisSerializer进行序列化的值,在Java和Redis中保存的内容是一样的

2,用Jackson2JsonRedisSerializer进行序列化的值,在Redis中保存的内容,比Java中多了一对双引号。

3,用JdkSerializationRedisSerializer进行序列化的值,对于Key-Value的Value来说,是在Redis中是不可读的。对于Hash的Value来说,比Java的内容多了一些字符。

参考:https://snailclimb.gitee.io/javaguide/#/?id=redis