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购物车的数据,保证本地购物车的数据是同步的
	/*
     * 注册登录成功后,同步cookie和redis中的购物车数据
     * */
    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)) {
    
    
                /*
                 * 1.已经存在的,把cookie中对应的数量,覆盖redis
                 * 2.该项商品标记为待删除,统一放入一个待删除的list
                 * 3.从cookie 中清理所有的待删除list
                 * 4.合并redis和cookie中的数据
                 * 5.更新到redis和cookie中
                 * */
                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后,则会恢复

  • 优点:
    1. 每隔一段时间备份,全量备份
    2. 灾备简单,可以远程传输
    3. 子进程备份的时候,主进程不会有任何的IO操作(可读),保证备份数据的完整性
    4. 相对于AOF,当有更大的文件的时候可以快速的重启恢复
  • 劣势:
    1. 发生故障时,可能会丢失最后一次的备份数据
    2. 子进程所占用的内存会和父进程一模一样,会造成CPU的负担
    3. 由于定时全量备份是重量级操作,所以对于实时备份,就无法处理

配置RDB:

  1. 保存机制:
  2. 开启RDB文件压缩模式
    rdbcompression:yes
  3. 对RDB文件进行校验(但是会有10%的内存损耗)
    rdbchecksum:yes

总结:RDB适合大量数据的恢复,但是数据的完整性和一致性可能会不足。不过嘞,RDB丢失的那一点点其实也无所谓,反正是缓存,丢了就丢了

AOF

AOF可以保证数据的完整性。
特点:1.以日志的形式来记录用户请求的写操作。读操作不会记录。
2.文件以追加的形式而不是修改的形式
3.redis通过AOF恢复,其实就是读取日志,把写操作重新执行一遍

  • 优点:

    1. AOF可以以秒级别为单位进行备份,若发生问题,也只会丢失最后一秒数据,增加数据可靠性和完整性。
    2. 以log日志形式追加,若磁盘满了,会执行redis-check-aof工具
    3. 当数据量太大,redis在后台可以自动重写aof,当redis继续把日志追加到老的文件中去,重写也非常安全,不会影响客户端的读写操作。
  • 缺点

    1. 同一份数据,AOF文件会比RDB文件大
    2. 针对不同的同步机制,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怎么处理?

  1. 主动定时清理:定时随机检查过期的key,如果过期则清除(配置频率HZ)。
  2. 被动惰性删除:当客户端请求一个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 {
    
    
                /*
                * 若被非法用户攻击(疯狂访问数据库,使数据库宕机)
                * 即缓存穿透
                * 解决方法:将用户非法的请求得到的空数据也缓存在redis中,避免直接访问数据库
                * */
                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都不会进(很明显,会存在误判)

布隆过滤器的缺点:

  1. 不能移除数据(多个数据存在同一个位置)
  2. 存在误判
  3. 错误率越低,占用空间越大
  4. 要维护一个集合,且要和redis交互

十二、缓存雪崩

Redis中的缓存恰好在某一时间点大面积的失效,而此时恰好出现了大量的请求,导致数据库宕机

十三、Redis的批量查询


可以建立一个管道来一次性完成多个key的查询

猜你喜欢

转载自blog.csdn.net/qq_44357371/article/details/109367400