Redis从入门到实战

  • Post author:
  • Post category:其他





前言


今天老爸发生活费了,比以前多给了几百块,于是就新买了个云服务器结点(学生优惠只能支持一台服务器,没优惠就贵得多,所以照着原来的配置只买了两星期),新结点主要用来做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的查询





版权声明:本文为qq_44357371原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。