P9-缓存商品与用户

233 阅读17分钟

杂谈

分布式目的是解决并发的问题。每个环节都启用多个节点,哨兵or集群。 分布式是微服务的前提。微服务是把大项目拆分成很多子项目,不同服务可以直接调用,基于什么url协议。 微服务两体系:阿里的duubo、springclould

缓存

mysql也有缓存,但是级别靠后,性能不好。

缓存离客户端越近、离DB越远、越靠前,效果越好。

初识

引入Redis和guawa做缓存

  • guawa一级缓存,Redis二级缓存。
  • guawa本地缓存,Redis分布式缓存。

guawa

  • hashmap做的缓存,跟tomcat服务器一体,跟服务器共用缓存,比redis离用户更进一步。

  • 分给guawa的内存不能太多,影响服务器性能,一般几百M,一个G。

  • 非常热的数据放guawa,次热的放redis。guawa离得近性能更好内存小,redis稍远反应稍慢但内存大,两者结合

  • 并且,两级缓存,起互相备份的功能。

guava缓存使用:

pom.xml 引入guava依赖包

image.png

示例:

  • 初始化cache,catch接口的引用类型,k是字符串,v是对象,用cachebuilder创建,不能直接new。

  • cachebuilder实例化newbuilder(),其中的方法大部分返回的都是build本身,所以可以引用很长点点点

  • initialCapacity(10)初始化容量10,maximumSize(100)最大容量100个(存100个热门商品),expireAfterWrite(duration:5,TimeUnit.SECONDS)在数据写入缓存以后,5秒过期。build()最后调用build方法返回catch对象

  • cache.put("user","Tom");存入一组数据

  • 循环查看cache.getIfPresent(key:"user"),如果catch中user这个key存在,获取对应的value。打印

  • 打印以后,Thread.sleep( millis: 1000);线程睡眠一秒

image.png

前五秒user存在,打印,6秒开始,catch中user过期,打印null

image.png

后端引入缓存,对前端是透明化的,前端不变。

代码

service/impl/ItemServiceimpl

引入redis分布式(远程)缓存。前面已经配置/初始化好了,只引入就可以。

@Autowired
private RedisTemplate redisTemplate;

引入guava本地缓存,guava是比较轻量级的包,手动处理配置。

private Cache<String, Object> cache;

先查本地缓存有没有,没有再查redis缓存,还没有最后才查mysql。

在对象初始化阶段,创建cache

@PostContruct注解:spring工厂在对象初始化完成/实例化后,调用带这个注解的方法。

private Cache<String, Object> cache;

@PostConstruct
public void init() {
    cache = CacheBuilder.newBuilder()
            .initialCapacity(10)
            .maximumSize(100)
            .expireAfterWrite(1, TimeUnit.MINUTES)
            .build();
}

service/impl/ItemService

  • 主要优化查询的性能
  • 调用的是ItemService接口,里面创建在缓存查询的方法。
  • 更多的是替代finditembyid方法

image.png

service/impl/ItemServiceimpl

findItemInCache根据id在缓存中取商品,方法的实现:

  • 结构与之前类似。
  • id小于等于0,参数不合法
  • 最后要返回item,先声明
  • guava和redis都是kv结构,声明一下key的格式
  • 先从guava取,item = (Item) cache.getIfPresent(key);,取到了非空说明商品在guava一级缓存,直接返回item
  • 没有,就从redis取。取到了非空说明商品信息在redis(二级缓存)中,把k和v(item商品)存到guava一级缓存中,返回item
  • 如果redis中还没有(一二级缓存都没命中),调用finditembyid()方法,直接在mysql中查商品item。查到了item非空,就把商品数据存到一二级缓存中
  • guava本地缓存默认有过期时间,redis过期时间设置三分钟
  • 扔空,返回null
public Item findItemInCache(int id) {
    if (id <= 0) {
        throw new BusinessException(PARAMETER_ERROR, "参数不合法!");
    }

    Item item = null;
    String key = "item:" + id;

    // guava
    item = (Item) cache.getIfPresent(key);
    if (item != null) {
        return item;
    }

    // redis
    item = (Item) redisTemplate.opsForValue().get(key);
    if (item != null) {
        cache.put(key, item);
        return item;
    }

    // mysql
    item = this.findItemById(id);
    if (item != null) {
        cache.put(key, item);
        redisTemplate.opsForValue().set(key, item, 3, TimeUnit.MINUTES);
        return item;
    }

    return null;
}

controller/itemcontroller

把findItemById方法,改成在缓存中查

@RequestMapping(path = "/detail/{id}", method = RequestMethod.GET)
    @ResponseBody
    public ResponseModel getItemDetail(@PathVariable("id") int id) {
//        Item item = itemService.findItemById(id);
        Item item = itemService.findItemInCache(id);
        return new ResponseModel(item);
    }

略?P8,35min

Redis持久化

redis持久化存点赞数量,可能丢1s的数据,可以接受

RDB持久化:

  • 默认持久化方式

  • 全量快照形式,把内存中某个时刻的全部数据转换成二进制后,持久化到硬盘中。RDB文件存的是二进制数据

  • 用BGSAVE(后台save)命令触发,(SAVE容易阻塞),手动/自动都可

  • 优点:体积小,保存所有数据

  • 缺点:不能实时,适合备份。如:每隔6小时执行一次,备份之前的数据 一般用于定时备份数据。 AOF持久化

  • 以AOF日志方式存数据,记录每次执行的命令,追加存到AOF文件中

  • 优点:实时性好(实时保存数据),保证性能的前提下,可以最多丢失1秒的数据(刷盘频率)

  • 缺点:存的是命令,会冗余,体积大 一般用于宕机后重启恢复缓存数据。

RDB-AOF混合持久化

  • 从redis4.0开始支持,是基于AOF实现的
  • 在AOF重写时,先执行BGSAVE命令生成RDB文件,再把新处理的命令,追加到AOF文件末尾

核心问题:redis的单线程-IO多路复用前提是不能阻塞-持久化容易出现阻塞

image.png

RDB持久化机制阻塞问题

  • 执行bgsave,通知redis当前父进程(主进程),fork一个子进程进行持久化,把二进制数据存入RDB文件。(如果都是父进程做,会长时间阻塞)
  1. 如果当前已经有子进程(已经foge了一个子进程正在持久化,又来了一个bgsave),不创建新进程,返回。
  2. 父进程fork创建子进程时这一刻父进程会产生阻塞,无法响应客户端请求。过程比较短暂。
  3. fork子进程创建完成后,父进程解除阻塞,继续响应其他命令
  4. 子进程进行持久化:读取父进程内存中的数据(共享父进程数据),转换成二进制后存入(硬盘的)RDB文件。如果已有旧RDB文件,存入新生成的RDB中。3、4步几乎同时进行
  5. 持久化完成(数据存储完)后,通知父进程替换旧的RDB文件。

image.png

问题:子进程在持久化时读取父进程数据,父进程在响应其他命令写数据,如何保证一致性?

  • 写时复制/Copy On Write
  • 内存最小的管理单元是页(page)
  • 子进程持久化时,共享读父进程数据,父进程读写不同页的数据不受影响。
  • 父进程可以同时读同页数据
  • 改同页的话:先复制一份副本page数据,在副本page上修改。
  • 修改以后,副本就是父进程的内存,原page释放空间回收。

快照

  • 快照是数据存储的某一时刻的状态记录。
  • 将内存文件锁住,不能更改。新建文件,把更改都放到新建的文件中。读取时,先读取新建文件,没有再读取锁定的数据。
  • 共享数据时,把此时内存的数据页锁住,不能修改,只能修改复制(新建)的副本数据。
  • 子进程创建好后与父进程共享数据,此时的父进程的数据对子进程来说,就是快照数据。

image.png

第五步实现原理: vim etc/redis.conf查看redis的配置文件。 image.png

注:如果运行中redis出现问题,看这个日志文件 image.png

  • 持久化时RDB文件的文件名。dump.rdb
  • 生成的RDB文件的存储路径
  • 启动持久化时,redis底层肯定会有个变量持有RDB的文件名和路径。
  • 持久化后生成新的RDB文件。加入叫dump1.rdb
  • 通知父进程旧RDB作废,引用新的RDB1。就是更新变量的RDB文件名+路径 image.png

AOF持久化阻塞问题

关注AOF的重写机制:AOF存命令,数据很大,重复交叉的命令操作会有冗余,重新生成一个压缩(清理冗余)的AOF文件。

  • 往Redis中写(增删改) 数据时
  • redis把命令缓存到缓冲区(aof_buf),按一定频率刷到磁盘sync,同步到磁盘的AOF文件

什么时候同步,有几个同步机制,都是操作系统的api,自己调用就行

- 一般定时刷盘,一秒一次

  • 掉电死机以后,重启服务要恢复数据,加载AOF文件

image.png

redis配置中可以查看

  • 是否开启,no没启用
  • 启用后aof文件名

image.png

AOF存的是命令,数据很大,重复交叉的命令操作会有冗余,恢复时不方便——所以要AOF重写。

AOF重写(服务器自动触发)

缓冲区是操作系统的页缓存

  • 重写由bgrewriteaof命令触发。服务器自动触发
  • 前面类似RDB持久化,也需要通知父进程fork一个子进程来持久化新的AOF文件
  1. bgrewriteaof触发重写,如果正在AOF重写,则返回(已经有一个子进程了,就不再做重写)/正在BGSAVE则推迟执行
  2. 父进程fork一个子进程,期间是阻塞的,时间很短。 fork结束解除阻塞,父进程可以继续响应新的写入命令。
  3. 父进程还拷贝一个当前数据集的快照,给子进程进行重写。
  4. 子进程读快照数据,获得去掉冗余,去重以后的命令集,写入新的AOF文件。(3、4)和5并行
  5. 期间如果父进程有新的写命令,父进程把新命令缓存到aof_buf的同时,也同步缓存到rewrite_buf
  6. 新AOF写入完成,会先把rewrite_buf重写子线程缓冲池中的数据刷盘sync(操作系统机制)到新AOF中
  7. 子进程再通知父进程替换旧AOF文件

问题:子进程读快照写入新AOF文件,期间父进程也在响应其他的修改命令,没被快照到的新命令被缓存到aof_buf,再同步到旧AOF。新AOF替换旧,如何防止这部分数据丢失?

解决:引入rewrite_buf缓冲区,防止重写期间丢失新的写入命令

:重写过程有两个缓冲区,分配内存不大,几M;子进程写入新AOF,数据量很大几十G,rewrite_buf只包含很短时间内的命令几十M,刷盘(操作系统的机制)很快

image.png

image.png

小结

  • bgsave是存二进制数据,把整个内存数据都存入RDB文件,不能每次执行命令都做bgsave操作,会阻塞父进程。一般五六小时做一次,保证数据完整性,起备份作用。

  • 如图AOF白色区域跟bgsave效果等同,能保证数据完整性。区别是AOF每次有写入命令时,都实时同步到AOF文件中。每次实时追加一条命令

  • AOF的问题是文件存命令,大量的命令会有冗余+体积大,所以有重写机制来压缩AOF文件。隔很久才做一次,不是每次AOF持久化,实时追加命令时就必须做。

  • 黄色框都是额外的问题事情,白色区域是持久化主要操作。

问题:如果重写过程中,567步期间断电服务器死机,没有执行到8步。有何影响? :对数据完整性无影响。由于旧AOF保存了所有的命令以及重写期间的新命令。重启后加载旧AOF,依然能保证数据完整性。掉电只导致没有得到压缩后的AOF。

======================================================

什么是Redis持久化? Redis持久化就是把内存的数据写到磁盘中去,防止服务宕机了内存数据丢失;

Redis提供两种持久化机制RDB(默认)和AOF机制;

RDB RDB(Redis DataBase缩写快照)

是Redis默认的持久化方式。按照一定的时间将内存的数据以快照的形式保存到硬盘中,对应产生的数据文件为dump.rdb。通过配置文件中的save参数来定义快照的周期;记录Redis数据库的所有键值对,在某个时间点将数据写入一个临时文件持久化结束后,用这个临时文件替换上次持久化的文件达到数据恢复;

优点:

  • 只有一个文件 dump.rdb,方便持久化;
  • 容灾性好,一个文件可以保存到安全的磁盘;
  • 性能最大化,fork子进程来完成写操作,让主进程继续处理命令,所以是IO最大化。使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了Redis的高性能;
  • 相对于数据集大时,比 AOF 的启动效率更高;

缺点:

  • 数据安全性低;
  • RDB 是间隔一段时间进行持久化,如果在持久化时Redis发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候;

AOF AOF持久化(即Append Only File持久化)

是将Redis执行的每次写命令记录到单独的日志文件中,当重启Redis会重新将持久化的日志中文件恢复数据。当两种方式同时开启时数据恢复Redis会优先选择AOF恢复;

优点:

  • 数据安全,AOF持久化可以配置 appendfsync 属性,有always属性,每进行一次命令操作就记录到AOF文件中一次;
  • 通过append模式写文件,即使中途服务器宕机,可以通过 redis-check-aof 工具解决数据一致性问题;
  • AOF机制的rewrite模式。AOF文件没被 rewrite 之前(文件过大时会对命令 进行合并重写),可以删除其中的某些命令(比如误操作的 flushall);

缺点:

  • AOF文件比RDB文件大,且恢复速度慢;
  • 数据集大的时候,比RDB启动效率低;

RDB和AOF的对比

  • AOF文件比RDB更新频率高,优先使用AOF还原数据;
  • AOF比RDB更安全也更大;
  • RDB性能比AOF好;
  • 如果两个都配了优先加载AOF; Redis持久化就是把内存的数据写到磁盘中去,防止服务宕机了内存数据丢失;

原文链接:blog.csdn.net/weixin_4260…

===================================================

分布式缓存

缓存淘汰策略

数据过期策略(被动淘汰)

  • 惰性删除
    • 访问一个key时,redis先检查过期时间,如果已过期立即删除 【性能好,但大量过期key未删除占用空间】
  • 定期删除
    • 把设置过期时间的key放入一个独立字典
    • redis默认对字典每100ms扫描一次,检查过期时间,已过期就删除
    • 扫描采用贪心策略,每次随机扫描20个key。如果已过期比例超过25%则再次随机。

内存淘汰策略(内存已满,主动淘汰)

  • 当写入数据发现超出maxmemory限制时,采用策略删除
  • 八种策略:
    • noeviction:直接返回错,不进行内存淘汰
    • volatile-ttl:从设置了过期时间的key中,选择过期时间最早的key,淘汰
    • volatile-radom:从设置了过期时间的key中,随机选择key,淘汰
    • volatile-lru:从设置了过期时间的key中,用LRU算法选择key,淘汰
    • volatile-lfu:从设置了过期时间的key中,用LFU算法选择key,淘汰
    • allkeys-random:从所有key中,随机选择key,淘汰
    • allkeys-lru:从所有key中,用LRU算法选择key,淘汰
    • allkeys-lfu:从所有key中,用LFU算法选择key,淘汰
  • LRU算法

    • 按照最近最少使用原则筛选数据,筛选出最不常用数据。(比较访问时间)
    • 底层用链表实现,当访问/更新/写入key时,放到链表头,末尾就是要淘汰的
    • 【不靠谱:如果一个冷key不经常访问,只是刚刚被用过比较新,会被误认为是常用热数据】
  • LFU算法

    • 统计访问次数,访问次数最低的数据淘汰
    • 若访问次数相同,按LRU淘汰。(比较访问时间,淘汰访问时间更早的数据)

image.png

原文链接:blog.csdn.net/a745233700/…

缓存与数据库同步

增/删/改时考虑,读直接读缓存+数据库

  • 同步策略:

    • 先更新缓存,再更新数据库
    • 先更新数据库,再更新缓存
    • 先删除缓存,再更新数据库
    • 先更新数据库,再删除缓存
  • 建议方式:

    • 先更新数据库,再删缓存
    • 第二部失败,采用重试机制解决

为什么是删缓存不是更新?

  • 更新缓存不会出现查询未命中,便于读取,但性能消耗大。如果频繁更新但没有读操作,浪费性能。
  • 删除缓存操作简便。下次查询缓存,会未命中,再读一次数据库,回填到缓存就行。

假如第二步失败时,先操作缓存还是数据库?

  • 先删除缓存再更新数据库
    • 数据不同步

image.png

  • 先更新数据库再删除缓存
    • 数据可同步(下会读取数据库回填到缓存),但一些线程会读到旧数据

image.png

先删除缓存,再更新数据库 先更新数据库,再删除缓存

原文链接:blog.csdn.net/weixin_4412…

分布式缓存常见问题

缓存穿透

问题: 客户端查询根本不存在的数据,使得请求直达存储层,导致其负载过大,甚至宕机可能是业务层误将缓存和库中数据删除了,也可能是有人恶意攻击,专门访问库中不存在的数据。

解决:

  1. 缓存空对象:存储层未命中后,仍然将空值存入缓存层,客户端再次访问数据时,缓存层会直接返回空值
  2. 布隆过滤器:将数据存入布隆过滤器,访问缓存之前以过滤器拦截,若请求的数据不存在则直接返回空值。底层是好个哈希算法,估算可能是否存在。

缓存击穿

问题:一份热点数据,它的访问量非常大。在其缓存失效的瞬间,大量请求直达存储层,导致服务崩溃。(单个数据过热,挂了以后,局部击穿

解决:

  1. 永不过期

    • 热点数据不设置过期时间,是“物理”上的永不过期;
    • 程序中为每个数据设置逻辑过期时间(redis中不设置过期时间)。当数据逻辑过期时,redis不删缓存,使用单独的线程重建缓存;
  2. 加互斥锁

    • 对数据的访问加互斥锁,当一个线程访问该数据时,其他线程只能等待。这个线程访问过后,缓存中的数据将被重建,届时其他线程就可以直接从缓存中取值。??

缓存雪崩

问题:在某一时刻,缓存层无法继续提供服务,导致所有的请求直达存储层,造成数据库宕机。可能是缓存中有大量数据同时过期,也可能是Redis节点发生故障,导致大量请求无法得到处理。(整个缓存服务宕机,挂了,整体雪崩

解决:

  1. 避免数据同时过期

    • 设置过期时间时,附加一个随机数,避免大量的key同时过期
  2. 启用降级和熔断措施

    • 发生雪崩时,若应用访问非核心数据,则直接返回预定义信息/空值/错误信息
    • 在发生雪崩时,对于访问缓存接口请求,客户端并不会把请求发给Redis,而是直接返回
  3. 构建高可用的缓存服务

    • 采用哨兵或集群模式,部署多个Redis实例,个别节点宕机,依然可以保持服务的整体可用。

image.png