文章目录
- 前言
- 一、为什么要用redis
- 二、多路复用模型
- 三、Redis的基操
-
- 1.String
- 2.Hash
- 3.List
- 4.Set
- 四、项目中购物车模块整合redis优化
-
- 1.原始业务
- 2.目标任务
- 3.实现逻辑
- 五、发布与订阅
- 六、Redis的持久化
-
- RDB
- AOF
- 使用RDB还是AOF?
- 七、搭建Redis主从复制,实现读写分离
-
- 主从架构
- info replication查看当前主从状态
- 修改从节点配置
- 八、Redis缓存过期与内存淘汰机制
-
- 过期的key怎么处理?
- 内存满了怎么办?内存淘汰机制
- 九、哨兵机制
-
- 故障转移
- 约定
- 十、Redis集群:多主多从
-
- 槽结点
- 十一、缓存穿透
-
- 1. 将非法用户请求的信息得到的空值也存到redis里面,屏蔽对数据库的攻击
- 2. 布隆过滤器
- 十二、缓存雪崩
- 十三、Redis的批量查询
前言
今天老爸发生活费了,比以前多给了几百块,于是就新买了个云服务器结点(学生优惠只能支持一台服务器,没优惠就贵得多,所以照着原来的配置只买了两星期),新结点主要用来做redis主从复制、集群负载均衡。 项目背景:目前是单体架构的一个电商平台,包括用户、订单、购物车、搜索、评价等等,数据库使用的是MySQL,项目已经部署到了云服务器,并用nginx进行了反向代理,做了动静分离。 目标:要用redis优化购物车部分(特别关注cookie与redis的缓存逻辑),搭建主从复制,优化redis架构,提高redis可靠性,解决缓存穿透、预防缓存雪崩。一、为什么要用redis
- Redis的优点
- 速度快。Redis完全基于内存, 使用C语言实现,网络层使用epoll解决高并发问题,单线程模型避免了不必要的上下文切换及竞争条件。
- 丰富的数据结构。包括String、Hash、Set、List等
- 支持持久化、主从同步、故障转移等功能
- Redis的缺点
- 单线程
- 单核
- Memcache与redis对比
- Memcache不支持持久化
- Memcache多核多线程
- Memcache数据结构少
总结:redis适合存储热点数据(访问量大),支持持久化存储,并且提供了丰富的功能。
二、多路复用模型
前文已经提到,redis是单线程、单核的,设想下面的情况:假若有多个IO任务需要redis去完成,对于redis来说,如果一直阻塞等待IO,会导致效率的低下。
redis采用了多路复用模型,当请求到来时,若要等待,多路复用器就去处理其他请求;在处理请求时,多路复用器并不真正实现处理逻辑,而是把任务丢给后面的处理器。
一方面单线程避免了CPU的切换及加锁,另一方面多路复用避免了阻塞等待的效率损耗。这使redis的速度得以保证。
三、Redis的基操
1.String
2.Hash
3.List
4.Set
四、项目中购物车模块整合redis优化
1.原始业务
原本的购物车中数据并没有更新到数据库,只是做了一个cookie进行浏览器的缓存。当用户未登录,可以提交商品到购物车,但是不能下单,当用户登录后,可以进行下单操作,并将订单更新到数据库。
2.目标任务
使用redis将购物车中数据进行持久化存储,并且要能和cookie进行融合
3.实现逻辑
1.redis中无数据
如果cookie中的购物车为空,那么这个时候不做处理
如果cookie中的购物车不为空,直接覆盖redis
2.redis中有数据
如果cookie中的购物车为空,那么直接把redis的购物车覆盖本地cookie中
如果cookie中的购物车不为空,redis中也存在,则以cookie为准,覆盖redis
3.同步到redis中之后,覆盖本地cookie购物车的数据,保证本地购物车的数据是同步的
private void sychShopcartData(String userId, HttpServletRequest request,
HttpServletResponse response) {
//1 从redis中获取购物车
String shopcartJsonRedis = redisOperator.get(FOODIE_SHOPCART + ":" + userId);
//2 从cookie中获取购物车
String shopcartStrCookie = CookieUtils.getCookieValue(request, FOODIE_SHOPCART, true);
if (StringUtils.isBlank(shopcartJsonRedis)) {
//redis为空,cookie不为空,把cookie放进redis
if (StringUtils.isNotBlank(shopcartStrCookie)) {
redisOperator.set(FOODIE_SHOPCART + ":" + userId, shopcartStrCookie);
}
} else {
//redis不为空,cookie不为空,合并cookie和redis中购物车的商品数据(同一商品覆盖redis)
if (StringUtils.isNotBlank(shopcartStrCookie)) {
List<ShopcartBO> shopcartBOListRedis = JsonUtils.jsonToList(shopcartJsonRedis, ShopcartBO.class);
List<ShopcartBO> shopcartBOListCookie = JsonUtils.jsonToList(shopcartStrCookie, ShopcartBO.class);
//定义待删除List
List<ShopcartBO> pendingDeleyeList = new ArrayList<>();
for (ShopcartBO redisShopcart : shopcartBOListRedis) {
String redisSpecId = redisShopcart.getSpecId();
for (ShopcartBO cookieShopcart : shopcartBOListCookie) {
String cookieSpecId = redisShopcart.getSpecId();
if (redisSpecId.equals(cookieSpecId)) {
//覆盖购买数量,不累加
redisShopcart.setBuyCounts(cookieShopcart.getBuyCounts());
//把cookieShopcart放入待删除列表,用于最后的删除合并
pendingDeleyeList.add(cookieShopcart);
}
}
}
//从现有cookie中删除对应的覆盖过的商品数据
shopcartBOListCookie.removeAll(pendingDeleyeList);
//合并两个list
shopcartBOListRedis.addAll(shopcartBOListCookie);
//更新到cookie和redis
CookieUtils.setCookie(request, response, FOODIE_SHOPCART, JsonUtils.objectToJson(shopcartBOListRedis), true);
redisOperator.set(FOODIE_SHOPCART + ":" + userId, JsonUtils.objectToJson(shopcartBOListRedis));
} else {
//redis不为空,cookie为空,直接把redis覆盖cookie
CookieUtils.setCookie(request, response, FOODIE_SHOPCART, shopcartJsonRedis, true);
}
}
}
五、发布与订阅
六、Redis的持久化
看了前面的理论,可能会有人感到懵逼:redis完全基于内存的,然而它却可以持久化??当断电了,内存里面的数据不就没了??
参考:Redis官方文档
Redis提供两种持久化方案:RDB(Redis Database)、AOF(Append Only File)
RDB
每隔一段时间,把内存中的数据写入磁盘的临时文件,作为快照,恢复的时候把快照文件读进内存。如果宕机重启,内存里的数据丢失,那么再次启动Redis后,则会恢复
- 优点:
- 每隔一段时间备份,全量备份
- 灾备简单,可以远程传输
- 子进程备份的时候,主进程不会有任何的IO操作(可读),保证备份数据的完整性
- 相对于AOF,当有更大的文件的时候可以快速的重启恢复
- 劣势:
- 发生故障时,可能会丢失最后一次的备份数据
- 子进程所占用的内存会和父进程一模一样,会造成CPU的负担
- 由于定时全量备份是重量级操作,所以对于实时备份,就无法处理
配置RDB:
- 保存机制:
- 开启RDB文件压缩模式
rdbcompression:yes - 对RDB文件进行校验(但是会有10%的内存损耗)
rdbchecksum:yes
总结:RDB适合大量数据的恢复,但是数据的完整性和一致性可能会不足。不过嘞,RDB丢失的那一点点其实也无所谓,反正是缓存,丢了就丢了
AOF
AOF可以保证数据的完整性。
特点:1.以日志的形式来记录用户请求的写操作。读操作不会记录。
2.文件以追加的形式而不是修改的形式
3.redis通过AOF恢复,其实就是读取日志,把写操作重新执行一遍
-
优点:
- AOF可以以秒级别为单位进行备份,若发生问题,也只会丢失最后一秒数据,增加数据可靠性和完整性。
- 以log日志形式追加,若磁盘满了,会执行redis-check-aof工具
- 当数据量太大,redis在后台可以自动重写aof,当redis继续把日志追加到老的文件中去,重写也非常安全,不会影响客户端的读写操作。
-
缺点
- 同一份数据,AOF文件会比RDB文件大
- 针对不同的同步机制,AOF会比RDB慢,因为AOF每秒都会备份做写操作。
配置AOF
使用RDB还是AOF?
- 若可以接受一段时间的缓存丢失,可以用RDB
- 若对实时性的数据比较关心,就用AOF
- 还可以使用RDB和AOF一起做持久化,RDB做冷备,可以在不同时期对不同版本做恢复,AOF做热备,保证数据仅仅有1秒的损失。当AOF破损不可用,再用RDB进行恢复。即Redis先去加载AOF,若AOF出了问题,再去加载RDB。
七、搭建Redis主从复制,实现读写分离
主从架构
一般来说,主从模式是采用一主二从,但是由于资金有限,下面的配置中只搞了一个从结点,即一主一从
另一种主从方式:无磁盘化复制,若服务器中的磁盘是机械硬盘,可能磁盘的读写效率比较低,那么若网络带宽比较好的话,可以采用网络的方式进行传输,避免了磁盘的交互。
info replication查看当前主从状态
修改从节点配置
这时候,我们的slave即从节点已经配置好了,通过/etc/init.d下的redis_init_script进行重启
在主结点上添加信息,从节点上可以看到,而从节点不能写数据
在这过程中可能遇到的问题:莫名其妙连接不上?建议检查一下redis.conf文件中的replicaof,可能是我的幻觉吧,不知道为啥它会自动的改为127.0.0.1 感觉有一丝恐怖,上面截图里面就可以看到,它不正常
这才是正确的信息:
八、Redis缓存过期与内存淘汰机制
过期的key怎么处理?
- 主动定时清理:定时随机检查过期的key,如果过期则清除(配置频率HZ)。
- 被动惰性删除:当客户端请求一个key时,若这个key过期了,就删除。
(因此,虽然key过期了,但只要没被清理,它就还是占着内存)
内存满了怎么办?内存淘汰机制
九、哨兵机制
在主从模式中,当主结点挂掉了怎么办?它会导致写不了,从结点也不能使用。
可以采用哨兵机制进行控制,当主结点被干掉了,就可以用哨兵来将子结点设置为主结点。
配置哨兵
故障转移
当哨兵集群中的一个哨兵发现了Master挂掉了,它并不能决定是否进行主从的切换即故障转移。因为在网络环境中,有可能是因为网络问题导致的错误判断。这是称为主观下线
当多个哨兵都发现这个结点有问题时(客观下线),才会进行故障转移
故障转移:将slave转为Master继续进行服务。这个过程由众多哨兵中的一个leader来执行,leader要进行选举(少数服从多数)。
当某个哨兵获得了多票之后,它就称为leader,将原来的某个Slave转为Master,然后将新Master的信息和Slave进行同步。
若之前的Master恢复了,之前的Master会作为Slave重新加入。
约定
- 哨兵的结点至少有三个,或者奇数结点。(便于进行选举,少数服从多数)
- 哨兵应部署在不同的计算机结点。(若都在一个结点上,当结点挂了,哨兵也完蛋了,就没意义了)
- 一组哨兵只去监听一组主从。
十、Redis集群:多主多从
一主的缺点:当老的master挂掉了,这时候进行主从故障转移。然而若此时新来了一些写操作,就会丢失。于是就有了多主多从
槽结点
redis中的数据存储在槽结点中
对于一个数据hash后求模,得到存储的位置
十一、缓存穿透
什么是缓存穿透?
对于一些热点数据,我们是存在redis中的,目的是减少数据库的访问量。但是若有一些非法用户对系统进行攻击,传入一些根本不存在的值,按照之前的逻辑,会先去redis找,若查不到就去数据库中查,如果不进行处理,会导致数据库的访问量增大,最后宕机。如何屏蔽掉这种非法访问?
1. 将非法用户请求的信息得到的空值也存到redis里面,屏蔽对数据库的攻击
List<CategoryVO> list = new ArrayList<>();
String subCatsStr = redisOperator.get("subCat:"+rootCatId);
if (StringUtils.isBlank(subCatsStr)) {
list = categoryService.getSubCatList(rootCatId);
if (list!=null && list.size()>0){
redisOperator.set("subCat:"+rootCatId, JsonUtils.objectToJson(list));
}else {
redisOperator.set("subCat:"+rootCatId, JsonUtils.objectToJson(list),5*60);
}
} else {
list = JsonUtils.jsonToList(subCatsStr, CategoryVO.class);
}
return IMOOCJSONResult.ok(list);
2. 布隆过滤器
对于每个key,经过一定的运算,保存到数组上的某个位置,并设置为1
当一个非法值过来之后,它匹配不上1,就连redis都不会进(很明显,会存在误判)
布隆过滤器的缺点:
- 不能移除数据(多个数据存在同一个位置)
- 存在误判
- 错误率越低,占用空间越大
- 要维护一个集合,且要和redis交互
十二、缓存雪崩
Redis中的缓存恰好在某一时间点大面积的失效,而此时恰好出现了大量的请求,导致数据库宕机
十三、Redis的批量查询
可以建立一个管道来一次性完成多个key的查询