缓存设计

导流:将原本复杂的操作请求(sql 大堆),引导到简单的请求上。前人栽树后人乘凉。

缓存:空间换时间的一个做法。

redis, memcached,localcache guava,客户端缓存,

user_info_xxxx : 姓名,年龄,xxx。getKey 内存操作

select * from user where id = xxx。 硬盘IO

缓存的收益

成本,收益。

读、写。

位置:介于 请求方和提供方之间。

收益:节省了响应时间。

成本:

kv

计算key的时间,查询key的时间,转换值的时间。命中率P。

所有数据的查询时间=计算key的时间+查询key的时间+转换值的时间+(1-p) * 原始查询时间

计算key的时间+查询key的时间+转换值的时间+(1-p) * 原始查询时间 <<<< 所有数据 的原始查询时间

适合:耗时特别长的查询(复杂sql),读多写少。

缓存键的设计

kv

单向函数:给定输入,很容易,很快能计算出结果,但是给你结果,很难计算出输入。

正向快速,逆向困难,输入敏感,冲突避免。

sha-256

冲突的概率 极低。

查询key的速度 取决于:物理位置 (内存,硬盘)。

值:

序列化

对象

总结:

无碰撞。高效生成。高效查询,高效转换。

上面所有:都被中间件提供的api 封装了。

实际中:前缀_业务关键信息_后缀。 公司统一制定规范

user_order_xxxx:

user+order+xxxx:

user-order-xxxx:

$

缓存的更新机制

被动更新

调用方 暂存方(缓存) 数据提供方

被动:有效期到后,再次写入。

1。客户端 查数据,缓存中没有,从提供方获取,写入缓存(有一个过期时间t)。

2。在t内,所有的查询,都由缓存提供。所有的写,直接写数据库。

3。当 缓存数据 t 到点了,缓存 数据 变没有。后面的查询,回到了第1步。

适合:对数据准确性和实时性要求不高的场景。比如:商品 关注的人数。

主动更新

主动:被其他操作直接填充。

数据库:更新数据库

缓存:更新缓存,删除缓存

保持一个定量,考虑围绕它的变量,这样才不会有异常的遗漏。

更新缓存,更新数据库

数据不一致的风险比较高,所以一般不采用。

更新数据库,更新缓存

一般也不采用。

请求被阻塞,

业务要求:修改数据库,然后经过大量的计算,才能得出缓存的值。浪费了性能。如果缓存还没用,更浪费。

删除缓存,为了节省计算时间。

删除缓存,更新数据库

一般不采用,因为大概率 读比写快。

延时双删,休眠多久,系统吞吐量下降。

昨天被高德一个面试问:说,你这个延时双删有这么几步操作。如果其中某一步失败了这么办?

删除缓存

更新数据库:事务,回滚就OK。

第二次删除缓存

重试删除:当你前面的操作,无法回滚时,为了保证后续数据的一致性,

(最便宜的做法)硬着头皮往前走,重试。

借用中间件:消息队列,重发消息。

系统外订阅:canal。binlog。

二次删除key,和我们的业务代码解耦。

更新数据库,删除缓存

经常采用的方式。

cache-aside模式。

异常流程:

前提:缓存无数据。数据库有数据。

A:查询,B:更新

A查缓存,无数据,去读数据库,旧值。-----查

B更新数据库 新值

B删除缓存

A 将旧值写入缓存。

脏数据。

就是说这个方案也有问题?这次是读的速度慢了?

读比写慢 概率很低,极低。

缓存无数据。

如果非要解决,延时双删。再删除一次。

Read/Write Through

程序启动时,将数据库 的数据, 放到缓存中,不能等启动完成,再放缓存中。

Write Behind

降低了写操作的时间,提高了系统吞吐量。

双写一致性。

缓存清理机制

如何提升缓存命中率:尽可能多的缓存。所有数据都放缓存,命中率 100%。

我们需要用有限的缓存空间,发挥最大的作用。

如何判断 一个数据 在未来被访问的次数呢?

读的时间频繁:当清理一个数据的时候,发现,它一直被访问,那我就认为他 马上的未来,也会被访问。

写入时间的时间节点。

我是问代码怎么实现:当清理数据时,发现他一直被访问。

读一次,记录一次 ,时间。阈值。

读:

getKey, k =0 ttl 1min , incr

if(!getK > 1){

delete k

}

时效性清理

给缓存设置一个过期时间,到期 缓存 自动 清理。

缓存中的数据 有 一个 生存时间:ttl。过期时间。set k v ex 10 s

set cookie   过期时间。

k v 10s

定时任务轮询。delete

自动清理机制: cookie redis expire .。(本质:轮询)

数目阈值式清理机制

判断缓存中的缓存的数量 达到一定值 ,对缓存进行清理。

阈值:根据自己的业务来定。1g,1m,1024个, 800 80%。

采取什么策略去清理:

fifo: 先进先出

package com.example.cachetest;

import java.util.LinkedList;
import java.util.Queue;

/**
 * 数据阈值式清理
 */
public class CacheThresholdTest {
    public static void main(String[] args) {
        Queue<String> queue = new LinkedList<>();
        for (int i = 0; i < 4; i++) {
            setCache(queue,""+i);
        }

    }

    public static void setCache(Queue<String> queue,String cache){
        int size = queue.size();
        if (size >= 3){
            queue.poll();
        }
        queue.add(cache);
        System.out.println("缓存中的值如下:");
        for (String q: queue) {
            System.out.println(q);
        }
    }
}

random:随机

lru:规律:

LinkedHashMap 套。fifo,lru。

map:存 键值对。

顺序:插入顺序 fifo,访问顺序 lru。

removeEldestEntry。

package com.example.cachetest;

import org.junit.jupiter.api.Test;

import java.util.LinkedHashMap;
import java.util.Map;
public class TestCache {
    @Test
    public void testLinkedHashMap() {
        // 在介绍accessOrder属性的时候说accessOrder设置为false时,按照插入顺序,设置为true时,按照访问顺序
        LinkedHashMap<String, String> map = new LinkedHashMap<String, String>(5, 0.75F, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
                //当LinkHashMap的容量大于等于5的时候,再插入就移除旧的元素
                return this.size() >= 5;
            }
        };
        map.put("1", "bb");
        map.put("2", "dd");
        map.put("3", "ff");
        map.put("4", "hh");

        System.out.println("原始顺序:");
        print(map);
        map.get("2");
        System.out.println("2 最近访问:");
        print(map);

        map.get("3");
        System.out.println("3 最近访问:");
        print(map);


        map.put("5","oo");
        System.out.println("加元素");
        print(map);
    }
 
    void print(LinkedHashMap<String, String> source) {
        source.keySet().iterator().forEachRemaining(System.out::println);
    }
}   

实现:k v。 map 一台服务器上能用。redis。

软引用清理

用空间换时间的模块。尽量用空间,用以提高缓存 命中率p。

适时的释放空间,gc。

识别出要清理的缓存,然后清除。

gc root引用。

强:哪怕自己oom,不清理。(不用)

软:当空间不足的时候,会被回收。√。

空间不足时,进行缓存清理。软引用。

把值 放到 SoftReference 包装中。

package com.example.cachetest;

import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.*;

/**
 * 软引用 缓存 实验
 */
public class CacheSoftReferenceTest {
    public static void main(String[] args) throws InterruptedException {

        soft();
    }
    static void soft(){
        // 缓存
        Map<Integer, SoftRefedStudent> map = new HashMap<Integer, SoftRefedStudent>();
        ReferenceQueue<Student> queue = new ReferenceQueue<Student>();
        int i = 0;
        while (i < 10000000) {
            Student p = new Student();
            map.put(i, new SoftRefedStudent(i, p, queue));
            //p = null;
            SoftRefedStudent pollref = (SoftRefedStudent) queue.poll();
            if (pollref != null) {//找出被软引用回收的对象
                //以key为标志,从map中移除
                System.out.println("回收"+pollref.key);
                map.remove(pollref.key);

                System.out.println(i+"新一轮================================================");
                Iterator<Map.Entry<Integer, SoftRefedStudent>> iterator = map.entrySet().iterator();
                while (iterator.hasNext()){
                    Map.Entry entry = iterator.next();
                    if ((int)entry.getKey() == pollref.key){
                        System.out.println("见鬼了");
                    }
                }
                System.out.println(i+"新一轮================================================");


            }
            i++;
        }
        System.out.println("done");
    }
}


class Student{
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

class SoftRefedStudent extends SoftReference<Student> {
    public int key;
    // 第3个参数叫做ReferenceQueue,是用来存储封装的待回收Reference对象的
    /**
     * 当Student对象被回收后,SoftRefedStudent对象会被加入到queue中。
     */
    public SoftRefedStudent(int key, Student referent, ReferenceQueue<Student> q) {
        super(referent, q);
        this.key = key;
    }
}

缓存清理机制总结

时效式清理+数目阈值:防止:短期内,密集查询,导致缓存空间的急剧增大。

–自己的完整思路。

lru+软引用:保证热数据,最大限度的提高 缓存命中率,p。

不建议:仅仅使用 软引用。因为我们失去了对它的控制。

目的:提高缓存命中率,节省空间,=》提升性能。

缓存风险概述

在系统中,每增加一个环节,就多一份风险。用是不得已。

缓存穿透

缓存中没有,数据库也没有。

方案:在第一次调用的时候,数据提供方返回一个空值,将空值放到缓存中。

缓存雪崩

大量缓存突然失效,导致大量的请求,倾泻到数据库上,引起数据库压力骤增。

时效式清理:批量缓存,统一时间到期。缓存ttl=(固定时间,结合业务)+随机时间。

软引用清理:某个时间点,空间突然紧张,常用的缓存用强引用,不常用的用软引用。

缓存击穿

高频率的缓存,突然失效,大量请求倾泻到数据库上。

lru:

read write through or write behind.更新机制:无所谓。数据永远留在缓存当中。

缓存预热

read write through or write behind

预热:高频访问的,提前准备。

计价规则,提前加载到缓存中。

系统启动前:加载缓存,不让缓存统一时间过期。

电商系统:热门商品,提前加入缓存。网约车中,计价规则提前加入缓存。

热门数据,加到缓存。

缓存风险的总结

遇到风险,分析原因,解决之。

原因:更新机制,清理机制。

缓存的位置

缓存来源:L1 L2 L3。

缓存的读取过程:

思考:如何避免cpu浪费时间。

减少我的等待时间----缓存。

我尽量多做事情-----多线程。

目的:降本增效。

级联系统缓存位置

要想系统性能好,缓存一定要趁早。

客户端缓存位置

1

2 秒杀系统,商品详情页就是静态文件(扣一块动态的)。

降级。

代码:storage。

浏览器:cookie。失效时间。如果非必要,不要用cookie做缓存。

静态缓存

1。静态页面。apache。录个视频,apache静态页。

2。通过数据库查出来的。

如果每个用户查出来的都一样。 物流信息:省市区。

凡是与用户个体无关的具有较强通用性的数据,都可以作为静态数据缓存。

已经有缓存页面,后台更改数据之后,如何让数据快速生效。cache aside。

不适合缓存通用性很差的数据。

服务缓存

个性化的动态的不值得缓存。但是 这些数据的生成 都有一个过程。

数据库本身的缓存

aside。

数据库耗时比较久。

怎么做?

冗余字段。订单表里 id,有用户姓名,商品名称。

中间表:学生表,课程表,排课表。

查询缓存:建议不用。mysql8以上,抛弃了。query cache.

select * from user_info

SELECT * from user_info.

指定规范,大小写该统一就统一。my.ini my.cnf。

清理碎片,flush query cache., reset query cache.

历史表:将数据放到 历史表中,以后的操作比如说 统计,可以延迟操作。而中间的数据存储 ,相当于一次缓存。

coder学员:新老数据,还在一起,统计数据剥离。

写缓存

目的:削峰。

数据处理方的 处理速率是固定的 ,为了 防止请求 洪峰 压垮系统。采用写缓存。

写缓存收益

只要能给数据进行更改的操作,都叫写。

数据处理方时间。10s

引入缓存后:写缓存时间 2s,从缓存读取数据的时间,传递时间,数据处理方时间。

收益在于:用户。-8s。减少了用户响应时间,提升了系统吞吐量。

读缓存和写缓存:

读缓存:用缓存的命中率,替换数据提供方的操作。能减少用户的请求时间,能减少系统的总处理时间。

写缓存:花费额外的时间。来延迟数据处理方的操作,减少用户的等待。只能减少请求响应的时间,反而会增加系统的总处理时间。

分布式之缓存详解-LMLPHP

是。

1s 0.1s(1s) 10倍。

分布式之缓存详解-LMLPHP

i like u(you)

4(for) u

2(to) B

log4j log for java

C#。

day day up:

cache aside。更新完数据库,删除缓存。 迁移到: 修改完页面,删除静态页。io.write(“d:/apache/www/x.html”)

写缓存实践

利用redis 发布订阅。

MQ

数据库。(先写数据,剩下的和主业务无关的操作,后置)

目的:只要能减少用户的响应时间。就OK。

适合场景:请求峰谷值变化明显、对实时性要求不高的场景。

03-29 09:49