缓存设计
导流:将原本复杂的操作请求(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。减少了用户响应时间,提升了系统吞吐量。
读缓存和写缓存:
读缓存:用缓存的命中率,替换数据提供方的操作。能减少用户的请求时间,能减少系统的总处理时间。
写缓存:花费额外的时间。来延迟数据处理方的操作,减少用户的等待。只能减少请求响应的时间,反而会增加系统的总处理时间。
是。
1s 0.1s(1s) 10倍。
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。
适合场景:请求峰谷值变化明显、对实时性要求不高的场景。