开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

逻辑思维题

发表于 2020-12-07

⭐️ 本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 和 BaguTree Pro 知识星球提问。

在计算机面试中,偶尔会遇到逻辑类题目。由于题目花样百出,准备难度较大,题海战术可能不是推荐的策略。同时,我认为回答问题的思路比答案本身更加重要。在这个专栏里,我将精选一些经典的逻辑思维题,希望能帮助你找到解题思路 / 技巧。

本文是逻辑思维系列的第 2 篇文章,完整文章目录请移步到文章末尾~

前言

大家好,我是小彭。

在计算机面试中,逻辑类题目是规模以上互联网公司的必考题。由于题目花样百出,准备难度较大,题海战术可能不是推荐的做法。在这个系列里,我将精选十道非常经典的逻辑题,希望能帮助你找到解题思路 / 技巧。如果能帮上忙,请务必点赞加关注,这真的对我非常重要。


  1. 题目描述

一群人开舞会,每人头上都戴着一顶帽子。帽子只有黑白两种,黑的至少有一顶。每个人都能看到其它人帽子的颜色,却看不到自己的。主持人先让大家看看别人头上戴的是什么帽子,然后关灯,如果有人认为自己戴的是黑帽子,就打自己一个耳光。第一次关灯,没有声音。于是再开灯,大家再看一遍,关灯时仍然鸦雀无声。一直到第三次关灯,才有劈劈啪啪打耳光的声音响起。问有多少人戴着黑帽子?(假设每个人都足够聪明)


2.解题关键

  • 定义问题: 假设问题为 y=F(n)y=F(n)y=F(n),表示当有 nnn 顶黑帽时,会在第 yyy 天打脸;
  • 每个人都看不到自己的帽子,只能通过观察别人的帽子的表现猜测自己的帽子;
  • 终止条件: 当一个人眼前都是白帽时,由于至少有一个黑帽,则说明他自己是黑帽。

  1. 题解

  • F(1)F(1)F(1):由于只且仅有 1 顶黑帽,那么黑帽 A 眼前全是白帽,他很清楚自己是黑帽,因此一定会在第 1 天打脸。即 F(1)=1F(1) = 1F(1)=1;
  • F(2)F(2)F(2):由于只有 2 顶黑帽,大多数人眼前有 2 顶黑帽,而其中黑帽 A 和 B 最为特殊,他们眼前只有 1 顶黑帽。聪明的 A 知道 B 眼前只有两种情况:全是白帽 or 只有 A 头上的黑帽。聪明的 B 也知道 A 眼前只有两种情况:全是白帽 or 只有 B 头上的黑帽。因为 F(2)F(2)F(2) 没有人眼前全是白色,所以第 1 天不会有人打脸。那么 A 和 B 观察到对方没有在第 1 天打脸,分别都知道自己是黑帽,因此会在第 2 天打脸。即 F(2)=2F(2) = 2F(2)=2;
  • F(3)F(3)F(3):由于只有 2 顶黑帽,大多数人眼前有 3 顶黑帽,特殊的黑帽 A、B 和 C 眼前只有 2 顶。它们分别观察到其他两个人均没有在第 2 天打脸,同理,就确信自己是黑帽。即 F(3)=3F(3)=3F(3)=3。
  • 一次类推,有 F(x)=xF(x) = xF(x)=x,有几顶黑帽,就会在第几天打脸。

推荐阅读

逻辑思维系列往期回顾:

  • #1 25 匹马 5 条赛道,最快需要几轮求出前 3 名?
  • #2 舞会上有多少顶黑帽?
  • #3 至少要几个砝码,可以称出 1g ~ 40g 重量
  • #4 我知道你不知道,我到底知不知道

⭐️ 永远相信美好的事情即将发生,欢迎加入小彭的 Android 交流社群~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

java中这些stream流不需要关闭,你知道几个?

发表于 2020-12-07

曾几何时,作为java程序员要记住的一条准则就是,流用完了一定要在关闭,一定要写在finally里。

1
2
3
4
csharp复制代码finally {
out.flush();
out.close();
}

但是最近发现一个stream是不需要关闭的。它就是ByteArrayOutputStream,当然还有它的妹妹ByteArrayInputStream和表哥StringWriter。道理一样,我们就不讨论亲戚们了。
作为一种OutputStream它也extends了OutputStream,自然也有继承了flush()和close()。
但这2个方法的方法体为空。

1
2
3
4
5
6
7
8
9
10
11
12
13
csharp复制代码    /**
* Closing a <tt>ByteArrayOutputStream</tt> has no effect. The methods in
* this class can be called after the stream has been closed without
* generating an <tt>IOException</tt>.
*/
public void close() throws IOException {
}

/***
* OutputStream的方法,ByteArrayInputStream并未重写
*/
public void flush() throws IOException {
}

究其原因也不难理解。其实ByteArrayInputStream内部实现是一个byte数组,是基于内存中字节数据的访问。并没有占用硬盘,网络等资源。就算是不关闭,用完了垃圾回收器也会回收掉。这点跟普通数组并没有区别。既然是操作内存,就要考虑到内存大小,如果字节流太大,就要考虑内存溢出的情况。

但是,作为一个蛋疼的程序员,习惯性关闭流是一个好习惯,不管三七五十八,先close掉再说,现在close是空方法,保不齐哪天就有了呢?这也是百利无一害的事,就好像保健品,吃了不治病,但是也吃不坏。

  • 结论就是:指向内存的流可以不用关闭,指向硬盘/网络等外部资源的流一定要关闭。
    本文已收录到java中这些stream流不需要关闭,你知道几个

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

逻辑思维题

发表于 2020-12-07

⭐️ 本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 和 BaguTree Pro 知识星球提问。

在计算机面试中,偶尔会遇到逻辑类题目。由于题目花样百出,准备难度较大,题海战术可能不是推荐的策略。同时,我认为回答问题的思路比答案本身更加重要。在这个专栏里,我将精选一些经典的逻辑思维题,希望能帮助你找到解题思路 / 技巧。

本文是逻辑思维系列的第 3 篇文章,完整文章目录请移步到文章末尾~

前言

大家好,我是小彭。

在计算机面试中,逻辑类题目是规模以上互联网公司的必考题。由于题目花样百出,准备难度较大,题海战术可能不是推荐的做法。在这个系列里,我将精选十道非常经典的逻辑题,希望能帮助你找到解题思路 / 技巧。如果能帮上忙,请务必点赞加关注,这真的对我非常重要。


  1. 问题描述

给定一台天平,至少要几个砝码,可以称出 1g ~ 40g 这 40 个重量?

这个问题等同于 “德·梅齐利亚克砝码”问题:一位商人有一个 40 磅的砝码,由于跌落在地而碎成4 块。后来,称得每块碎片的重量都是整磅数,而且可以用这 4 块来称从 1 ~ 40 磅之间的任意整数磅的重物。(引用自法国数学家 G.B.德·梅齐里亚克)问这 4 块砝码碎片各重多少?


2.解题关键

砝码的和与差: 假设有 m 和 n 两个砝码(m > n),除了可以称出 m + n 的重量外,还可以称出 m - n 的重量。


  1. 题解

令 AxA_xAx 表示第 xxx 块砝码的重量。

  • 第 1 块砝码 A1A_1A1:为了称取重量 1g ,必须拥有一枚重量为 1g 的砝码,即 A1A_1A1 = 1。目前可以称 {1, 2, 3}。
  • 第 2 块砝码 A2A_2A2:砝码组 [1,A2][1, A2][1,A2],可以称出 {1,A2−1,A2,A2+1}{1, A_2 - 1, A_2, A_2 + 1}{1,A2−1,A2,A2+1}。为了称取重量 2g,显然有 A2A_2A2 - 1 = 2,即 A2A_2A2 = 3。目前可以称 {1, 2, 3, 4}。
  • 第 3 块砝码 A3A_3A3:砝码组 [1,3,A3][1, 3, A3][1,3,A3],可以称出 {1,2,3,4,A3−4,A3−3,A3−2,A3−1,A3,A3+1,A3+2,A3+3,A3+4}{1, 2, 3, 4, A_3 - 4, A_3 - 3, A_3 - 2, A_3 - 1, A_3, A_3 + 1, A_3 + 2, A_3 + 3, A_3 + 4}{1,2,3,4,A3−4,A3−3,A3−2,A3−1,A3,A3+1,A3+2,A3+3,A3+4}。为了称取重量 5g,显然有 A3A_3A3 - 4 = 5,即 A3A_3A3 = 9。目前可以称 {1, 2, 3, 4, …, 13}。
  • 第 4 块砝码:同理,第 4 块砝码 A4A_4A4 = 27,可以称出 {1,2,3,4,…,40}{1, 2, 3, 4,…, 40}{1,2,3,4,…,40}。总共需要 4 个砝码。

参考资料

  • 《拜托,面试别再问我三进制了!!!》 —— 沈剑 著
  • 《世界上最完美的砝码组合—神秘的“3”重现江湖!》 —— 隔壁家的二傻子 著

推荐阅读

逻辑思维系列往期回顾:

  • #1 25 匹马 5 条赛道,最快需要几轮求出前 3 名?
  • #2 舞会上有多少顶黑帽?
  • #3 至少要几个砝码,可以称出 1g ~ 40g 重量
  • #4 我知道你不知道,我到底知不知道

⭐️ 永远相信美好的事情即将发生,欢迎加入小彭的 Android 交流社群~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

实战 如何消除又臭又长的ifelse判断更优雅的编程?

发表于 2020-12-07

最近在做代码重构,发现了很多代码的烂味道。其他的不多说,今天主要说说那些又臭又长的if…else要如何重构。

在介绍更更优雅的编程之前,让我们一起回顾一下,不好的if…else代码

一、又臭又长的if…else

废话不多说,先看看下面的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
typescript复制代码public interface IPay {

void pay();
}


@Service
public class AliaPay implements IPay {


@Override
public void pay() {
System.out.println("===发起支付宝支付===");
}
}


@Service
public class WeixinPay implements IPay {

@Override
public void pay() {
System.out.println("===发起微信支付===");
}
}


@Service
public class JingDongPay implements IPay {

@Override
public void pay() {
System.out.println("===发起京东支付===");
}
}

@Service
public class PayService {

@Autowired
private AliaPay aliaPay;
@Autowired
private WeixinPay weixinPay;
@Autowired
private JingDongPay jingDongPay;


public void toPay(String code) {
if ("alia".equals(code)) {
aliaPay.pay();
} else if ("weixin".equals(code)) {
weixinPay.pay();
} else if ("jingdong".equals(code)) {
jingDongPay.pay();
} else {
System.out.println("找不到支付方式");
}
}
}

PayService类的toPay方法主要是为了发起支付,根据不同的code,决定调用用不同的支付类(比如:aliaPay)的pay方法进行支付。

这段代码有什么问题呢?也许有些人就是这么干的。

试想一下,如果支付方式越来越多,比如:又加了百度支付、美团支付、银联支付等等,就需要改toPay方法的代码,增加新的else…if判断,判断多了就会导致逻辑越来越多?

很明显,这里违法了设计模式六大原则的:开闭原则 和 单一职责原则。
❝开闭原则:对扩展开放,对修改关闭。就是说增加新功能要尽量少改动已有代码。

单一职责原则:顾名思义,要求逻辑尽量单一,不要太复杂,便于复用。

那有什么办法可以解决这个问题呢?

二、使用注解

代码中之所以要用code判断使用哪个支付类,是因为code和支付类没有一个绑定关系,如果绑定关系存在了,就可以不用判断了。

我们先定义一个注解。

1
2
3
4
5
6
7
8
less复制代码@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface PayCode {

String value();

String name();
}

然后在所有的支付类上都加上注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
typescript复制代码@PayCode(value = "alia", name = "支付宝支付")
@Service
public class AliaPay implements IPay {


@Override
public void pay() {
System.out.println("===发起支付宝支付===");
}
}



@PayCode(value = "weixin", name = "微信支付")
@Service
public class WeixinPay implements IPay {

@Override
public void pay() {
System.out.println("===发起微信支付===");
}
}


@PayCode(value = "jingdong", name = "京东支付")
@Service
public class JingDongPay implements IPay {

@Override
public void pay() {
System.out.println("===发起京东支付===");
}
}

然后增加最关键的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
typescript复制代码@Service
public class PayService2 implements ApplicationListener<ContextRefreshedEvent> {

private static Map<String, IPay> payMap = null;

@Override
public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
ApplicationContext applicationContext = contextRefreshedEvent.getApplicationContext();
Map<String, Object> beansWithAnnotation = applicationContext.getBeansWithAnnotation(PayCode.class);

if (beansWithAnnotation != null) {
payMap = new HashMap<>();
beansWithAnnotation.forEach((key, value) -> {
String bizType = value.getClass().getAnnotation(PayCode.class).value();
payMap.put(bizType, (IPay) value);
});
}
}

public void pay(String code) {
payMap.get(code).pay();
}
}

PayService2类实现了ApplicationListener接口,这样在onApplicationEvent方法中,就可以拿到ApplicationContext的实例。我们再获取打了PayCode注解的类,放到一个map中,map中的key就是PayCode注解中定义的value,跟code参数一致,value是支付类的实例。

这样,每次就可以每次直接通过code获取支付类实例,而不用if…else判断了。如果要加新的支付方法,只需在支付类上面打上PayCode注解定义一个新的code即可。

注意:这种方式的code可以没有业务含义,可以是纯数字,只有不重复就行。

三、动态拼接名称

再看看这种方法,主要针对code是有业务含义的场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typescript复制代码@Service
public class PayService3 implements ApplicationContextAware {

private ApplicationContext applicationContext;

private static final String SUFFIX = "Pay";

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}

public void toPay(String payCode) {
((IPay) applicationContext.getBean(getBeanName(payCode))).pay();
}

public String getBeanName(String payCode) {
return payCode + SUFFIX;
}

}

我们可以看到,支付类bean的名称是由code和后缀拼接而成,比如:aliaPay、weixinPay和jingDongPay。这就要求支付类取名的时候要特别注意,前面的一段要和code保持一致。调用的支付类的实例是直接从ApplicationContext实例中获取的,默认情况下bean是单例的,放在内存的一个map中,所以不会有性能问题。

特别说明一下,这种方法实现了ApplicationContextAware接口跟上面的ApplicationListener接口不一样,是想告诉大家获取ApplicationContext实例的方法不只一种。

四、模板方法判断

当然除了上面介绍的两种方法之外,spring的源码实现中也告诉我们另外一种思路,解决if…else问题。

我们先一起看看spring AOP的部分源码,看一下DefaultAdvisorAdapterRegistry的wrap方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码  public Advisor wrap(Object adviceObject) throws UnknownAdviceTypeException {
if (adviceObject instanceof Advisor) {
return (Advisor) adviceObject;
}
if (!(adviceObject instanceof Advice)) {
throw new UnknownAdviceTypeException(adviceObject);
}
Advice advice = (Advice) adviceObject;
if (advice instanceof MethodInterceptor) {
// So well-known it doesn't even need an adapter.
return new DefaultPointcutAdvisor(advice);
}
for (AdvisorAdapter adapter : this.adapters) {
// Check that it is supported.
if (adapter.supportsAdvice(advice)) {
return new DefaultPointcutAdvisor(advice);
}
}
throw new UnknownAdviceTypeException(advice);
}

重点看看supportAdvice方法,有三个类实现了这个方法。我们随便抽一个类看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typescript复制代码class AfterReturningAdviceAdapter implements AdvisorAdapter, Serializable {

@Override
public boolean supportsAdvice(Advice advice) {
return (advice instanceof AfterReturningAdvice);
}

@Override
public MethodInterceptor getInterceptor(Advisor advisor) {
AfterReturningAdvice advice = (AfterReturningAdvice) advisor.getAdvice();
return new AfterReturningAdviceInterceptor(advice);
}

}

该类的supportsAdvice方法非常简单,只是判断了一下advice的类型是不是AfterReturningAdvice。

我们看到这里应该有所启发。

其实,我们可以这样做,定义一个接口或者抽象类,里面有个support方法判断参数传的code是否自己可以处理,如果可以处理则走支付逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
typescript复制代码public interface IPay {

boolean support(String code);

void pay();
}


@Service
public class AliaPay implements IPay {

@Override
public boolean support(String code) {
return "alia".equals(code);
}

@Override
public void pay() {
System.out.println("===发起支付宝支付===");
}
}


@Service
public class WeixinPay implements IPay {


@Override
public boolean support(String code) {
return "weixin".equals(code);
}

@Override
public void pay() {
System.out.println("===发起微信支付===");
}
}


@Service
public class JingDongPay implements IPay {
@Override
public boolean support(String code) {
return "jingdong".equals(code);
}

@Override
public void pay() {
System.out.println("===发起京东支付===");
}
}

每个支付类都有一个support方法,判断传过来的code是否和自己定义的相等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
java复制代码@Service
public class PayService4 implements ApplicationContextAware, InitializingBean {

private ApplicationContext applicationContext;

private List<IPay> payList = null;

@Override
public void afterPropertiesSet() throws Exception {
if (payList == null) {
payList = new ArrayList<>();
Map<String, IPay> beansOfType = applicationContext.getBeansOfType(IPay.class);

beansOfType.forEach((key, value) -> payList.add(value));
}
}

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}

public void toPay(String code) {
for (IPay iPay : payList) {
if (iPay.support(code)) {
iPay.pay();
}
}
}

}

这段代码中先把实现了IPay接口的支付类实例初始化到一个list集合中,返回在调用支付接口时循环遍历这个list集合,如果code跟自己定义的一样,则调用当前的支付类实例的pay方法。

五、其他的消除if…else的方法

当然实际项目开发中使用if…else判断的场景非常多,上面只是其中几种场景。下面再列举一下,其他常见的场景。

1.根据不同的数字返回不同的字符串

1
2
3
4
5
6
7
8
9
10
11
12
arduino复制代码public String getMessage(int code) {
if (code == 1) {
return "成功";
} else if (code == -1) {
return "失败";
} else if (code == -2) {
return "网络超时";
} else if (code == -3) {
return "参数错误";
}
throw new RuntimeException("code错误");
}

其实,这种判断没有必要,用一个枚举就可以搞定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
arduino复制代码public enum MessageEnum {

SUCCESS(1, "成功"),
FAIL(-1, "失败"),
TIME_OUT(-2, "网络超时"),
PARAM_ERROR(-3, "参数错误");

private int code;
private String message;

MessageEnum(int code, String message) {
this.code = code;
this.message = message;
}

public int getCode() {
return this.code;
}

public String getMessage() {
return this.message;
}

public static MessageEnum getMessageEnum(int code) {
return Arrays.stream(MessageEnum.values()).filter(x -> x.code == code).findFirst().orElse(null);
}
}

再把调用方法稍微调整一下

1
2
3
4
arduino复制代码public String getMessage(int code) {
MessageEnum messageEnum = MessageEnum.getMessageEnum(code);
return messageEnum.getMessage();
}

完美。

2.集合中的判断

上面的枚举MessageEnum中的getMessageEnum方法,如果不用java8的语法的话,可能要这样写

1
2
3
4
5
6
7
8
csharp复制代码public static MessageEnum getMessageEnum(int code) {
for (MessageEnum messageEnum : MessageEnum.values()) {
if (code == messageEnum.code) {
return messageEnum;
}
}
return null;
}

对于集合中过滤数据,或者查找方法,java8有更简单的方法消除if…else判断。

1
2
3
scss复制代码public static MessageEnum getMessageEnum(int code) {
return Arrays.stream(MessageEnum.values()).filter(x -> x.code == code).findFirst().orElse(null);
}

3.简单的判断

其实有些简单的if…else完全没有必要写,可以用三目运算符代替,比如这种情况:

1
2
3
4
5
6
arduino复制代码public String getMessage2(int code) {
if(code == 1) {
return "成功";
}
return "失败";
}

改成三目运算符:

1
2
3
arduino复制代码public String getMessage2(int code) {
return code == 1 ? "成功" : "失败";
}

修改之后代码更简洁一些。

4.判断是否为null

java中自从有了null之后,很多地方都要判断实例是否为null,不然可能会出现NPE的异常。

1
2
3
4
5
6
7
8
typescript复制代码  public String getMessage2(int code) {
return code == 1 ? "成功" : "失败";
}

public String getMessage3(int code) {
Test test = null;
return test.getMessage2(1);
}

这里如果不判断异常的话,就会出现NPE异常。我们只能老老实实加上判断。

1
2
3
4
5
6
7
java复制代码public String getMessage3(int code) {
Test test = null;
if (test != null) {
return test.getMessage2(1);
}
return null;
}

有没有其他更优雅的处理方式呢?

1
2
3
4
5
csharp复制代码public String getMessage3(int code) {
Test test = null;
Optional<Test> testOptional = Optional.of(test);
return testOptional.isPresent() ? testOptional.get().getMessage2(1) : null;
}

答案是使用Optional

当然,还有很多其他的场景可以优化if…else,我再这里就不一一介绍了,感兴趣的朋友可以给我留言,一起探讨和研究一下。

最后说一句(求关注,别白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙扫描下发二维码关注一下,您的支持是我坚持写作最大的动力。

求一键三连:点赞、转发、在看。

在公众号中回复:面试、代码神器、开发手册、时间管理有超赞的粉丝福利,另外回复:加群,可以跟很多BAT大厂的前辈交流和学习。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Android 毫分缕析!说说图片加载的整个过程 前言

发表于 2020-12-07

点赞关注,不再迷路,你的支持对我意义重大!

🔥 Hi,我是丑丑。本文 GitHub · Android-NoteBook 已收录,这里有 Android 进阶成长路线笔记 & 博客,欢迎跟着彭丑丑一起成长。(联系方式在 GitHub)

前言

  • 最近我负责了一些相册相关的需求,在完成业务的同时,也希望对图片加载的过程有更深入的认识;
  • 在这篇文章里,我将从源码上探讨 图片加载 的过程,文章中引用的核心源码我已经进行了简化与梳理,相信能极大减低你的学习成本。如果能帮上忙,请务必点赞加关注,这真的对我非常重要。

相关文章

  • 《图形学 | 格物致知!PNG 除了无损压缩你还知道什么?》
  • 《Android | 老生常谈!屏幕适配原理 & 方案总结笔记》
  • 《Android | 毫分缕析!说说图片加载的整个过程》

目录


  1. 图片解码选项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
arduino复制代码public static class Options {

public Bitmap inBitmap;
public boolean inMutable;
public boolean inJustDecodeBounds;
public boolean inPremultiplied;
public int inDensity;
public int inTargetDensity;
public int inScreenDensity; // 用不到
public boolean inScaled;
public int inSampleSize;
public int outWidth;
public int outHeight;
public Bitmap.Config outConfig;
public ColorSpace outColorSpace;
}
1
2
3
4
ini复制代码public Options() {
inScaled = true;
inPremultiplied = true;
}

  1. 图片资源加载过程概述

现在,我们来看加载图片资源的入口方法:

BitmapFactory.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
csharp复制代码入口方法 1
public static Bitmap decodeResource(Resources res, int id) {
return decodeResource(res, id, null);
}

入口方法 2(已简化)
public static Bitmap decodeResource(Resources res, int id, Options opts) {
1、匹配资源 id,打开 InputStream
final TypedValue value = new TypedValue();
InputStream is = res.openRawResource(id, value);

2、解码资源,返回 Bitmap
return decodeResourceStream(res, value, is, null, opts);
}

可以看到,两个入口方法的区别在于是否传入Options,在 第 5 节 我会讲到 通过配置Options来自定义解码,目前我们先当opts == null。

简化后的decodeResource(...)非常清晰,无非是分为两个步骤:

  • 1、匹配资源,打开 InputStream
  • 2、解码资源,返回 Bitmap


  1. 步骤一:匹配资源,打开 InputStream

这个步骤主要完成 从资源 id(一个 int 值)定位到具体某一个文件夹下的资源:

ResourcesImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
less复制代码-> 1、匹配资源 id,打开 InputStream

最终调用到(已简化):
InputStream openRawResource(@RawRes int id, TypedValue value) {
1.1 匹配资源
getValue(id, value, true);

1.2 打开输入流
return mAssets.openNonAsset(value.assetCookie, value.string.toString(),
AssetManager.ACCESS_STREAMING);
}

-> 1.1
void getValue(@AnyRes int id, TypedValue outValue, boolean resolveRefs) {
1.1.1 查找资源 id,并将相关信息存储在 outValue
boolean found = mAssets.getResourceValue(id, 0, outValue, resolveRefs);
if (!found) {
1.1.2 资源未找到,抛出异常
throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id));
}
}

AssetManager.java

1
2
3
4
5
6
7
8
java复制代码-> 1.1.1
boolean getResourceValue(@AnyRes int resId, int densityDpi, TypedValue outValue, boolean resolveRefs) {
final int cookie = nativeGetResourceValue(mObject, resId, (short) densityDpi, outValue, resolveRefs);
if (cookie <= 0) {
return false;
}
return true;
}

AssetManager.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
scss复制代码static jint NativeGetResourceValue(JNIEnv* env, jclass /*clazz*/, jlong ptr, jint resid,
jshort density, jobject typed_value,
jboolean resolve_references) {
匹配过程略,见下图...
return CopyValue(env, cookie, value, ref, flags, &selected_config, typed_value);
}

static jint CopyValue(JNIEnv* env, ...jobject out_typed_value) {
...
if (config != nullptr) {
关键信息:将文件夹对应的 density 保存在 TypeValue 中
env->SetIntField(out_typed_value, gTypedValueOffsets.mDensity, config->density);
}
return static_cast<jint>(ApkAssetsCookieToJavaCookie(cookie));
}

以上代码已经非常简化了,主要关注以下几点:

  • 在 1.1.1 分支,查找资源 id,并将相关信息存储在 outValue,其中比较关键的信息是:将文件夹对应的 densityDpi 保存在 TypeValue 中(这个值在下一节会用到);
  • 匹配过程比较冗长,直接看示意图:


引用自 blog.csdn.net/xuaho0907/a… —— 爱吃冰淇淋的羊驼


  1. 步骤二:解码资源,返回 Bitmap

在上一步匹配资源中,我们已经获得 InputStream & TypedValue(带有文件夹对应的 densityDpi),这一步我们将对图片资源进行解码,decodeResourceStream()代码如下:

BitmapFactory.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
ini复制代码-> 2、解码资源,返回 Bitmap

public static Bitmap decodeResourceStream(Resources res, TypedValue value,
InputStream is, Rect pad, Options opts) {

if (opts == null) {
opts = new Options();
}

2.1 如果未设置 inDensity,则设置为文件夹对应的 densityDpi
if (opts.inDensity == 0 && value != null) {
final int density = value.density;
if (density == TypedValue.DENSITY_DEFAULT) {
opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
} else if (density != TypedValue.DENSITY_NONE) {
关键点
opts.inDensity = density;
}
}

2.2 如果未设置 inTargetDensity,则设置为设备的 densityDpi
if (opts.inTargetDensity == 0 && res != null) {
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}

2.3 执行解码
return decodeStream(is, pad, opts);
}

-> 2.3 执行解码(已简化)
public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts) {
2.3.1 AssetManager 输入流(例如:/asset、/raw、/drawable)
if (is instanceof AssetManager.AssetInputStream) {
final long asset = ((AssetManager.AssetInputStream) is).getNativeAsset();
return nativeDecodeAsset(asset, outPadding, opts, Options.nativeInBitmap(opts), Options.nativeColorSpace(opts));
} else {
2.3.2 其他输入流(例如 FileInputStream)
return decodeStreamInternal(is, outPadding, opts);
}
}

-> 2.3.2
private static Bitmap decodeStreamInternal(InputStream is, Rect outPadding, Options opts) {
一块可复用的中间内存
byte [] tempStorage = null;
if (opts != null) tempStorage = opts.inTempStorage;
if (tempStorage == null) tempStorage = new byte[DECODE_BUFFER_SIZE];
return nativeDecodeStream(is, tempStorage, outPadding, opts,
Options.nativeInBitmap(opts), Options.nativeColorSpace(opts));
}

可以看到,在执行decodeStream()之前有两个比较重要的步骤:

  • 2.1 如果未设置 inDensity,则设置为 文件夹对应的 densityDpi
  • 2.2 如果未设置 inTargetDensity,则设置为 设备的 densityDpi

到了【2.3 执行解码】,根据是否为 AssetInputStream,调用不同的 native 方法:

  • 2.3.1 AssetManager 输入流(例如:/asset、/raw、/drawable),调用nativeDecodeAsset()
  • 2.3.2 其他输入流(例如 FileInputStream),调用nativeDecodeStream()

BitmapFactory.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
scss复制代码-> 2.3.1 AssetManager 输入流
static jobject nativeDecodeAsset(JNIEnv* env, jobject clazz, jlong native_asset,
jobject padding, jobject options) {

Asset* asset = reinterpret_cast<Asset*>(native_asset);
执行解码
return doDecode(env, skstd::make_unique<AssetStreamAdaptor>(asset), padding, options);
}

-> 2.3.2 其他输入流
static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage, jobject padding, jobject options) {

jobject bitmap = NULL;
std::unique_ptr<SkStream> stream(CreateJavaInputStreamAdaptor(env, is, storage));

if (stream.get()) {
std::unique_ptr<SkStreamRewindable> bufferedStream(
SkFrontBufferedStream::Make(std::move(stream), SkCodec::MinBufferedBytesNeeded()));
执行解码
bitmap = doDecode(env, std::move(bufferedStream), padding, options);
}
return bitmap;
}

最终都走到doDecode(),这段代码是图片解码的核心逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
ini复制代码已简化
static jobject doDecode(JNIEnv* env, std::unique_ptr<SkStreamRewindable> stream, jobject padding, jobject options) {

1、获取 java 层 Options 对象 In 字段值
int sampleSize; 对应于 Options#inSampleSize(默认 1)
if (sampleSize <= 0) {
sampleSize = 1;
}
bool onlyDecodeSize; 对应于 Options#inJustDecodeBounds(默认 false)
bool isHardware; 对应于 Options#inPreferredConfig(默认 ARGB_8888)
bool isMutable; 对应于 Options#inMutable(默认 false)
jobject javaBitmap; 对应于 Options#inBitmap(默认 null)
boolean inScale; 对应于 Options#inScaled(默认 true)
int density; 对应于 Options#inDensity
int targetDensity; 对应于 Options#inTargetDensity

2、设置 java 层 Options 对象 out 字段初始值
Options#outWidth = -1
Options#outHeight = -1
Options#outMimeType = 0
Options#outConfig = 0
Options#outColorSpace = 0

3、获得 inDensity / inTargetDensity
float scale = 1.0f;
if (density != 0 && targetDensity != 0 && density != screenDensity) {
scale = (float) targetDensity / density;
}

mutable 和 hardware 是冲突的
if (isMutable && isHardware) {
doThrowIAE(env, "Bitmaps with Config.HARWARE are always immutable");
return nullObjectReturn("Cannot create mutable hardware bitmap");
}

4、根据 sampleSize 确定采样后的尺寸(size)
SkISize size = codec->getSampledDimensions(sampleSize);
int scaledWidth = size.width();
int scaledHeight = size.height();

5、确定 java 层 Options 对象 out 字段最终值
Options#outWidth = scaledWidth
Options#outHeight = scaledHeight
Options#outMimeType = (例如 "image/png")
Options#outConfig = (例如 ARGB_8888)
Options#outColorSpace =(例如 RGB)

6、返回点一:inJustDecodeBounds = true,只获取采样后的尺寸(无缩放)
if (onlyDecodeSize) {
return nullptr;
}

7、确定最终缩放到的目标尺寸(先采样,再缩放)
bool willScale = false;
if (scale != 1.0f) {
willScale = true;
scaledWidth = static_cast<int>(scaledWidth * scale + 0.5f);
scaledHeight = static_cast<int>(scaledHeight * scale + 0.5f);
}

8、存在 java 层的 Options#inBitmap,做一些准备工作
android::Bitmap* reuseBitmap = nullptr;
if (javaBitmap != NULL) {
reuseBitmap = &bitmap::toBitmap(env, javaBitmap);
}
RecyclingPixelAllocator recyclingAllocator(reuseBitmap, existingBufferSize);

9、采样解码得到 SkBitmap(注意:只使用了采用后尺寸)
const SkImageInfo decodeInfo = SkImageInfo::Make(size.width(), size.height(), decodeColorType, alphaType, decodeColorSpace);
SkBitmap decodingBitmap;
decodingBitmap.setInfo(bitmapInfo);
decodingBitmap.tryAllocPixels(decodeAllocator)

10、执行缩放
SkBitmap outputBitmap;
if (willScale) {

10.1 根据是否有可回收的 inBitmap,确定不同的分配器
SkBitmap::Allocator* outputAllocator;
if (javaBitmap != nullptr) {
outputAllocator = &recyclingAllocator;
} else {
outputAllocator = &defaultAllocator;
}

10.2 复制(注意:使用了缩放后尺寸)
SkColorType scaledColorType = decodingBitmap.colorType();
outputBitmap.setInfo(bitmapInfo.makeWH(scaledWidth, scaledHeight).makeColorType(scaledColorType));
outputBitmap.tryAllocPixels(outputAllocator)

10.3 利用 Canvas 进行缩放
const float scaleX = scaledWidth / float(decodingBitmap.width());
const float scaleY = scaledHeight / float(decodingBitmap.height());

SkCanvas canvas(outputBitmap, SkCanvas::ColorBehavior::kLegacy);
canvas.scale(scaleX, scaleY);
canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint);
} else {
outputBitmap.swap(decodingBitmap);
}

11、返回点二:返回 java 层的 Options#inBitmap
if (javaBitmap != nullptr) {
return javaBitmap
}

12、返回点三:硬件位图(from Android 8)
if (isHardware) {
sk_sp<Bitmap> hardwareBitmap = Bitmap::allocateHardwareBitmap(outputBitmap);
return bitmap::createBitmap(env, hardwareBitmap.release(), bitmapCreateFlags,
ninePatchChunk, ninePatchInsets, -1);
}

13、返回点四:一般方式
return bitmap::createBitmap(env, defaultAllocator.getStorageObjAndReset(),
bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);
}

提示: 整段源码非常长,阅读的过程是比较痛苦。好处是最终通过源码也发现了不少错误 / 片面的认识,也是收获颇丰。贴心的我当然都帮你整理好了,如果能帮上忙,请务必点赞加关注 ,这真的对我非常重要。


参考资料

  • 《Android 开发绕不过的坑:你的 Bitmap 究竟占多大内存?》 —— 霍丙乾 著
  • 《Android 开发高手课·内存优化(上/下)》 —— 张绍文 讲,极客时间 出品
  • 《Android内存优化杂谈》 —— 张绍文 著

推荐阅读

  • 密码学 | Base64是加密算法吗?
  • 操作系统 | 中断 & 系统调用浅析
  • 算法面试题 | 回溯算法解题框架
  • 算法面试题 | 链表问题总结
  • 计算机网络 | 图解 DNS & HTTPDNS 原理
  • 逻辑 | “我知道你不知道”!精选十道逻辑类面试题(一)
  • Android | 说说从 android:text 到 TextView 的过程
  • Android | 食之无味!App Startup 可能比你想象中要简单
  • Android | 适可而止!看Glide如何把生命周期安排得明明白白

创作不易,你的「三连」是丑丑最大的动力,我们下次见!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Android 老生常谈!屏幕适配原理 & 方案总结笔记

发表于 2020-12-07

点赞关注,不再迷路,你的支持对我意义重大!

🔥 Hi,我是丑丑。本文 GitHub · Android-NoteBook 已收录,这里有 Android 进阶成长路线笔记 & 博客,欢迎跟着彭丑丑一起成长。(联系方式在 GitHub)

前言

  • Android 屏幕适配 是一个老生常谈的问题,已经被讲烂了。这个问题虽然总体上复杂性不高,但是涉及的概念比较多,容易混淆;
  • 在这篇文章里,我将总结 屏幕适配原理 & 方案(内容上没什么新意,可能主要是自己看)。如果能帮上忙,请务必点赞加关注,这真的对我非常重要。

相关文章

  • 《Android | 老生常谈!屏幕适配原理 & 方案总结笔记》
  • 《Android | 毫分缕析!说说图片加载的整个过程》

目录


  1. 屏幕的相关概念

提示: 网上大量文章存在概念混淆,阅读起来相当痛苦!希望丑丑的总结能更加严谨。

  • 硬件 / 物理参数:

  • 软件概念:

提示: ppi 和 dpi 容易混淆,前者是一个硬件物理参数,而后者是一个软件概念。最大的区别是 dpi 可以人为修正,例如 ppi 为 430、450 的手机,dpi 可能会被指定为 480。

  • 密度限定符:


  1. 为什么要进行屏幕适配?

  • 原因 1:设备碎片化

屏幕适配的问题根源是设备碎片化(由于 Android 系统天生的开放性),主要表现为:系统碎片化 & 屏幕尺寸碎片化 & 屏幕像素密度碎片化。

引用自 blog.csdn.net/jiashuai94/… —— jiashuai94 著

  • 原因 2:屏幕宽 dp 与 设计图宽 dp不一致

UI 设计图一般是按照360dp为宽度来设计,但是实际屏幕的宽度不一定是360dp。此时,同样是 360 dp 的尺寸在这台手机上是整个宽度的大小,在另一台手机上却不是。


  1. 今日头条适配方案

3.1 方案要点

  • 1、选定以宽或高维度适配(多数选择宽)
  • 2、修改 DisplayMetrics#density
  • 3、修改 DisplayMetrics#scaledDensity
  • 4、监听系统设置中字体大小修改

参考资料《一种极低成本的 Android 屏幕适配方式》 —— 字节跳动技术团队 著

3.2 评估

  • 优点:侵入性极低,效果稳定

  1. 宽高限定符适配方案

4.1 方案要点

  • 1、为每种需要适配的分辨率提供一个文件夹
1
2
3
4
5
6
perl复制代码res
├── values
├── values-480x320
├── ...
├── values-1920x1080
├── values-2560x1440
  • 2、选定一个基准的分辨率

举个例子,选定以480 x 320为基准分辨率,将宽和高进行整份,得到以下尺寸:

1
2
3
4
5
6
7
8
9
ini复制代码<dimen name="x1">1px</dimen>
<dimen name="x2">2px</dimen>
...
<dimen name="x320 ">320 px</dimen>

<dimen name="y1">1px</dimen>
<dimen name="y2">2px</dimen>
...
<dimen name="y480 ">480px</dimen>

注意: 单位是 px

  • 3、按比例计算其他分辨率下的尺寸

以480 x 320为基准分辨率,在不同分辨率的文件夹下按比例缩放尺寸,例如800 x 480:

1
2
3
4
ini复制代码<dimen name="x1">1.5px</dimen> 480/320 = 1.5
<dimen name="x2">3px</dimen> 1.5*2 = 3
...
<dimen name="x320 ">480 px</dimen> 1.5*320 = 480

4.2 评估

  • 致命缺点:容错率低,必须精准命中才能适配
  • 风险点:增大包体积

  1. smallestWidth 限定符适配方案

提示: 最小宽度值得是屏幕的两条边最小的一个,而不是指感官意义上的宽。

5.1 方案要点

  • 1、为每种需要适配的最小宽度提供一个文件夹
1
2
3
4
5
6
7
perl复制代码res
├──values
├──values-sw320dp
├──values-sw360dp
├──values-sw400dp
├──values-sw411dp
├──values-sw480dp
  • 2、选定一个基准的最小宽度

举个例子,选定为360dp为最小宽度,将宽整份为 360 份,得到以下尺寸:

1
2
3
4
ini复制代码<dimen name="x1">1dp</dimen>
<dimen name="x2">2dp</dimen>
...
<dimen name="dp_360 ">360dp</dimen>

注意: 单位是 dp

  • 3、按比例计算其他最小宽度下的尺寸

以360dp为最小宽度,在不同最小宽度的文件夹下按比例缩放尺寸,例如sw480dp:

1
2
3
4
ini复制代码<dimen name="x1">1.333dp</dimen> 480/360
<dimen name="x2">2.666dp</dimen>
...
<dimen name="dp_360 ">480dp</dimen>

5.2 评估

  • 风险点:增大包体积
  • 不足:不能自动支持横竖屏切换时的适配
  • 不足:sp 也需要增加一份尺寸

  1. 总结

每种方案都不是完美的,应该注意自己的侧重点,以确定优缺点中那些是可以妥协的,哪些是不能接受的,才能选出最合适的方案。


参考资料

  • 《设备兼容性》 —— Android Developers
  • 《Android 开发高手课 · UI 优化(上)》 —— 张绍文 著,极客时间 出品
  • 《Android 屏幕适配方案》 —— Carson_Ho
  • 《Android 目前稳定高效的 UI 适配方案》 —— 拉丁吴 著
  • 《骚年你的屏幕适配方式该升级了! smallestWidth 限定符适配方案》 —— JessYan 著
  • 《一种极低成本的 Android 屏幕适配方式》 —— 字节跳动技术团队 著

推荐阅读

  • 密码学 | Base64是加密算法吗?
  • 操作系统 | 中断 & 系统调用浅析
  • 算法面试题 | 回溯算法解题框架
  • 算法面试题 | 链表问题总结
  • 计算机网络 | 图解 DNS & HTTPDNS 原理
  • 逻辑 | “我知道你不知道”!精选十道逻辑类面试题(一)
  • Android | 说说从 android:text 到 TextView 的过程
  • Android | 食之无味!App Startup 可能比你想象中要简单
  • Android | 适可而止!看Glide如何把生命周期安排得明明白白

感谢喜欢!你的点赞是对我最大的鼓励!欢迎关注彭旭锐的GitHub!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

必看!PostgreSQL参数优化

发表于 2020-12-07

前不久,一个朋友所在的公司,业务人员整天都喊慢。

朋友是搞开发的,不是很懂DB,他说他们应用的其实没什么问题,但是就是每天一到高峰期就办理特别的慢啊,各种堵塞,一堆请求无法完成。他们没有专门的DBA,想找我帮忙看看。

我下班后打开他们的数据库看了几眼,让我大跌眼睛,数据库全部都是默认配置的参数。然后就给他们随便修改了一通,告诉他们重启了一下数据库。到了第二天他反映,大哥,你也太神了,昨天做了什么操作,我们的数据库就突然就变快了。

我说:啥也没做啊,就是根据经验值把默认参数调了一下,草率的很! 你们参数都是开箱即用的默认值。当做生产使用肯定不行。

PostgreSQL应该调整的参数

max_connections

允许的最大客户端连接数。这个参数设置大小和work_mem有一些关系。配置的越高,可能会占用系统更多的内存。通常可以设置数百个连接,如果要使用上千个连接,建议配置连接池来减少开销。

shared_buffers

PostgreSQL使用自己的缓冲区,也使用Linux操作系统内核缓冲OS Cache。这就说明数据两次存储在内存中,首先是PostgreSQL缓冲区,然后是操作系统内核缓冲区。与其他数据库不同,PostgreSQL不提供直接IO,所以这又被称为双缓冲。PostgreSQL缓冲区称为shared_buffer,建议设置为物理内存的1/4。而实际配置取决于硬件配置和工作负载,如果你的内存很大,而你又想多缓冲一些数据到内存中,可以继续调大shared_buffer。

Effective_cache_size

这个参数主要用于Postgre查询优化器。是单个查询可用的磁盘高速缓存的有效大小的一个假设,是一个估算值,它并不占据系统内存。由于优化器需要进行估算成本,较高的值更有可能使用索引扫描,较低的值则有可能使用顺序扫描。一般这个值设置为内存的1/2是正常保守的设置,设置为内存的3/4是比较推荐的值。通过free命令查看操作系统的统计信息,您可能会更好的估算该值。

1
2
3
4
shell复制代码[pg@e22 ~]$ free -g
total used free shared buff/cache available
Mem: 62 2 5 16 55 40
Swap: 7 0 7

work_mem

这个参数主要用于写入临时文件之前内部排序操作和散列表使用的内存量,增加work_mem参数将使PostgreSQL可以进行更大的内存排序。这个参数和max_connections有一些关系,假设你设置为30MB,则40个用户同时执行查询排序,很快就会使用1.2GB的实际内存。同时对于复杂查询,可能会运行多个排序和散列操作,例如涉及到8张表进行合并排序,此时就需要8倍的work_mem。

如下面案例所示,该环境使用4MB的work_mem,在执行排序操作的时候,使用的Sort Method是external merge Disk。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
sql复制代码kms=> explain (analyze,buffers) select * from KMS_BUSINESS_HALL_TOTAL  order by buss_query_info;
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------------------------
Gather Merge (cost=262167.99..567195.15 rows=2614336 width=52) (actual time=2782.203..5184.442 rows=3137204 loops=1)
Workers Planned: 2
Workers Launched: 2
Buffers: shared hit=68 read=25939, temp read=28863 written=28947
-> Sort (cost=261167.97..264435.89 rows=1307168 width=52) (actual time=2760.566..3453.783 rows=1045735 loops=3)
Sort Key: buss_query_info
Sort Method: external merge Disk: 50568kB
Worker 0: Sort Method: external merge Disk: 50840kB
Worker 1: Sort Method: external merge Disk: 49944kB
Buffers: shared hit=68 read=25939, temp read=28863 written=28947
-> Parallel Seq Scan on kms_business_hall_total (cost=0.00..39010.68 rows=1307168 width=52) (actual time=0.547..259.524 rows=1045735 loops=3)
Buffers: shared read=25939
Planning Time: 0.540 ms
Execution Time: 5461.516 ms
(14 rows)

当我们把参数修改成512MB的时候,可以看到Sort Method变成了quicksort Memory,变成了内存排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sql复制代码kms=> set work_mem to "512MB";
SET
kms=> explain (analyze,buffers) select * from KMS_BUSINESS_HALL_TOTAL order by buss_query_info;
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------
Sort (cost=395831.79..403674.80 rows=3137204 width=52) (actual time=7870.826..8204.794 rows=3137204 loops=1)
Sort Key: buss_query_info
Sort Method: quicksort Memory: 359833kB
Buffers: shared hit=25939
-> Seq Scan on kms_business_hall_total (cost=0.00..57311.04 rows=3137204 width=52) (actual time=0.019..373.067 rows=3137204 loops=1)
Buffers: shared hit=25939
Planning Time: 0.081 ms
Execution Time: 8419.994 ms
(8 rows)

maintenance_work_mem

指定维护操作使用的最大内存量,例如(Vacuum、Create Index和Alter Table Add Foreign Key),默认值是64MB。由于通常正常运行的数据库中不会有大量并发的此类操作,可以设置的较大一些,提高清理和创建索引外键的速度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sql复制代码postgres=# set maintenance_work_mem to "64MB";
SET
Time: 1.971 ms
postgres=# create index idx1_test on test(id);
CREATE INDEX
Time: 7483.621 ms (00:07.484)
postgres=# set maintenance_work_mem to "2GB";
SET
Time: 0.543 ms
postgres=# drop index idx1_test;
DROP INDEX
Time: 133.984 ms
postgres=# create index idx1_test on test(id);
CREATE INDEX
Time: 5661.018 ms (00:05.661)

可以看到在使用默认的64MB创建索引,速度为7.4秒,而设置为2GB后,创建速度是5.6秒

wal_sync_method

每次发生事务后,PostgreSQL会强制将提交写到WAL日志的方式。可以使用pg_test_fsync命令在你的操作系统上进行测试,fdatasync是Linux上的默认方法。如下所示,我的环境测试下来fdatasync还是速度可以的。不支持的方法像fsync_writethrough直接显示n/a。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
shell复制代码postgres=# show wal_sync_method ;
wal_sync_method
-----------------
fdatasync
(1 row)

[pg@e22 ~]$ pg_test_fsync -s 3
3 seconds per test
O_DIRECT supported on this platform for open_datasync and open_sync.

Compare file sync methods using one 8kB write:
(in wal_sync_method preference order, except fdatasync is Linux's default)
open_datasync 4782.871 ops/sec 209 usecs/op
fdatasync 4935.556 ops/sec 203 usecs/op
fsync 3781.254 ops/sec 264 usecs/op
fsync_writethrough n/a
open_sync 3850.219 ops/sec 260 usecs/op

Compare file sync methods using two 8kB writes:
(in wal_sync_method preference order, except fdatasync is Linux's default)
open_datasync 2469.646 ops/sec 405 usecs/op
fdatasync 4412.266 ops/sec 227 usecs/op
fsync 3432.794 ops/sec 291 usecs/op
fsync_writethrough n/a
open_sync 1929.221 ops/sec 518 usecs/op

Compare open_sync with different write sizes:
(This is designed to compare the cost of writing 16kB in different write
open_sync sizes.)
1 * 16kB open_sync write 3159.780 ops/sec 316 usecs/op
2 * 8kB open_sync writes 1944.723 ops/sec 514 usecs/op
4 * 4kB open_sync writes 993.173 ops/sec 1007 usecs/op
8 * 2kB open_sync writes 493.396 ops/sec 2027 usecs/op
16 * 1kB open_sync writes 249.762 ops/sec 4004 usecs/op

Test if fsync on non-write file descriptor is honored:
(If the times are similar, fsync() can sync data written on a different
descriptor.)
write, fsync, close 3719.973 ops/sec 269 usecs/op
write, close, fsync 3651.820 ops/sec 274 usecs/op

Non-sync'ed 8kB writes:
write 400577.329 ops/sec 2 usecs/op

wal_buffers

事务日志缓冲区的大小,PostgreSQL将WAL记录写入缓冲区,然后再将缓冲区刷新到磁盘。在PostgreSQL 12版中,默认值为-1,也就是选择等于shared_buffers的1/32 。如果自动的选择太大或太小可以手工设置该值。一般考虑设置为16MB。

synchronous_commit

客户端执行提交,并且等待WAL写入磁盘之后,然后再将成功状态返回给客户端。可以设置为on,remote_apply,remote_write,local,off等值。默认设置为on。如果设置为off,会关闭sync_commit,客户端提交之后就立马返回,不用等记录刷新到磁盘。此时如果PostgreSQL实例崩溃,则最后几个异步提交将会丢失。

default_statistics_target

PostgreSQL使用统计信息来生成执行计划。统计信息可以通过手动Analyze命令或者是autovacuum进程启动的自动分析来收集,default_statistics_target参数指定在收集和记录这些统计信息时的详细程度。默认值为100对于大多数工作负载是比较合理的,对于非常简单的查询,较小的值可能会有用,而对于复杂的查询(尤其是针对大型表的查询),较大的值可能会更好。为了不要一刀切,可以使用ALTER TABLE .. ALTER COLUMN .. SET STATISTICS覆盖特定表列的默认收集统计信息的详细程度。

checkpoint_timeout、max_wal_size,min_wal_size、checkpoint_completion_target

了解这两个参数以前,首先我们来看一下,触发检查点的几个操作。

  • 直接执行checkpoint命令
  • 执行需要检查点的命令(例如pg_start_backup,Create database,pg_ctl stop/start等等)
  • 自上一个检查点以来,达到了已经配置的时间量(checkpoint_timeout )
  • 自上一个检查点以来生成的WAL数量(max_wal_size)

使用默认值,检查点将在checkpoint_timeout=5min。也就是每5分钟触发一次。而max_wal_size设置是自动检查点之间增长的最大预写日志记录(WAL)量。默认是1GB,如果超过了1GB,则会发生检查点。这是一个软限制。在一个特殊的情况下,比如系统遭遇到短时间的高负载,日志产生几秒种就可以达到1GB,这个速度已经明显超过了checkpoint_timeout ,pg_wal目录的大小会急剧增加。此时我们可以从日志中看到相关类似的警告。

1
2
3
4
sql复制代码LOG:  checkpoints are occurring too frequently (9 seconds apart)
HINT: Consider increasing the configuration parameter "max_wal_size".
LOG: checkpoints are occurring too frequently (2 seconds apart)
HINT: Consider increasing the configuration parameter "max_wal_size".

所以要合理配置max_wal_size,以避免频繁的进行检查点。一般推荐设置为16GB以上,不过具体设置多大还需要和工作负荷相匹配。

min_wal_size参数是只要 WAL 磁盘使用量保持在这个设置之下,在做检查点时,旧的 WAL 文件总是被回收以便未来使用,而不是直接被删除。

而检查点的写入不是全部立马完成的,PostgreSQL会将一次检查点的所有操作分散到一段时间内。这段时间由参数checkpoint_completion_target控制,它是一个分数,默认为0.5。也就是在两次检查点之间的0.5比例完成写盘操作。如果设置的很小,则检查点进程就会更加迅速的写盘,设置的很大,则就会比较慢。一般推荐设置为0.9,让检查点的写入分散一点。但是缺点就是出现故障的时候,影响恢复的时间。

使用PGTune工具来配置参数

对于朋友这样的公司,没有DBA专业人士,我一般会建议他们使用PGTune来配置参数,这款工具是在线软件,链接。设置很简单,你只需要知道你的数据库版本,操作系统类型,内存数量,CPU数量,磁盘类型,连接数,还有应用的类型。就可以轻轻松松得到一些建议的参数值。

使用postgresqltuner来优化参数

当然我们还可以使用postgresqltuner工具来优化参数,作者说受到了mysqltuner的启发,它是perl脚本写的。

这个软件使用起来也很简单,直接下载解压,执行脚本就行了。

1
sql复制代码postgresqltuner.pl --host=dbhost --database=testdb --user=username --password=qwerty

执行输出结果如下:

这个软件要比PGTune要专业一些,它还输出了一些操作系统的配置,同时它根据数据库当前的负载来判断内存参数是否合理,类似于Advisor。

总结

最后来做个ending吧,系统默认的配置只适合自己玩玩,并不适合开箱即用。还是需要根据DBA的专业经验来进行相关参数的配置,如果没有专业的DBA童鞋,也可以使用PGTune或者是postgresqltuner脚本来进行一些优化,做完这些优化,系统性能将会大幅提升。

参考文档

Tuning Your PostgreSQL Server

Tuning PostgreSQL Database Parameters to Optimize Performance

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

图文带你了解volatile底层和内存屏障之间的关系

发表于 2020-12-07

    欢迎大家搜索“小猴子的技术笔记”关注我的公众号,有问题可以及时和我交流。

    为了保证内存的可见性,Java编译器会在生成指令序列的适当位置插入内存屏障指令来禁止特定类型的处理器重排序。JMM被内存屏障指令分为了4类(Load表示读,store表示写):

    LoadLoad Barriers:在两个读指令之间插入一个“LoadLoad”的内存屏障,确保Load1的数据装载,先于Load2的数据装载。

在这里插入图片描述

    StoreStore Barriers:在两个写指令之间插入一个“StoreStore”的内存屏障。确保Store1的数据先刷新到主内存,并且对其数据可见。Store1的写数据先于Store2的写数据。
在这里插入图片描述

    LoadStore Barriers:在读和写指令之间加一个“LoadStore”屏障,确保Load1的数据装载先于Store2的写数据。

在这里插入图片描述

    StoreLoad Barriers:在写和读之间加一个“StoreLoad”屏障,确保Store1的数据写入并且刷新到内存先于Load2。“StoreLoad”会使该屏障之前所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令。执行“StoreLoad”屏障的开销比较昂贵,因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中(不了解写缓冲区概念的小伙伴,可以查看我上篇文章《为什么会有重排序?它对线程有什么影响?》)。

在这里插入图片描述

    如果你简历上写的有多线程的知识的话,那么面试官很大几率会问你volatile这个关键字的问题。也许你会说出,volatile是解决了内存可见性问题和禁止重排序的作用。那么你知道它底层是怎么解决的吗?

    为了实现volatile的内存语义,编译器在生成字节码的时候,JMM采取保守策略会向指令序列中插入内存屏障来禁止特定类型的处理器重排序。

    1.在每个volatile写操作前面插入一个StoreStore屏障。

    2.在每个volatile写操作后面插入一个StoreLoad屏障。

    3.在每个volatile读操作后面插入一个LoadLoad屏障。

    4.在每个volatile读操作后面插入一个LoadStore屏障。

    下图将对保守策略的内存屏障做一个关系的解读:
在这里插入图片描述

    注意:上述的volatile写和volatile的读的内存屏障插入策略非常保守。其实在实际执行时,只要不改变volatile写-读的内存语义,编译器就可以根据具体情况省略不必要的屏障。比如下面的这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ini复制代码public class VolatileBarriersExample {
int a;
volatile int v1 = 1;
volatile int v2 = 2;
void readAndWrite() {
// 第一个volatile读
int i = v1;
// 第二个volatile读
int j = v2;
// 普通写
a = i + j;
// 第一个volatile写
v1 = i + 1;
// 第二个volatile写
v2 = j * 2;
}
}

在这里插入图片描述

    注意,最后的StoreLoad屏障不能省略,因为第二个volatile写之后,方法立即返回。此时编译器无法准确判断后面是否会有volatile读或写。为了安全起见,编译器通常会在这里插入一个StoreLoad屏障。其实,volatile禁止指令重排序就是使用了内存屏障作为保证来实现的。

    了解volatile的底层内存屏障的实现之后,我们来看一下对一个volatile变量的读写时,该共享变量所在的本地内存和主内存的变化(也就是内存可见性的问题):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
csharp复制代码public class VolatileExample {
int a = 0;
volatile boolean flag = false;
public void writer() {
// 第一步
a = 1;
// 第二步
flag = true;
}
public void reader() {
// 第三步
if (flag) {
// 第四步
int i = a;
System.out.println(i);
}
}
}

    假设线程A首先执行了writer()方法,随后线程B执行reader()方法,那么A线程执行之后的共享变量的状态示意图如下:

在这里插入图片描述

    结论:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
在这里插入图片描述

    结论:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

    由此可以得出结论:volatile的内存可见性是如果一个线程修改了共享变量,那么该共享变量会立刻刷新到主存中。同时,会通知另外一个持有该共享变量的线程,告诉它这个共享变量已经修改了,不要再使用你工作内存中的变量值了,快去主内存中重新获取吧。

    总的来说:volatile使用了内存屏障来禁止指令的重排序,使用刷新主内存,通知其他线程工作内存中的共享变量失效,使其他线程强制去主内存获取最新的值来保证,被volatile修饰的变量的内存可见性。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

php 调试指南(Xdebug版)(续)

发表于 2020-12-07

欢迎转载,但请在开头或结尾注明原文出处【blog.chaosjohn.com】

前言

在前文 php 调试指南(Xdebug版) 开头,笔者吹了一句 ”吃透本文,没有人将比你更懂 Php Xdebug 调试“。没想到打脸来的如此之快,这才过了三四天,我发现今天的我比写前文时的我,更懂 了。

关于 PhpStorm 2020.3 和 Xdebug 3

在前文中,笔者用的还是 PhpStorm 2020.2.4,所以结合 Xdebug 3.0.0 在使用 Web Server Debug Validation 进行 调试环境验证 时,提示类似 Xdebug port is invalid 的报错,分析是 不兼容 3.0.0 版本 导致的。所以降级成了 Xdebug 2.9.8 才顺利写完了文章。

写完前文的第二天,PhpStorm 2020.3就发布了,新增

  • 对 PHP 8 的支持
  • 对 Xdebug 3 的支持

在重新用 PHPBrew 安装 PHP 8 + Xdebug 3.0.0 环境并且用 Nginx + php-fpm 部署起来后,Web Server Debug Validation 成功验证。

关于 Web Server Debug Validation 功能

笔者在前文写完后,在升级 PhpStorm 2020.3 之前,笔者重新安装了 PHP 7.4.13 + Xdebug 3.0.0 环境,在忽略 验证报错 的情况下直接开启调试,是完全没有任何问题的。所以,该功能只是 验证调试环境,在调试之前仅作参考,不影响实际调试。

关于 远程调试

笔者在写前文关于 远程调试 的几节时,参考了网上很多文章,看似吃透了,其实不然,原因有这样几点:

  • 网上很多文章的作者写文时也如同笔者写前文时一样,对原理一知半解,导致很多讲解其实不完善或者南辕北辙。正确的做法:研读 JetBrains / VSCode / Xdebug 的官方文档,去理解它的原理。
    • Configure Xdebug—PhpStorm PhpStorm官方:配置 Xdebug
    • Remote debugging via SSH tunnel—PhpStorm PhpStorm官方:通过SSH隧道进行远程调试
    • Multiuser debugging via Xdebug proxies—PhpStorm PhpStorm官方:通过 Xdebug 代理进行多人调试
    • PHP Debug Adapter for Visual Studio Code VSCode PHP Debug 插件主页文档
    • Step Debugging Xdeubg官方:单步调试
    • Documentation - all settings Xdebug官方:所有配置项
    • Upgrading from Xdebug 2 to 3 Xdebug官方:从Xdebug 2 升级到 Xdebug 3
  • 笔者写前文时搭建的 远程调试环境 不完善
    • 本机 和 远程服务器 都是 macOS
    • 本机 和 远程服务器 都在 局域网内

IDE 结合 Xdebug 调试的原理

  • IDE(本身或利用插件) 打开本地的 9000 端口并进行监听(Xdebug 2.X 默认为 9000,Xdebug 3.X 默认为 9003,但均可修改)
  • IDE(本身或利用插件) 做好路径映射(path mapping),即本地在IDE中打开的项目目录与远程服务器上的项目目录做一一映射,例如
    • macOS 本地:/Users/chaos/Work/php/demos/debug/
    • Linux 服务器:/home/chaos/Work/php/demos/debug/
  • 本地向服务器发送请求时带上 Cookie: XDEBUG_SESSION=IDEKEY
  • 服务器接受到请求时,经历了 Nginx -> php-fpm 后到达 Xdebug,Xdebug 检测到 XDEBUG_SESSION 的 cookie,认为这条请求是带着 调试目的 来的,同时挂起 PHP解释器 进一步处理请求
  • 然后 Xdebug 从 php.ini 中获取目标地址或从 $_SERVER 里获取到请求的来源地址(比如 223.104.148.182)作为目标地址,然后就向目标地址的 9000 端口发起建立 调试连接
  • 本地的 IDE(本身或利用插件) 发现监听的 9000 端口有 调试连接 建立,判断一下 XDEBUG_SESSION 是否为自己预设的 IDE key
    • 如果不是预设 IDE key,通过 9000 端口上的 调试连接 告诉服务器的 Xdebug,“不归我管,我不处理”,然后双方协商一下断连接
    • 如果是预设 IDE key,同时发现本地打了断点或者本地设置了 "stopOnEntry": true (VSCode) / Break at first line in PHP scripts (PhpStorm),则告诉服务器的 Xdebug,“收到,调试准备就绪”,然后双方协商一下进入调试状态

真实环境下 远程调试 的 陷阱

聪明的小伙伴可能已经发现上述 调试流程 里存在的一个 陷阱:即服务器要向本地请求建立 调试连接,但问题是,现在所有的 家用/企业 网络环境下,所谓的 本地 都在上级路由器的 NAT 下,根本就没有暴露在公网的 IP地址,所以 本地 的 9000 端口对于服务器来说,是不可达的,想要访问,做梦!

填坑 陷阱

笔者重新模拟了真实的调试环境,即 本地 为 macOS,远程服务器 选了两台 Linux,一台是笔者在公司搭建的 物理机,另一台是公司购买的阿里云 云主机。

填坑的最终目的,是要使得 本地 的 9000 端口暴露给服务器,让其直接可达。

这里分:

  1. 本地和远程服务器同处一个局域网内,例如,都加入同一个 VPN 网络,本地通过 VPN 分配给服务器的 私网IP 访问服务器,服务器的 Xdebug 解析到的来源地址则也是通过 VPN 分配给本地的 私网IP,直接可达。
  2. 路由器本身从 ISP(宽带运营商) 通过 PPPoE 拨号 获取到了 公网IP,然后路由器上通过 端口映射 或 DMZ 模式,将本地的 9000 端口,映射到路由器的 9000 端口,这样服务器也可通过 公网IP:9000 访问到本地的 9000 端口。(该方式最推荐,但是在国内可行度不高,因为国内 IP地址池 即将枯竭,所以很难从运营商处申请到 公网IP)
  3. 其他环境只能借助 移花接木大法:借助 SSH 反向隧道,在本地和服务器之间建立一条 TCP通道,将本地的 9000 端口映射到服务器的 9000 端口。这样的话,服务器上的 Xdebug 访问 localhost:9000 就等于访问到了 IDE本地 的 9000 端口。(借用 JetBrains 官方文档里的一副插图)
    JetBrains 官方文档关于 SSH 隧道的插图

在这里,笔者将前两种环境归纳为 回程网络直接可达,否则则为 回程网络不可直达。

PhpStorm 的特殊配置

在分别罗列远程调试的具体参数配置之前,笔者还得额外将 PhpStorm 的特殊配置单独拎出来阐述一下。

PhpStorm 调试的 目标服务器,以 Server 的存在进行配置,具体位于 偏好设置 的 Languages and Frameworks | PHP | Servers 下。详见 JetBrains 官方文档,原文是这样的:

On this page, configure HTTP access for debugging engines to interact with local and remote Web servers and set correspondence between files on the server and their local copies in the PhpStorm project.

在本页,为调试配置 HTTP 访问,用以在 本地 和 远程 Web 服务器 之间交互,以及为 远程服务器的文件 与其在 PhpStorm 工程 中打开的 本地拷贝 设置关联。

PhpStorm 调试的特殊配置:在远程服务器上的 php-fpm 配置文件里添加这两行:

1
2
ini复制代码clear_env = no
env["PHP_IDE_CONFIG"] = "serverName=UbuntuServer"

这里 UbuntuServer 是自定义的服务器名称,自行更改。

然后在 PhpStorm 偏好设置的 Languages and Frameworks | PHP | Servers 添加一个 Server

  • name 填 UbuntuServer
  • Host 和 Port 笔者亲测可填任意合法值,PhpStorm 不校验,所以笔者都填写了 0 值
  • Debugger 选择 Xdebug
  • 勾选上 Use path mappings,并且设置好 本地文件目录路径 和 服务器文件目录路径 的映射(比如笔者本地的 /Users/chaos/Work/php/demos/debug/ 与服务器的 /home/chaos/Work/php/demos/debug/)

PhpStorm 里配置 Server

VSCode 的配置

只需要比本地调试多配置一个 路径映射,即 pathMappings 键值对,附上 launch.json 文件内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
bash复制代码{
"version": "0.2.0",
"configurations": [
{
"name": "Listen for XDebug",
"type": "php",
"request": "launch",
"port": 9000,
// "stopOnEntry": true,
"pathMappings": {
"/home/chaos/Work/php/demos/debug/": "${workspaceRoot}/",
}
},
{
"name": "Launch currently open script",
"type": "php",
"request": "launch",
"program": "${file}",
"cwd": "${fileDirname}",
"port": 9000
}
]
}

这里的 "/home/chaos/Work/php/demos/debug/": "${workspaceRoot}/" 是 "/home/chaos/Work/php/demos/debug/": "/Users/chaos/Work/php/demos/debug/" 的简化形式,${workspaceRoot} 则为 VSCode 中打开的 项目根目录。

服务器的 php.ini 在不同情况下的配置

情况一:回程网络直接可达

  • Xdebug 2.X
1
2
3
ini复制代码[xdebug]
xdebug.remote_enable=1
xdebug.remote_connect_back=1

| remote_enable=1 表示开启远程调试

| remote_connect_back=1 表示获取请求发起地址(从 $_SERVER['HTTP_X_FORWARDED_FOR'] 和 $_SERVER['REMOTE_ADDR'] 中获取),反向访问发起地址的 9000 端口建立调试连接

  • Xdebug 3.X
1
2
3
4
ini复制代码[xdebug]
xdebug.mode=debug
xdebug.discover_client_host=true
xdebug.port=9000

| mode=debug 取代了 2.X 版本的 remote_enable=1

| discover_client_host=true 取代了 2.X 版本的 xdebug.remote_connect_back=1

| port=9000 是个人喜好,在 3.X 中默认为 9003。笔者倾向于保持与 2.X 一致,这样同一份 IDE 配置可以同时调试 2.X 版本和 3.X 版本

情况二:回程网络不可直达

先用 SSH 隧道 反向将本地的 9000 端口映射到服务器的 9000 端口上

1
sql复制代码ssh -g -N -R 9000:127.0.0.1:9000 user@server
  • Xdebug 2.X (Method A)
1
2
3
ini复制代码[xdebug]
xdebug.remote_enable=1
xdebug.remote_host=127.0.0.1

| remote_host=127.0.0.1 表示直接向 127.0.0.1 这个地址请求建立调试连接

  • Xdebug 2.X (Method B)
1
2
3
4
ini复制代码[xdebug]
xdebug.remote_enable=1
xdebug.remote_connect_back=1
xdebug.remote_addr_header="HTTP_X_XDEBUG_REMOTE_ADDR"

| remote_connect_back=1 表示获取请求发起地址,反向访问发起地址的 9000 端口建立调试连接

| xdebug.remote_addr_header="HTTP_X_XDEBUG_REMOTE_ADDR" 表示优先从 _SERVER[‘HTTP_X_XDEBUG_REMOTE_ADDR’] 获取发起地址,获取不到再去 \_SERVER['HTTP\_X\_FORWARDED\_FOR']和$\_SERVER['REMOTE\_ADDR'] 中寻找

| 调试请求类似于 $ curl server.com:8000 -b XDEBUG_SESSION=IDEKEY -H "X-Xdebug-Remote-Addr: 127.0.0.1"

| 注意:HTTP- 发送请求时不用添加,因为对于未自定义的 header,Nginx 会自动加上前缀 HTTP_,并全部大写,横线 转为 下划线,存入 $_SERVER 全局变量中

  • Xdebug 3.X (Method A)
1
2
3
4
ini复制代码[xdebug]
xdebug.mode=debug
xdebug.port=9000
xdebug.client_host=127.0.0.1

| client_host=127.0.0.1 表示直接告诉 Xdebug,发起地址就是 127.0.0.1,你往这个地址请求建立调试连接就可以了

  • Xdebug 3.X (Method B)
1
2
3
4
5
ini复制代码[xdebug]
xdebug.mode=debug
xdebug.port=9000
xdebug.discover_client_host=true
xdebug.client_discovery_header="HTTP_X_XDEBUG_REMOTE_ADDR"

| client_discovery_header="HTTP_X_XDEBUG_REMOTE_ADDR" 取代了 2.X 版本的 xdebug.remote_addr_header="HTTP_X_XDEBUG_REMOTE_ADDR"


其中 Xdebug 2.X (Method B) 与 Xdebug 3.X (Method B) 还有些许不同:

  • Xdebug 2.X (Method B) 如果按配置规则没找到请求发起地址,它不会降级使用 127.0.0.1/localhost 作为请求发起地址
  • Xdebug 3.X (Method B) 如果按配置规则没找到请求发起地址,它会降级使用 127.0.0.1/localhost 作为请求发起地址

多人调试 - DBGp

先附上参考文档

  • Multiuser debugging via Xdebug proxies PhpStorm官方:通过 DBGp 代理进行多人调试
  • DBGp Proxy Tool Xdebug官方

这块我不仅会略过,我还会狠狠的吐槽一下。先来看一下上述 “Multiuser debugging via Xdebug proxies” 这篇文里的一张插图
PhpStorm 提供的 DBGp 原理图

看似很美妙是不是,在笔者下载了 dbgpProxy 并且反复实验后,发现这个工具真的是神坑。

笔者在那台阿里云上运行 ./dbgpProxy -i 0.0.0.0:9001 -s 127.0.0.1:9000 后,从本地的 PhpStorm 带着自定义IDE key "PS" 发起调试请求,结果 dbgpProxy 日志打印 Connecting to 112.3.2.42:9000,而笔者所在的本地宽带并没有公网IP,112.3.2.42 这个IP是多层 NAT 之前的IP,肯定访问不进来,只能上 SSH 隧道方案。
阿里云运行 dbgpProxy

那这个 dbgpProxy 的意义何在呢?笔者在网上找到了 Xdebug 的作者 Derick Rethans(他同时设计了 DBGp 协议)的一篇文章 - Debugging with multiple users,在文中,他描绘了 DBDp 的使用场景:

Running a DBGp proxy also allows you to avoid NAT issues where (as seen from PHP+Xdebug on the server) all connections seem to come from the same IP (because your internal network is NATted). In this case, you can simple run the dbgp proxy on your NAT machine, configure xdebug.remote_host setting to the IP address of your NAT machine, and configure the IDEs to connect to the proxy running at :9001.

大致解释一下:

  • 你的路由器得从ISP处获取到公网IP
  • 在你的路由器上运行 dbgpProxy
  • 路由器下所有人的的 IDE 的 DBGp Proxy 配置 的 Host 填写路由器网关,即路由器内网地址

到这里,你可以发现,搭建 DBGp调试环境 非常苛刻,结合 公网IP 和 路由器运行 dbgpProxy 这两点看,只有 软路由 可以满足,因为 dbgpProxy 的二进制可执行文件只有 Windows / macOS / Linux x86-64 版本。所以大部分人/公司 可以洗洗睡了。

结语

为了写这两篇文,笔者是翻来覆去做了很多实验,还整理了很多知识点,打了很多草稿(真正意义上的纸质草稿),最后附上最满意的一张草稿。
笔者最满意的一张草稿

希望读者们能有所收获,感谢阅读!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

《终章》后端分布式多级缓存架构,也许你一直考虑的太简单了

发表于 2020-12-06

这篇想聊的话题是:分布式多级缓存架构的终章,如何解决大流量、高并发这样的业务场景,取决于你能不能成为这个领域金字塔上层的高手? 能不能把这个问题思考清楚决定了你的成长速度。

很多人在一个行业5年、10年,依然未达到这个行业的中层甚至还停留在底层,因为他们从来不关心这样的话题。作为砥砺前行的践行者,我觉得有必要给大家来分享一下。

file

开篇

服务端缓存是整个缓存体系中的重头戏,从开始的网站架构演进中,想必你已看到服务端缓存在系统性能的重要性。

但数据库确是整个系统中的“半吊子|慢性子”,有时数据库调优却能够以小搏大,在不改变架构和代码逻辑的前提下,缓存参数的调整往往是条捷径。

在系统开发的过程中,可直接在平台侧使用缓存框架,当缓存框架无法满足系统对性能的要求时,就需要在应用层自主开发应用级缓存。

缓存常用的就是Redis这东西,那到底什么是平台级、应用级缓存呢?

后面给大家揭晓。但有一点可表明,平台级就是你所选择什么开发语言来实现缓存,而应用级缓存,则是通过应用程序来达到目的。

01数据库缓存

为何说数据库是“慢性子”呢? 对现在喜欢快的你来说,慢是解决不了问题的。就好像总感觉感觉妹子回复慢

因为数据库属于IO密集型应用,主要负责数据的管理及存储。数据一多查询本身就有可能变慢, 这也是为啥数据上得了台面时,查询爱用索引提速的原因。当然数据库自身也有“缓存”来解决这个问题。

数据多了查询不应该都慢吗? 小白说吒吒辉你不懂额

。。。这个,你说的也不全是,还得分情况。例如:数据有上亿行

file

原因:

  1. 因为简单的SQL的结果不会特别多。你请求也不大,磁盘跟的上
  2. 并发总量超过磁盘吞吐上限,是谁都没招

就算你们不喜欢吒吒辉,我也要奋笔疾书

数据库缓存是自身一类特殊的缓存机制。大多数数据库不需要配置就可以快速运行,但并没有为特定的需求进行优化。在数据库调优的时候,缓存优化你可以考虑下。

以MySQL为例,MySQL中使用了查询缓冲机制,将SELECT语句和查询结果存放在缓冲区中,以键值对的形式存储。以后对于同样的SELECT语句,将直接从缓冲区中读取结果,以节省查询时间,提高了SQL查询的效率。

1.1.MySQL查询缓存

Query cache作用于整个MySQL实例,主要用于缓存MySQL中的ResultSet,也就是一条SQL语句执行的结果集,所以它只针对select语句。

当打开 Query Cache 功能,MySQL在接收到一条select语句的请求后,如果该语句满足Query Cache的条件,MySQL会直接根据预先设定好的HASH算法将接收到的select语句以字符串方式进行 hash,然后到Query Cache中直接查找是否已经缓存。

file

如果结果集已经在缓存中,该select请求就会直接将数据返回,从而省略后面所有的步骤(如SQL语句的解析,优化器优化以及向存储引擎请求数据等),从而极大地提高了性能。

当然,若数据变化非常频繁的情况下,使用Query Cache可能会得不偿失。

这是为啥,用它不是提速吗?咋还得不偿失

因为MySQL只要涉及到数据更改,就会重新维护缓存。

  1. 如果SQL请求量比较大,你在维护的时候,就透过缓存走磁盘检索。这样数据库的压力肯定大。
  2. 重建缓存数据,它需要mysql后台线程来工作。也会增加数据库的负载。

所以在MySQL8已经取消了它。 故一般在读多写少,数据不怎么变化的场景可用它,例如:博客

Query Cache使用需要多个参数配合,其中最为关键的是query_cache_size和query_cache_type, 前者用于设置缓存ResultSet的内存大小,后者设置在何种场景下使用Query Cache。

file
这样可以通过计算Query Cache的命中率来进行调整缓存大小。

1.2.检验Query Cache的合理性

检查Query Cache设置的是否合理,可以通过在MySQL控制台执行以下命令观察:

  • SHOW VARIABLES LIKE ‘%query_cache%’;
  • SHOW STATUS LIKE ‘Qcache%’; 通过检查以下几个参数可以知道query_cache_size设置得是否合理:
    • Qcache_inserts:表示Cache多少次未命中然后插入到缓存
    • Qcache_hits: 表示命中多少次,它可反映出缓存的使用效果。

如果Qcache_hits的值非常大,则表明查询缓冲使用非常频繁,如果该值较小反而会影响效率,那么可以考虑不用查询缓存;

  • Qcache_lowmem_prunes: 表示多少条Query因为内存不足而被清除出Query_Cache。

如果Qcache_lowmem_prunes的值非常大,则表明经常出现缓冲不够的情况,因增加缓存容量。

  • Qcache_free_blocks: 表示缓存区的碎片

Qcache_free_blocks值非常大,则表明缓存区中的碎片很多,可能需要寻找合适的机会进行整理。

通过 Qcache_hits 和 Qcache_inserts 两个参数可以算出Query Cache的命中率:

file

通过 Qcache_lowmem_prunes 和 Qcache_free_memory 相互结合,能更清楚地了解到系统中Query Cache的内存大小是否真的足够,是否频繁的出现因内存不足而有Query被换出的情况。

1.3.InnoDB的缓存性能

当选择 InnoDB 时,innodb_buffer_pool_size 参数可能是影响性能的最为关键的一个参数,它用来设置缓存InnoDB索引及数据块、自适应HASH、写缓冲等内存区域大小,更像是Oracle数据库的 db_cache_size。

简单来说,当操作InnoDB表的时候,返回的所有数据或者查询过程中用到的任何一个索引块,都会在这个内存区域中去查询一遍。

和MyISAM引擎中的 key_buffer_size 一样,innodb_buffer_pool_size设置了 InnoDB 引擎需求最大的一块内存区域,直接关系到InnoDB存储引擎的性能,所以如果有足够的内存,尽可将该参数设置到足够大,将尽可能多的InnoDB的索引及数据都放入到该缓存区域中,直至全部。

说到缓存肯定少不了,缓存命中率。那innodb该如何计算?

file

计算出缓存命中率后,在根据命中率来对
innodb_buffer_pool_size 参数大小进行优化

除开查询缓存。数据库查询的性能也与MySQL的连接数有关

table_cache 用于设置 table 高速缓存的数量。

show global status like ‘open%_tables’; # 查看参数

由于每个客户端连接都会至少访问一个表,因此该参数与max_connections有关。当某一连接访问一个表时,MySQL会检查当前已缓存表的数量。

如果该表已经在缓存中打开,则会直接访问缓存中的表以加快查询速度;如果该表未被缓存,则会将当前的表添加进缓存在进行查询。

在执行缓存操作之前,table_cache参数用于限制缓存表的最大数目:

如果当前已经缓存的表未达到table_cache数目,则会将新表添加进来;若已经达到此值,MySQL将根据缓存表的最后查询时间、查询率等规则释放之前的缓存。

02平台级缓存

什么是平台级缓存,说的这个玄乎?

平台级缓存是指你所用什么开发语言,具体选择的是那个平台,毕竟缓存本身就是提供给上层调用。主要针对带有缓存特性的应用框架,或者可用于缓存功能的专用库。

如:

  • PHP中的Smarty模板库
  • Java中,缓存框架更多,如Ehcache,Cacheonix,Voldemort,JBoss Cache,OSCache等等。

Ehcache是现在最流行的纯Java开源缓存框架,配置简单、结构清晰、功能强大,是从hibernate的缓存开始被广泛使用起来的。EhCache有如下特点:

file
Ehcache的系统结构如图所示:

file

什么是分布式缓存呢?好像我还没搞明白,小吒哥

首先得看看恒古不变的“分布式”,即它是独立的部署到多个服务节点上或者独立的进程,彼此之间仅仅通过消息传递进行通信和协调。

也就是说分布式缓存,它要么是在单机上有多个实例,要么就独立的部署到不同服务器,从而把缓存分散到各处

最后通过客户端连接到对应的节点来进行缓存操作。

Voldemort是一款基于Java开发的分布式键-值缓存系统,像JBoss的缓存一样,Voldemort同样支持多台服务器之间的缓存同步,以增强系统的可靠性和读取性能。

Voldemort有如下特点:

file
Voldemort的逻辑架构图

file
Voldemort相当于是Amazon Dynamo的一个开源实现,LinkedIn用它解决了网站的高扩展性存储问题。

简单来说,就平台级缓存而言,只需要在框架侧配置一下属性即可,而不需要调用特定的方法或函数。

系统中引入缓存技术往往就是从平台级缓存开始,平台级缓存也通常会作为一级缓存使用。

既然平台级缓存都使用框架配置来实现,这咋实现缓存的分布式呢?节点之间都没有互相的消息通讯了

如果单看,框架缓存的调用,那确实没办法做到分布式缓存,因为自身没得像Redis那样分布式的部署方式,通过网络把各节点连接 。

但本地平台缓存可通过远程过程调用,来操作分布在各个节点上的平台缓存数据。

file

在 Ehcache 中:
file

03应用级缓存

当平台级缓存不能满足系统的性能时,就要考虑使用应用级缓存。 应用级缓存,需要开发者通过代码来实现缓存机制。

有些许 一方有难,八方支援 的感觉。自己搞不定 ,请教别人

这是NoSQL的战场,不论是Redis还是MongoDB,以及Memcached都可作为应用级缓存的技术支持。

一种典型的方式是每分钟或一段时间后统一生成某类页面存储在缓存中,或者可以在热数据变化时更新缓存。

为啥平台缓存还不能满足系统性能要求呢?它不是还可以减少应用缓存的网络开销吗
那你得看这几点:

file

3.1面向Redis的缓存应用

Redis是一款开源的、基于BSD许可的高级键值对缓存和存储系统,例如:新浪微博有着几乎世界上最大的Redis集群。

为何新浪微博是世界上最大的Redis集群呢?

微博是一个社交平台,其中用户关注与被关注、微博热搜榜、点击量、高可用、缓存穿透等业务场景和技术问题。Redis都有对应的hash、ZSet、bitmap、cluster等技术方案来解决。

在这种数据关系复杂、易变化的场景上面用到它会显得很简单。比如:

用户关注与取消:用hash就可以很方便的维护用户列表,你可以直接找到key,然后更改value里面的关注用户即可。

如果你像 memcache ,那只能先序列化好用户关注列表存储,更改在反序列化。然后再缓存起来,像大V有几百万、上千万的用户,一旦关注/取消。
当前任务的操作就会有延迟。

Reddis主要功能特点

  • 主从同步

Redis支持主从同步,数据可以从主服务器向任意数量的从服务器同步,从服务器可做为关联其他从服务器的主服务器。这使得Redis可执行单层树状复制。

file

  • 发布/订阅

由于实现了发布/订阅机制,使得从服务器在任何地方同步树的时候,可订阅一个频道并接收主服务器完整的消息发布记录。同步对读取操作的可扩展性和数据冗余很有帮助。

file

  • 集群

Redis 3.0版本加入cluster功能,解决了Redis单点无法横向扩展的问题。Redis集群采用无中心节点方式实现,无需proxy代理,客户端直接与Redis集群的每个节点连接,根据同样的哈希算法计算出key对应的slot,然后直接在slot对应的Redis上执行命令。

从Redis视角来看,响应时间是最苛刻的条件,增加一层带来的开销是不能接受的。因此,Redis实现了客户端对节点的直接访问,为了去中心化,节点之间通过Gossip协议交换相互的状态,以及探测新加入的节点信息。Redis集群支持动态加入节点,动态迁移slot,以及自动故障转移。

Redis集群的架构示意如图所示。

file

那什么是 Gossip 协议呢? 感觉好高大上,各种协议频繁出现

Gossip 协议是一个多播协议,基本思想是:

一个节点想要分享一些信息给网络中的其他的一些节点。于是,它周期性的随机选择一些节点,并把信息传递给这些节点。这些收到信息的节点接下来会做同样的事情,即把这些信息传递给其他一些随机选择的节点。直至全部的节点。

即,Redis集群中添加、剔除、选举主节点,都是基于这样的方式。

例如:当加入新节点时(meet),集群中会随机选择一个节点来邀请新节点,此时只有邀请节点和被邀请节点知道这件事,其余节点要等待 ping 消息一层一层扩散。
除了 Fail 是立即全网通知的,其他诸如新节点、节点重上线、从节点选举成为主节点、槽变化等,都需要等待被通知到,所以Gossip协议也是最终一致性的协议。

这种多播的方式,是不是忽然有种好事不出门,坏事传千里的感脚

然而,Gossip协议也有不完美的地方,例如,拜占庭问题(Byzantine)。即,如果有一个恶意传播消息的节点,Gossip协议的分布式系统就会出问题。

注:Redis集群节点通信消息类型

file

所有的Redis节点通过PING-PONG机制彼此互联,内部使用二进制协议优化传输速度和带宽。

这个ping为啥能提高传输速度和带宽? 感觉不大清楚,小吒哥。那这里和OSI网络层级模式有关系了

在OSI网络层级模型下,ping协议隶属网络层,所以它会减少网络层级传输的开销,而二进制是用最小单位0,1表示的位。

带宽是固定的,如果你发送的数据包都很小,那传输就很快,并不会出现数据包很大还要拆包等复杂工作。

相当于别人出差1斤多MacPro。你出差带5斤的战神电脑。

Redis的瓶颈是什么呢? 吒吒辉给安排

Redis本身就是内存数据库,读写I/O是它的强项,瓶颈就在单线程I/O上与内存的容量上。 目前已经有多线程了,

例如:Redis6具备网络传输的多线程模式,keydb直接就是多线程。

啥? 还没了解多Redis6多线程模式,后面单独搞篇来聊聊

集群节点故障如何发现?

节点故障是通过集群中超过半数的节点检测失效时才会生效。客户端与Redis节点直连,客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。

Redis Cluster把所有的物理节点映射到slot上,cluster负责维护node、slot和value的映射关系。当节点发生故障时,选举过程是集群中所有master参与的,如果半数以上master节点与当前master节点间的通信超时,则认为当前master节点挂掉。

这为何不没得Slave节点参与呢?

集群模式下,请求在集群模式下会自动做到读写分离,即读从写主。但现在是选择主节点。只能由主节点来进行身份参与。

毕竟集群模式下,主节点有多个,每个从节点只对应一个主节点,那这样,你别个家的从节点能够参与选举整个集群模式下的主节点吗?

就好像小姐姐有了对象,那就是名花有主,你还能在有主的情况下,去选一个? 小心遭到社会的毒打

如果集群中超过半数以上master节点挂掉,无论是否有slave集群,Redis的整个集群将处于不可用状态。

当集群不可用时,所有对集群的操作都不可用,都将收到错误信息:

[(error)CLUSTERDOWN The cluster is down]。

支持Redis的客户端编程语言众多,可以满足绝大多数的应用,如图所示。

file

3.2.多级缓存实例

一个使用了Redis集群和其他多种缓存技术的应用系统架构如图所示

file

负载均衡

首先,用户的请求被负载均衡服务分发到Nginx上,此处常用的负载均衡算法是轮询或者一致性哈希,轮询可以使服务器的请求更加均衡,而一致性哈希可以提升Nginx应用的缓存命中率。

什么是一致性hash算法?

hash算法计算出的结果值本身就是唯一的,这样就可以让每个用户的请求都落到同一台服务器。

默认情况下,用户在那台在服务器登录,就生成会话session文件到该服务器,但如果下次请求重新分发给其他服务器就又需要重新登录。

而有了一致性hash算法就可以治愈它,它把请求都专心交给同一台服务器,铁打的专一,从而避免上述问题。 当然这里的一致性hash原理就没给大家讲了。后面安排

nginx本地缓存

请求进入到Nginx应用服务器,首先读取本地缓存,实现本地缓存的方式可以是Lua Shared Dict,或者面向磁盘或内存的 Nginx Proxy Cache,以及本地的Redis实现等,如果本地缓存命中则直接返回。

这本地缓存怎么感觉那么特别呢? 好像你家附近的小姐姐,离得这么近,可惜吃不着。呸呸呸,跑题啦

  • Lua Shard Dict是指在nginx上,通过lua开辟一块内存空间来存储缓存数据。相当于用的是nginx的进程资源
  • nginx Cache指nginx获取上游服务的数据缓存到本地。
  • 本地Redis指nginx和Redis部署在同一台服务上,由nginx直接操作Redis

啥! nginx还可直接操作Redis呀,听我细细到来

这些方式各有千秋,Lua Shard Dict 是通过Lua脚本控制缓存数据的大小并可以灵活的通过逻辑处理来修改相关缓存数据。

而Nginx Proxy Cache开发相对简单,就是获取上游数据到本地缓存处理。 而本地Redis则需要通过lua脚本编写逻辑来设置,虽然操作繁琐了,但解决了本地内存局限的问题。

所以nginx操作Redis是需要借助于 Lua 哒

nginx本地缓存有什么优点?

Nginx应用服务器使用本地缓存可以提升整体的吞吐量,降低后端的压力,尤其应对热点数据的反复读取问题非常有效。

本地缓存未命中时如何解决?

如果Nginx应用服务器的本地缓存没有命中,就会进一步读取相应的分布式缓存——Redis分布式缓存的集群,可以考虑使用主从架构来提升性能和吞吐量,如果分布式缓存命中则直接返回相应数据,并回写到Nginx应用服务器的本地缓存中。

如果Redis分布式缓存也没有命中,则会回源到Tomcat集群,在回源到Tomcat集群时也可以使用轮询和一致性哈希作为负载均衡算法。

file

那我是PHP技术栈咋办?都不会用到java的Tomcat呀

nginx常用于反向代理层。而这里的Tomcat更多是属于应用服务器,如果换成PHP,那就由php-fpm或者swoole服务来接受请求。即不管什么语言,都应该找对应语言接受请求分发的东西。

当然,如果Redis分布式缓存没有命中的话,Nginx应用服务器还可以再尝试一次读主Redis集群操作,目的是防止当从Redis集群有问题时可能发生的流量冲击。

这样的设计方案我在下表示看不懂

如果你网站流量比较大,如果一次在Redis分布式缓存中未读取到的话,直接透过到数据库,那流量可能会把数据库冲垮。这里的一次读主也是考虑到Redis集群中的主从延迟问题,为的就是防止缓存击穿。

在Tomcat | PHP-FPM集群应用中,首先读取本地平台级缓存,如果平台级缓存命中则直接返回数据,并会同步写到主Redis集群,在由主从同步到从Redis集群。

此处可能存在多个Tomcat实例同时写主Redis集群的情况,可能会造成数据错乱,需要注意缓存的更新机制和原子化操作。

如何保证原子化操作执行呢?

当多个实例要同时要写Redis缓存时,为了保持原子化,起码得在涉及这块业务多个的 Key 上采用lua脚本进行封装,然后再通过分布式锁或去重相同请求并入到一个队列来获取,让获取到锁或从队列pop的请求去读取Redis集群中的数据。

如果所有缓存都没有命中,系统就只能查询数据库或其他相关服务获取相关数据并返回,当然,我们已经知道数据库也是有缓存的。 是不是安排得明明白白。

file
这就是多级缓存的使用,才能保障系统具备优良的性能。

什么时候,小姐姐也能明白俺的良苦心。。。。 默默的独自流下了泪水

3.3.缓存算法

缓存一般都会采用内存来做存储介质,使用索引成本相对来说还是比较高的。所以在使用缓存时,需要了解缓存技术中的几个术语。

file

缓存淘汰算法

替代策略的具体实现就是缓存淘汰算法。

使用频率:

file

  1. Least-Recently-Used(LRU) 替换掉最近被请求最少的对象。

在CPU缓存淘汰和虚拟内存系统中效果很好。然而在直接应用与代理缓存中效果欠佳,因为Web访问的时间局部性常常变化很大。

浏览器就一般使用了LRU作为缓存算法。新的对象会被放在缓存的顶部,当缓存达到了容量极限,底部的对象被去除,方法就是把最新被访问的缓存对象放到缓存池的顶部。

  1. Least-Frequently-Used(LFU) 替换掉访问次数最少的缓存,这一策略意图是保留最常用的、最流行的对象,替换掉很少使用的那些数据。

然而,有的文档可能有很高的使用频率,但之后再也不会用到。传统的LFU策略没有提供任何移除这类文件的机制,因此会导致“缓存污染”,即一个先前流行的缓存对象会在缓存中驻留很长时间,这样,就阻碍了新进来可能会流行的对象对它的替代。

  1. Pitkow/Recker 替换最近最少使用的对象

除非所有对象都是今天访问过的。如果是这样,则替换掉最大的对象。这一策略试图符合每日访问Web网页的特定模式。这一策略也被建议在每天结束时运行,以释放被“旧的”、最近最少使用的对象占用的空间。

  1. Adaptive Replacement Cache(ARC) ARC介于LRU和LFU之间,为了提高效果,由2个LRU组成。

第一个包含的条目是最近只被使用过一次的,而第二个LRU包含的是最近被使用过两次的条目,因此,得到了新的对象和常用的对象。ARC能够自我调节,并且是低负载的。

  1. Most Recently Used(MRU) MRU与LRU是相对,移除最近最多被使用的对象。

当一次访问过来的时候,有些事情是无法预测的,并且在存系统中找出最少最近使用的对象是一项时间复杂度非常高的运算,这时会考虑MRU,在数据库内存缓存中比较常见。

访问计数
  1. Least Recently Used2 (LRU2)

LRU的变种,把被两次访问过的对象放入缓存池,当缓存池满了之后,会把有两次最少使用的缓存对象去除。

因为需要跟踪对象2次,访问负载就会随着缓存池的增加而增加。

  1. Two Queues(2Q) Two Queues是LRU的另一个变种。

把被访问的数据放到LRU的缓存中,如果这个对象再一次被访问,就把他转移到第二个、更大的LRU缓存,使用了多级缓存的方式。去除缓存对象是为了保持第一个缓存池是第二个缓存池的1/3。

当缓存的访问负载是固定的时候,把LRU换成LRU2,就比增加缓存的容量更好。

缓存容量算法
  1. SIZE 替换占用空间最大的对象,这一策略通过淘汰一个大对象而不是多个小对象来提高命中率。不过,可能有些进入缓存的小对象永远不会再被访问。SIZE策略没有提供淘汰这类对象的机制,也会导致“缓存污染”。
  2. LRU-Threshold 不缓存超过某一size的对象,其他与LRU相同。
  3. Log(Size)+LRU 替换size最大的对象,当size相同时,按LRU进行替换。
缓存时间
  1. Hyper-G LFU的改进版,同时考虑上次访问时间和对象size。
  2. Lowest-Latency-First 替换下载时间最少的文档。显然它的目标是最小化平均延迟。
缓存评估
  1. Hybrid Hybrid 有一个目标是减少平均延迟。

对缓存中的每个文档都会计算一个保留效用,保留效用最低的对象会被替换掉。位于服务器S的文档f的效用函数定义如下:

file
Cs是与服务器s的连接时间;

bs是服务器s的带宽;frf代表f的使用频率;sizef是文档f的大小,单位字节。K1和K2是常量,Cs和bs是根据最近从服务器s获取文档的时间进行估计的。

  1. Lowest Relative Value(LRV) LRV也是基于计算缓存中文档的保留效用,然后替换保留效用最低的文档。
随机与队列算法
  1. First in First out(FIFO)

FIFO通过一个队列去跟踪所有的缓存对象,最近最常用的缓存对象放在后面,而更早的缓存对象放在前面,当缓存容量满时,排在前面的缓存对象会被踢走,然后把新的缓存对象加进去。

  1. Random Cache 随机缓存就是随意的替换缓存数据,比FIFO机制好,在某些情况下,甚至比LRU好,但是通常LRU都会比随机缓存更好些。

还有很多的缓存算法,例如Second Chance、Clock、Simple time-based、Extended time-based expiration、Sliding time-based expiration……各种缓存算法没有优劣之分,不同的实际应用场景,会用到不同的缓存算法。在实现缓存算法的时候,通常会考虑**使用频率、获取成本、缓存容量和时间等因素。 **

04.使用公有云的缓存服务

file
国内的共有云服务提供商如阿里云、青云、百度云等都推出了基于Redis的云存储服务,这些服务的有如下特点:

  • 动态扩容:

用户可以通过控制面板升级所需Redis的存储空间,扩容过程中服务不需要中断或停止,整个扩容过程对用户是透明且无感知的,而自主使用集群解决Redis平滑扩容是个很烦琐的任务,现在需要用你的小手按几下鼠标就能搞定,大大减少了运维的负担。

  • 数据多备:

数据保存在一主一备两台机器中,其中一台机器宕机了,数据还在另外一台机器上有备份。

  • 自动容灾:

主机宕机后系统能自动检测并切换到备机上,实现了服务的高可用性。

  • 成本较低:

在很多情况下,为使Redis的性能更好,需要购买一台专门的服务器用于Redis的存储服务,但这样会导致某些资源的浪费,购买Redis云存储服务就能很好地解决这样的问题。

有了Redis云存储服务,能使后台开发人员从烦琐的运维中解放出来。应用后台服务中,如果自主搭建一个高可用、高性能的Redis集群服务,是需要投入相当的运维成本和精力。

如果使用云服务,就没必要投入这些成本和精力,可以让后台应用的开发人员更专注于业务。

我是吒吒辉,就爱分析进阶相关知识,下期在见。如果觉得文章对你有帮助,欢迎分享+关注额。 同时这边我也整理了后端系统提升的电子书和技术问题的知识卡片,也一并分享给大家,后面将持续更新,你们的关注将是我继续写作下去的最大动力。 需要的小伙伴可微信搜索【莲花童子哪吒】回复:知识

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…757758759…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%