SegmentFault 七淅在学Java最新的文章
2022-06-27T20:00:00+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
请求一下子太多了,数据库危
https://segmentfault.com/a/1190000042039069
2022-06-27T20:00:00+08:00
2022-06-27T20:00:00+08:00
七淅在学Java
https://segmentfault.com/u/qixijava
1
<p>大家好,我是七淅(xī)。</p><p>如标题所说,和大家分享一个我曾优化过的业务场景。</p><p>当然,具体业务细节不重要,重要的是优化的思路。如果大家以后有遇到类似特点的场景,能够想到七淅这篇优化文章,那我就觉得很值了。</p><p>接下来我就直接进入主题,要分享得优化思路就是<strong>请求合并</strong>。</p><p>弱弱说一句,由于优化效果特别明显,这一优化我直接写到简历上了。</p><p>之前面试有不少面试官都会来问我是怎么做的,你看这不就给我机会发挥了吗?所以大家懂的,有合适场景记得用起来,以后面试也和面试官谈笑风生。</p><h2>1. 什么是请求合并</h2><p>首先说明一下,这并不是什么高级的优化方式,不难,朴实无华,但有用。</p><p>如字面意思,就是(把多个)请求合并(成一个请求去处理)。</p><p>现在含义你知道了,现在我们看下文章标题:「请求一下子太多了,数据库危」</p><p>聪明的你是不是已经猜到七淅要怎么优化了?</p><h2>2. 业务背景</h2><p>我有一个推送业务,会把每次推送记录都存到 MongoDB 中。</p><blockquote>PS:不用在意是 MongoDB 哈,可能有的读者可能没接触过,没关系。反正它也是一个数据库,就算换成 MySQL,优化一样适用哈</blockquote><p>而推送业务一个非常常见的场景就是定时发送消息给用户,所以到点之后对应的每秒写请求就特别高。</p><p>当初我这边是有 8000 的每秒写入量,后面通过请求合并优化到每秒写 500。</p><h2>3. 优化实现</h2><p>现在问题来了,具体怎么实现的呢?</p><p>理清有 3 个小点就好了,我们顺着思路理一下:</p><p>1、首先,既然我们需要把多个请求合成一个,是不是需要有一个地方把这多个请求给存起来?</p><p>存数据的话,是不是就可以用数据库、缓存、队列了。如下图:</p><p><img src="/img/remote/1460000042039071" alt="image" title="image"></p><p>好了,现在数据存起来了,不可能一直存着吧,一直存着不处理,岂不是就是不处理请求了,那这肯定不对。</p><p>2、所以我们是不是就需要知道,什么时候要处理存起来的数据?</p><p>「什么时候」—— 用定时任务每隔多久取出来处理是不是就可以,或者当存够多少数据量的时候,我们就集中处理。</p><p>我这里的业务是用的定时任务来做的,那关于定时任务的实现也有多种方式。</p><p>我这里是用的定时线程池,每隔 xx 秒,取 500 个请求进行处理。这里你发现没有,优化后的效果就是每秒写 500,这个 500 就是这里每次取得请求量来的。</p><p>所以说,优化效果要多少的处理量都是我们自己决定的。当然了,因为请求总量是不变得,所以每秒处理量越少,对应处理时间就越长。具体是什么数字,这个就看具体业务啦</p><p>3、最后一点,多个请求存起来了,也知道什么时机去处理,那这个「处理」是怎么处理呢?</p><p>答案很简单,把多个单次操作换成批量操作。比如数据库批量插入,redis 的 mset。</p><p>以上 3 点,给大家整理个流程图:</p><p><img src="/img/remote/1460000042039072" alt="image" title="image"></p><h2>4. 适用场景</h2><p>最后,上面得优化姿势学废后,那什么业务场景可以用呢?</p><p>其实这个你想想,请求合并后的效果是怎样的,差不多也就知道了。</p><p>一开始说含义:(把多个)请求合并(成一个请求去处理)</p><p>这样的效果说明业务允许请求可以不用马上处理,高级用语就是数据实时性要求不高 —— 这是第一个业务场景特点</p><p>第二个特点:「请求合并」,业务得有一定的并发场景才有机会给你合并呀。不然你说每分钟才几个请求,那这不是合了个寂寞吗?(狗头)</p><h2>5. 文末休息区(求关注)</h2><p>其实很多优化方式都是朴实无华的,第一次听说时候可能会觉得牛蛙牛蛙,但实际接触了解后,会发现其实也没多高级。</p><p>不过,高级的优化方式肯定也是有的。就像我也很好奇,阿里京东这种亿级秒杀,微博的粉丝关系,抖音的推荐等等是怎么实现的。<br>倘若哪天真的有实际参与的大佬和我说,无非就是堆机器,没什么特别的,那我真的会尬住,哈哈哈哈。</p><blockquote>如果觉得文章不错,欢迎关注我的公众号:七淅在学Java</blockquote><p><img src="/img/bVc0yrw" alt="" title=""></p>
有关 ThreadLocal 的一切
https://segmentfault.com/a/1190000041822011
2022-05-10T13:15:49+08:00
2022-05-10T13:15:49+08:00
七淅在学Java
https://segmentfault.com/u/qixijava
0
<p>早上好,各位新老读者们,我是七淅(xī)。</p><p>今天和大家分享的是面试常驻嘉宾:ThreadLocal</p><p>当初鹅厂一面就有问到它,问题的答案在下面正文的第 2 点。</p><h2>1. 底层结构</h2><p>ThreadLocal 底层有一个默认容量为 16 的数组组成,k 是 ThreadLocal 对象的引用,v 是要放到 TheadLocal 的值</p><pre><code class="java">public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}</code></pre><p>数组类似为 HashMap,对哈希冲突的处理不是用链表/红黑树处理,而是使用链地址法,即尝试顺序放到哈希冲突下标的下一个下标位置。</p><p>该数组也可以进行扩容。</p><h2>2. 工作原理</h2><p>一个 ThreadLocal 对象维护一个 ThreadLocalMap 内部类对象,ThreadLocalMap 对象才是存储键值的地方。</p><p>更准确的说,是 ThreadLocalMap 的 Entry 内部类是存储键值的地方</p><p>见源码 <code>set()</code>,<code>createMap()</code> 可知。</p><p><strong>因为一个 Thread 对象维护了一个 ThreadLocal.ThreadLocalMap 成员变量,且 ThreadLocal 设置值时,获取的 ThreadLocalMap 正是当前线程对象的 ThreadLocalMap</strong>。</p><pre><code class="java">// 获取 ThreadLocalMap 源码
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}</code></pre><p>所以每个线程对 ThreadLocal 的操作互不干扰,即 ThreadLocal 能实现线程隔离</p><h2>3. 使用</h2><pre><code class="java">ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("七淅在学Java");
Integer i = threadLocal.get()
// i = 七淅在学Java</code></pre><h2>4. 为什么 ThreadLocal.ThreadLocalMap 底层是长度 16 的数组呢?</h2><p>对 ThreadLocal 的操作见第 3 点,可以看到 ThreadLocal 每次 set 方法都是对同个 key(因为是同个 ThreadLocal 对象,所以 key 肯定都是一样的)进行操作。</p><p>如此操作,看似对 ThreadLocal 的操作永远只会存 1 个值,那用长度为 1 的数组它不香吗?为什么还要用 16 长度呢?</p><p>好了,其实这里有个需要注意的地方,<strong>ThreadLocal 是可以存多个值的</strong></p><p>那怎么存多个值呢?看如下代码:</p><pre><code class="java">// 在主线程执行以下代码:
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("七淅在学Java");
ThreadLocal<String> threadLocal2 = new ThreadLocal<>();
threadLocal2.set("七淅在学Java2");</code></pre><p>按代码执行后,看着是 new 了 2 个 ThreadLocal 对象,但实际上,数据的存储都是在同一个 ThreadLocal.ThreadLocalMap 上操作的</p><p>再次强调:ThreadLocal.ThreadLocalMap 才是数据存取的地方,ThreadLocal 只是 api 调用入口)。真相在 ThreadLocal 类源码的 <code>getMap()</code></p><p>因此上述代码最终结果就是一个 ThreadLocalMap 存了 2 个不同 ThreadLocal 对象作为 key,对应 value 为 七淅在学Java、七淅在学Java2。</p><p>我们再看下 ThreadLocal 的 <code>set</code> 方法</p><pre><code class="java">public void set(T value) {
Thread t = Thread.currentThread();
// 这里每次 set 之前,都会调用 getMap(t) 方法,t 是当前调用 set 方法的线程
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
// 重点:返回调用 set 方法的线程(例子是主线程)的 ThreadLocal 对象。
// 所以不管 api 调用方 new 多少个 ThreadLocal 对象,它永远都是返回调用线程(例子是主线程)的 ThreadLocal.ThreadLocalMap 对象供调用线程去存取数据。
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// t.threadLocals 的声明如下
ThreadLocal.ThreadLocalMap threadLocals = null;
// 仅有一个构造方法
public ThreadLocal() {
}
</code></pre><h2>5. 数据存放在数组中,那如何解决 hash 冲突问题</h2><p>使用链地址法解决。</p><p>具体怎么解决呢?看看执行 get、set 方法的时候:</p><ul><li><p>set:</p><ul><li>根据 ThreadLocal 对象的 hash 值,定位到 ThreadLocalMap 数组中的位置。</li><li>如果位置无元素则直接放到该位置</li><li><p>如果有元素</p><ul><li>且数组的 key 等于该 ThreadLocal,则覆盖该位置元素</li><li>否则就找下一个空位置,直到找到空或者 key 相等为止。</li></ul></li></ul></li><li><p>get:</p><ul><li>根据 ThreadLocal 对象的 hash 值,定位到 ThreadLocalMap 数组中的位置。</li><li>如果不一致,就判断下一个位置</li><li>否则则直接取出</li></ul></li></ul><pre><code class="java">// 数组元素结构
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}</code></pre><h2>6. ThreadLocal 的内存泄露隐患</h2><p>三个前置知识:</p><ul><li>ThreadLocal 对象维护一个 ThreadLocalMap 内部类</li><li>ThreadLocalMap 对象又维护一个 Entry 内部类,并且该类继承弱引用 <code>WeakReference<ThreadLocal<?>></code>,用来存放作为 key 的 ThreadLocal 对象(可见最下方的 Entry 构造方法源码),可见最后的源码部分。</li><li>不管当前内存空间足够与否,GC 时 JVM 会回收弱引用的内存</li></ul><p>因为 ThreadLocal 作为弱引用被 Entry 中的 Key 变量引用,所以如果 ThreadLocal 没有外部强引用来引用它,那么 ThreadLocal 会在下次 JVM 垃圾收集时被回收。</p><p>这个时候 Entry 中的 key 已经被回收,但 value 因为是强引用,所以不会被垃圾收集器回收。这样 ThreadLocal 的线程如果一直持续运行,value 就一直得不到回收,导致发生内存泄露。</p><p><strong>如果想要避免内存泄漏,可以使用 ThreadLocal 对象的 remove() 方法</strong></p><h2>7. 为什么 ThreadLocalMap 的 key 是弱引用</h2><pre><code class="java">static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}</code></pre><p>为什么要这样设计,这样分为两种情况来讨论:</p><ul><li>key 使用强引用:只有创建 ThreadLocal 的线程还在运行,那么 ThreadLocalMap 的键值就都会内存泄漏,因为 ThreadLocalMap 的生命周期同创建它的 Thread 对象。</li><li>key 使用弱引用:是一种挽救措施,起码弱引用的值可以被及时 GC,减轻内存泄漏。另外,即使没有手动删除,作为键的 ThreadLocal 也会被回收。因为 ThreadLocalMap 调用 set、get、remove 时,都会先判断之前该 value 对应的 key 是否和当前调用的 key 相等。如果不相等,说明之前的 key 已经被回收了,此时 value 也会被回收。因此 key 使用弱引用是最优的解决方案。</li></ul><h2>8. (父子线程)如何共享 ThreadLocal 数据</h2><ol><li>主线程创建 InheritableThreadLocal 对象时,会为 t.inheritableThreadLocals 变量创建 ThreadLocalMap,使其初始化。其中 t 是当前线程,即主线程</li><li>创建子线程时,在 Thread 的构造方法,会检查其父线程的 inheritableThreadLocals 是否为 null。从第 1 步可知不为 null,接着 将父线程的 inheritableThreadLocals 变量值复制给这个子线程。</li><li>InheritableThreadLocal 重写了 getMap, createMap, 使用的都是 Thread.inheritableThreadLocals 变量</li></ol><p>如下:</p><pre><code class="java">public class InheritableThreadLocal<T> extends ThreadLocal<T>
关键源码:
第 1 步:对 InheritableThreadLocal 初始化
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
第 2 步:创建子线程时,判断父线程的 inheritableThreadLocals 是否为空。非空进行复制
// Thread 构造方法中,一定会执行下面逻辑
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
第 3 步:使用对象为第 1 步创建的 inheritableThreadLocals 对象
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
}
示例:
// 结果:能够输出「父线程-七淅在学Java」
ThreadLocal threadLocal = new InheritableThreadLocal();
threadLocal.set("父线程-七淅在学Java");
Thread t = new Thread(() -> System.out.println(threadLocal.get()));
t.start();
// 结果:null,不能够输出「子线程-七淅在学Java」
ThreadLocal threadLocal2 = new InheritableThreadLocal();
Thread t2 = new Thread(() -> {
threadLocal2.set("子线程-七淅在学Java");
});
t2.start();
System.out.println(threadLocal2.get());</code></pre><hr><p><strong>文章首发公众号:七淅在学Java</strong> ,持续原创输出 Java 后端干货。</p><p>如果对你有帮助的话,可以给个赞再走吗</p>
实战篇:单库单表变更成多库多表
https://segmentfault.com/a/1190000041681272
2022-04-10T11:06:07+08:00
2022-04-10T11:06:07+08:00
七淅在学Java
https://segmentfault.com/u/qixijava
0
<p>大家好,我是七淅(xī)。</p><p>如标题所说,本文会结合我自己的亲身经历,介绍 3 部分内容:</p><ol><li>线上单库单表变更到多库多表的各个实现方案</li><li>方案优劣对比</li><li>对于历史存在的单表,并且它们<strong>不需要</strong>变成多表,需要怎么处理</li></ol><p>先下个结论,<strong>没有百分百完美的方案,技术方案永远要结合产品业务来设计</strong>。</p><p>以下举例的方案也只是较为通用的做法,具体细节是可以根据业务场景进行变化调整的。</p><p>只要能够满足业务需求,就是好方案,不要为了秀技术而忽略业务。</p><p>看完这篇文章,如果后面有人问你,关于变更到多库多表的方案问题,那你可以和他谈笑风生了。</p><p>好了,下面我说下我这边的业务背景,和大家解释清楚为什么需要多库多表。后面会引申出方案的,莫急。</p><h2>1. 业务背景</h2><p>有一个<strong>在线上运行着</strong>的数据库,假设是 user 库,库中<strong>只有</strong> 1 张单表。</p><p>现在有个新需求,该需求的功能有一定的请求量和数据量。</p><p>其中数据量初期是百万级,考虑到业务增加,增长到千万、上亿都是有可能的。<strong>所以从数据量上看,单库单表不合适</strong>。</p><p>Q1:如果只是数据量问题,那用单库多表行不行? <br>A1:行。 <br>Q2:那为什么还用多库多表呢? <br>A2:<strong>因为一个数据库的连接数量是有限的,怕翻车</strong>。 </p><p>上面有介绍业务有一定的请求量,担心一个库来处理的话,万一哪天网络不好/慢查/该表业务有突发性活动等情况出现。</p><p>一不小心就把连接数占满了,那就直接翻车了。</p><p>加上我司对多库多表的基建比较成熟,所以我这边就直接上多库多表了。</p><p>Q3:既然如此,前期先上单库多表,等量上来后再多库多表行不行? <br>A3:可以。但是到时再来一次太累了。</p><p>比如再来一次会经历以下事情:</p><ul><li>每天需要看看数据监控</li><li>有没有到瓶颈</li><li>到时再次变更时,开发运维测试业务的排期和执行</li><li>业务变动:说好的下个季度大推,结果提前到下一个月进行,此时数据库能不能扛住,扛不住改造时间是否充足?</li></ul><p>所以,我们要不还是一步到位吧。</p><blockquote>滴,七淅提醒你:看到这,如果有人问你单库多表和多库多表的使用场景,你应该知道怎么发挥了吧</blockquote><h2>2. 历史数据处理</h2><p>我先说下对历史数据处理,篇幅较少。</p><p>这里的内容对应文章开头的第三点:对于历史存在的单表,并且它们<strong>不需要</strong>变成多表,需要怎么处理</p><p>这里可以有两种处理方式。</p><p>我们知道,历史数据在 user 库,假设业务需要增加到 8 个库,并且新表需要在这 8 个库中</p><h3>2.1 方式一</h3><p>新增 <code>user_0、user_1、...、user_7</code> 共 8 个库,使用 <code>rename</code> 命令,将 user 库的表迁移到 <code>user_0</code> 库中,最后将 user 库删掉即可。</p><p>rename 命令其实就是重新命名,实现剪切数据的效果,而不是复制。当然要用复制的方式迁移数据也是可以的,但我们这边没用。</p><p>reanme 命令使用如下:</p><p><code>rename table user.table_name to user_0.table_name;</code></p><h3>2.2 方式二</h3><p>新增 <code>user_0、user_1、...、user_7</code> 共 8 个库,user 库数据不动,继续使用。</p><p>至于选哪种方式,大多情况下,我个人认为都可以,但如果历史表本身请求就很高,那可以考虑用方式二,避免 0 号库压力太大。</p><p>我这边是选择的方式一。当用户要访问历史表时,指定路由到 0 号库就好了,顺便省下一台数据库的钱,真香</p><h2>3. 变更方案</h2><p>方案这块内容,我会基于方式一的历史数据处理方式来讲。</p><p>首先,先不考虑任何方案,我把最简单的,变更到多库多表的操作按顺序列举一下:</p><ol><li>修改服务连接数据库的配置,业务代码编写</li><li>增加 user 0-7 号数据库</li><li>将 user 库旧表数据迁移到新增 user_0 库</li><li>部署服务</li></ol><p>但是如果按照上述做法,在第 3、4 步执行期间,如果用户访问原 user 库的数据会有问题。</p><p>具体来说:user 库的旧数据此时已经通过 rename,迁移到了 user_0 库,但因为部署还没部署完成,连接数据库的配置没有更新。</p><p>所以请求依旧会跑去 user 库查询,导致查不到数据,后续业务逻辑没法顺序继续执行。</p><p>用户也会纳闷:「这个地方之前进来都有数据的呀,怎么现在全空了?」</p><p>所以,需要确定合理的升级方案,最大程度减少对业务和用户的影响,</p><h3>3.1 方案一</h3><p>这是最简单的方式。</p><p>看监控,挑选没有流量的时候,进行 db 变更和服务部署。</p><p>当然,监控也只是过去的情况,保不准功能上线那天就一直有流量没停歇过呢。</p><p>所以再求稳一点的话,可以发个公告,告知用户 xx 功能会在 xxx 时间段进行维护,期间不可访问。</p><p>如果有玩农药(王者荣耀)的朋友应该很熟悉吧,每次版本更新都需要停服,就是这样的效果哈。</p><p>最后在完成之后,进行回归测试和新功能测试,看看功能是否正常。</p><p>如果正常那就可以去睡觉了,有问题就继续改 bug 解决;</p><p>如果评估没法在公告所说的截止时间解决,那就只能进行回滚,改日再(jia)战(ban)。</p><p>PS:如果需要对历史数据进行分库分表的话,最好进行数据量的对比检验。因为我这边不涉及对历史数据进行分库分表,所以这步就省了。</p><h3>3.2 方案二</h3><p>这个方案会复杂很多,开发量也会很大。</p><p>我这边就只说关键步骤,具体细节就没法一一写了。因为要写的话又多出几千字的内容,篇幅太长,我估计也没多少人有耐心看完。</p><p>那话说回来,这个方案最大的好处就是业务功能不用停用,所以也就不用熬大夜了。</p><p>那要怎么做呢?</p><h4>3.2.1 历史单表数据处理</h4><p>1、先把 user 库现有的数据复制一份到 user_0 库。</p><p>2、因为 user 库的数据是会被修改和新增的。所以当复制完成后,数据依旧存在变化,所以需要新增双写逻辑,保证 user_0 库的数据也能同步到变更。</p><p>3、对于数据的读写,都支持由开关控制,分别可以控制数据读写是请求到哪个数据库。</p><p>4、服务更新完成后,进行两个库的数据一致性对比。都没问题后,开关控制读写数据都请求到 user_0 库</p><h4>3.2.2 新功能的多表数据处理</h4><p>因为是新功能,其实不用怎么特殊处理。</p><p>为什么这么说呢?</p><p>因为我们部署服务的顺序肯定是操作数据库的底层服务先发布,发布完成后,才对用到底层服务的应用服务进行发布。</p><p>所以作为业务功能入口的应用服务都还没发布,此时是不会有新功能数据到达底层服务的。</p><p>要是不能保证这个顺序,你想下功能入口开放了,用户请求进来后,底层服务发现找不到这个表,是不是就直接报错了?</p><p>所以才会有上面说的发布顺序,只要保证发布顺序没错,那这块新功能的数据是不需要特殊处理。</p><h3>3.3 方案优劣对比</h3><p>其实 2 个方案就是互补的,一个方案的优点就是解决了另一个方案的缺点。</p><p>七淅用表格总结一下:</p><table><thead><tr><th> </th><th>优点</th><th>缺点</th></tr></thead><tbody><tr><td>方案一</td><td>操作简单,无需编写复杂代码来保证有流量时,业务的正常执行</td><td>累人,熬大夜太酸爽了;会停用部分业务,影响用户体验</td></tr><tr><td>方案二</td><td>业务不必停用,不影响用户</td><td>开发成本大</td></tr></tbody></table><p>最后,你问我当初是选哪个方案?</p><p>那肯定是方案一啊,大不了熬一夜嘛。</p><p>不然那么麻烦的方案,排期又那么紧张,开发是不可能开发的,这辈子都不可能的。真有什么问题,大不了就人工介入处理,yyds</p><hr><p><strong>文章首发公众号:七淅在学Java</strong> ,持续原创输出 Java 后端干货。</p><p>如果对你有帮助的话,可以给个赞再走吗</p>
面试官:Redis中的缓冲区了解吗
https://segmentfault.com/a/1190000041572572
2022-03-19T11:21:04+08:00
2022-03-19T11:21:04+08:00
七淅在学Java
https://segmentfault.com/u/qixijava
0
<p>hello 大家好,我是七淅(xī)。</p><p>Redis 大家肯定不陌生,但在使用层面看不到的地方,就容易被忽略。今天想和大家分享的内容是 <strong>Redis 各个缓冲区的作用、溢出的后果及优化方向</strong>。</p><p>在开始正文前,想多叨叨几句。不管是 Redis 还是其他中间件,底层很多原理都是相似的,设计思想都是通用的。</p><p>大家以后如果在学什么新框架/组件,可以尽量和已经学过的知识点进行联想,这样会更容易理解点的,不至于说死记硬背。</p><p>比如现在说到的缓冲区,它的目的是什么呢?</p><p>无它,为了性能。</p><p>要么缓存数据,<strong>提高响应速度</strong>。比如 MySQL 中有个 change buffer</p><p>要么担心消费者速度跟不上生产,怕<strong>数据丢失</strong>。所以需要把生产数据先暂存起来。Redis 的缓冲区就是这个作用。</p><p>另外,消费者速度跟不上,如果是同步处理的话,那是不是也会拖慢生产者,所以这里其实也是在保证生产者的速度。</p><p>可能有的读者会说:扯淡,消费者都跟不上了,生产者再快有什么用?</p><p>其实有没有一种可能,生产者根本不关心消费者什么时候用呢?前者是负责把后者需要的东西处理好给它就完事了。生产者很忙,还有其他一大堆数据要处理,不能慢慢等消费者同步消费完才去做其他事情。</p><p>好像开头扩展得有点多,我收一收,下面会详细说到。有疑问的小伙伴请上车,七淅正式发车了。</p><h2>1. 各缓冲区</h2><p>首先 Redis 有什么缓冲区呢?</p><p>一共 4 个:</p><ul><li>客户端输入缓冲区</li><li>客户端输出缓冲区</li><li>复制缓冲区</li><li>复制积压缓冲区</li></ul><h2>2. 客户端输入缓冲区</h2><p>服务器端会给<strong>每个连接的客户端</strong>都设置了一个输入缓冲区。</p><h3>2.1 作用</h3><p>暂存请求数据。</p><p>输入缓冲区会先把客户端发送过来的命令暂存起来,Redis 主线程再从输入缓冲区中读取命令,进行处理。</p><p>为了避免客户端和服务器端的请求发送和处理速度不匹配,这点和等下要说的输出缓冲区是一样的。</p><h3>2.2 溢出场景</h3><p>首先缓冲区是一块固定大小的内存区域,如果要把这个地方填满的话,那 Redis 会直接把客户端连接关闭。</p><p>保护自己嘛,你客户端挂了总比我服务端挂了好,服务端一挂就是所有客户端都没用了。</p><p>那填满缓冲区就有 2 个情况了:</p><ol><li>要么一下子填满</li><li>要么生产速度大于消费速度,慢慢被填满</li></ol><p>那么把上述原理对应到 Redis 的场景。</p><p>一下子填满的情况可以是往 Redis 里写大量数据,百万千万数量级那种。</p><p>另一个情况可以是 Redis 服务端因执行耗时操作,阻塞住了,导致没法消费输入缓冲区数据。</p><h3>2.3 优化</h3><p>对应上面 2 个溢出场景,优化方向很自然就有了。</p><p>一下子填满的情况,是不是可以考虑不要一下子写这么多数据,能否拆下数据(其实一下子写大量数据本身就不合理哈)</p><p>另外,是否可以调高缓冲区大小呢?</p><p>这个其实是不行的哈,因为没有可以设置的地方,目前服务端默认为每个客户端输入缓冲区分配的大小是 1GB。</p><p>那轮到第 2 个溢出场景:两边处理速度不一致。</p><p>正常来说,服务端不应该出现长时间阻塞,所以需要看看是什么原因导致的阻塞,解决到就好了。</p><h2>3. 客户端输出缓冲区</h2><p>同输入缓冲区,服务器端也会给<strong>每个连接的客户端</strong>都设置了一个输出缓冲区。</p><h3>3.1 作用</h3><p>同上,也是暂存请求数据。</p><p>这个地方其实我在文章开头说的,生产者不关心消费者什么时候用,只负责把消费者之前请求的东西处理好就完事了。</p><p>可能有点抽象,我是这么理解的,如果有不妥的地方可以留言纠正我一下 </p><p>服务端一般都会和多个客户端连接,加上 redis 网络通信模块是单线程的(即使是新版本支持多线程也一样)</p><p>假如没有输出缓冲区会发生什么事呢?</p><p>服务端处理了很多客户端 A 的请求,需要经过网络这一耗时操作,返回给客户端 A。在这个过程中,客户端 B 的请求一直得不到服务端处理和响应,这样吞吐量就上不去了。</p><p>有了缓冲区之后,至少能解放服务端,让它去处理客户端 B 的请求。</p><h3>3.2 溢出场景</h3><p>这里也是同输入缓冲区,我就不啰嗦了,溢出的话服务端也会关闭客户端连接。</p><ol><li>服务器端返回了大量数据,一下子填满了</li><li>返回数据的速度太快,比如执行 MONITOR 命令,它会持续输出监测到的各个命令操作</li><li>缓冲区大小设置得不合理。</li></ol><h3>3.3 优化</h3><p>类似的,不要一下子读大量数据;不持续在线上执行 MONITOR 命令。</p><p>而输出缓冲区的大小是可以通过 client-output-buffer-limit 来设置的。</p><p>但是一般来说,我们都不用改,因为默认情况就够了,这里了解下就好。</p><p>值得说一点的是,Redis 发布订阅的消息也是在该缓冲区中,可以用 <code>client-output-buffer-limit pubsub 8mb 2mb 60</code> 来限制大小。</p><ul><li>pubsub 表示对订阅客户端进行设置。换成 normal 则表示当前设置的是普通客户端</li><li>整个配置的含义是:实际占用的缓冲区大小要超过 8MB,或者连续 60 秒内对输出缓冲区的写入量超过 2MB 的话,服务端就会关闭客户端连接。</li></ul><h2>4. 复制缓冲区</h2><p>温馨提示下,如果对 Redis 同步/复制不了解的读者,比如不知道全量/增量复制,建议可以看下我这篇文章:<a href="https://link.segmentfault.com/?enc=JkWnC1joIsa48yw9GvM%2Biw%3D%3D.M6w3DV5lp%2F4NdgMZhRwef%2FoLarQGDqDu3OHPRbKdbFcwe1%2F%2BusX9xRcFWfELU0AVqW2p09YSeY2dwUOMyGIzqw%3D%3D" rel="nofollow">一文让你明白Redis主从同步</a>。</p><p>下面回到正题哈。</p><p>有复制肯定有主从,而主从间的数据复制包括全量复制和增量复制两种。</p><p>全量复制是同步所有数据,而增量复制只会把主从库网络断连期间主库收到的命令,同步给从库。</p><h3>4.1 作用</h3><p>暂存数据。</p><p>主节点上会为<strong>每个从节点</strong>都维护一个复制缓冲区。</p><p>在全量复制时,主节点在向从节点传输 RDB 文件的同时,会继续接收客户端发送的写命令请求,并保存在复制缓冲区中,等 RDB 文件传输完成后,再发送给从节点去执行。</p><h3>4.2 溢出场景</h3><p>从节点接收和加载 RDB 较慢,同时主节点接收到了大量的写命令,写命令在复制缓冲区中就会越积越多,最后就会溢出。</p><p>一旦溢出,主节点会直接关闭和从节点进行复制操作的连接,导致全量复制失败</p><h3>4.3 优化</h3><p>可以控制主节点数据量在 2~4GB(仅供参考),这样可以让全量同步执行得更快些,避免复制缓冲区累积过多命令</p><p>也可以调整缓冲区大小,还是之前的 <code>client-output-buffer-limit</code> 参数。</p><p>比如:`<br>config set client-output-buffer-limit slave 512mb 128mb 60`</p><ul><li>slave 参数表明该配置项是针对复制缓冲区的.</li><li>整个配置的含义是:实际占用的缓冲区大小要超过 512MB,或者连续 60 秒内对输出缓冲区的写入量超过 128MB 的话,服务端就会关闭同步连接。</li></ul><h2>5. 复制积压缓冲区</h2><p>这个是在新增复制用到的缓冲区。</p><blockquote>具体介绍还是推荐看上面提到的文章哈,写到这里也 2k+ 字了,顶不住啦。</blockquote><h3>5.1 作用</h3><p>暂存数据。</p><p>从节点意外断开连接后重连,可从该缓冲区同步期间没同步到的数据。</p><h3>5.2 溢出场景</h3><p>不会溢出。(想不到吧.jpg)</p><p>该缓冲区本质是一个<strong>固定长度,先进先出的队列</strong>,默认 1MB。</p><p>所以当队列被占满,不是报错,也不像上面几个缓冲区直接关闭连接。而是覆盖最早进入队列的数据。</p><p>因此,如果有从节点还没有同步这些旧命令数据,就会导致主从节点重新进行全量复制,而不是增量复制。</p><blockquote>PS:全量复制性能开销远大于增量复制</blockquote><h3>5.3 优化</h3><p>调整复制积压缓冲区的大小,参数是:<code>repl_backlog_size</code></p><blockquote>原创不易,如果觉得文章不错,希望能关注下我的公众号:<strong>七淅在学Java</strong>,文章首发公号。</blockquote>
小白也能看懂的缓存雪崩、穿透、击穿
https://segmentfault.com/a/1190000041514210
2022-03-08T16:44:19+08:00
2022-03-08T16:44:19+08:00
七淅在学Java
https://segmentfault.com/u/qixijava
0
<p>大家好,我是七淅(xī)。</p><p>作为后端开发,我想缓存是大家再熟悉不过的东西了。</p><p>本文会介绍<strong>出现缓存雪崩、穿透和击穿的业务背景、解决方案和对业务可靠性处理</strong>。事先说明,最佳解决方案一定需要结合实际业务调整,不同业务的处理不完全相同</p><p>其实我在网上也看过不少关于缓存雪崩、穿透、击穿介绍,不知道是不是大家所做业务的不同,发现有不少小伙伴有以下疑问,比如:</p><ul><li>加随机时间过期后,如果访问时间刚好就是加了随机时间后的数据,这样岂不是白加了随机时间?</li><li>热点数据不过期,那岂不是有越来越多的脏数据?</li></ul><p>就以上问题,我都会在文中一一解释,以下说的缓存都指 Redis。</p><p>我争取把这一高频面试题讲明白,如果大家看后能在这块内容和面试官面前谈笑风生,那你就是最靓的仔。</p><p>下面,我就开始进入正题啦。</p><h2>1. 缓存雪崩</h2><p>即缓存同一时间大面积的失效,这个时候来了一大波请求,都怼到数据库上,最后数据库处理不过来崩了。</p><h3>1.1 业务场景举例</h3><p>APP 首页有大量热点数据,在某大型活动期间,针对不同时间段需要展示不同的首页数据。</p><p>比如在 0 点时需要替换新的首页数据,此时旧首页数据过期,新首页数据刚开始加载。</p><p>而 0 点正在有个小活动开始,大批请求涌入。因为新数据刚开始加载,请求多数没有命中缓存,请求到了数据库,最后就把数据库打挂了。</p><h3>1.2 解决方案</h3><p><strong>再强调一下,所谓的解决方案是需要根据实际业务调整,不同业务的处理不完全相同</strong></p><h4>1.2.1 方法一</h4><p>常见方式就是给过期时间加个随机时间。</p><p>注意这个随机时间不是几秒哈,可以长达几分钟。因为如果数据量很大,按照上述例子,加上 Redis 是单线程处理数据的。那么几秒的缓冲不一定能够保证新数据都被加载完成。</p><p>所以过期时间宁愿设置长一点,也好过短一点。反正最后都是会过期掉,最终效果是一样的。</p><p>而且过期时间范围加大,key 会更加分散,这样也是一定程度缩短 Redis 在过期 key 时候的阻塞时间。</p><p>而至于文章开头说的:「如果访问时间刚好就是加了随机时间后的数据,这样岂不是白加了随机时间」。</p><p>现在你结合上例活动的例子,它还会是一个问题吗?结合业务,一定要结合业务。</p><h4>1.2.2 方法二</h4><p>加互斥锁,但这个方案会导致吞吐量明显下降。所以还是要看实际业务,像上述例子就不合适用</p><h4>1.2.3 方法三</h4><p>热点数据不设置过期。不过期的话,正常业务请求自然就不会打到数据库了。</p><p>那新的问题又来了,不过期有脏数据,怎么办?</p><p>很简单,活动整体结束后再删除嘛。</p><p>那像上述例子,可以怎么处理呢?—— 选择方法一;或者提前把 0 点需要的新数据加载进 Redis,不必等到 0 点才去加载,这样也是可以的</p><h2>2. 缓存击穿</h2><p>缓存击穿是指一个热点 key 过期或被删除后,导致线上原本能命中该热点 key 的请求,瞬间大量地打到数据库上,最终导致数据库被击垮。</p><p>有种千里之堤,溃于蚁穴的感觉。</p><h3>2.1 业务场景举例</h3><p>出现情况一般是误操作,比如设置错了过期时间、误删除导致的。</p><blockquote>谁还没误操作过呢,删库跑路了解一下。反正我误删过测试库的数据,幸好人没事,狗头保命。</blockquote><h3>2.2 解决方案</h3><h3>方法一</h3><p>代码问题,该 review 的 review。</p><p>热点数据到底要不要过期,什么时候过期要明确</p><p>既然是热点数据,大概率是核心流程。那么该保证的核心功能还是需要保证的,减少犯错机会。万一出问题,那就是用户的一波输出了。</p><h3>方法二</h3><p>线上误操作的事情,该加强权限管理的加强,特别是线上权限,一定需要审核,以防手抖。</p><blockquote>PS:若有帮助希望大家可以点赞、在看、转发随便来一份鼓励吧,这对我真得很重要,非常感谢~</blockquote><h2>3. 缓存穿透</h2><p>缓存穿透是指:客户端请求缓存和数据库中不存在的数据,导致所有的请求都打到数据库上。如果请求很多,数据库依旧会挂得明明白白。</p><h3>3.1 业务场景举例</h3><ul><li>数据库主键 id 都是正数,然后客户端发起了 <code>id = -1</code> 的查询</li><li>一个查询接口,有一个状态字段 status,其实 0 表示开始、1 表示结束。结果有请求一直发 <code>status=3</code> 的请求过来</li></ul><h3>3.2 解决方案</h3><h4>3.2.1 方法一</h4><p>做好参数校验,对于不合理的参数要及时 return 结束</p><p>这点非常重点,做任何业务都一样,对于后端来说,要有<strong>互不信任原则</strong>。</p><p>简单来说,就是不要信任来自前端、客户端和上游服务的请求数据,该做的校验还是要做。</p><p>因为我们永远都不知道用户会写什么奇奇怪怪的数据;又或者即使你和对接的开发约定好了要怎么传参数,但你保不准他就没遵守呢;退一步来说,万一接口被破解呢。</p><p>你要保护好自己,不然到时出问题时,你和老大说,因为谁谁不遵守约定传参导致,或者因为没想到用户会这么填,你看看你老大会这么说(狗头.jpg)</p><h4>3.2.2 方法二</h4><p>对于查不到数据的 key,也将其短暂缓存起来。</p><p>比如 30s。这样能避免大量相同请求瞬间打到数据库上,减轻压力。</p><p>但是后面肯定要去看为什么会有这样的数据,从根本上解决问题,该方法只是缓解问题而已。</p><p>如果发现就是某些 ip 在请求,并且这些数据非法,那可以在网关层限制这些 ip 访问</p><h4>3.2.3 方法三</h4><p>提供一个能迅速判断请求是否有效的拦截机制,比如布隆过滤器,Redis 本身就具有这个功能。</p><p>让它维护所有合法的 key,如果请求参数不合法,则直接返回。否则就从缓存或数据库中获取。</p><p>关于布隆过滤器可以看我之前写的文章:<a href="https://link.segmentfault.com/?enc=%2BsygQDQStndWC%2B0E9%2FepFw%3D%3D.P8upLHEXxHwdqd6v3lDaCwIvHgooQVK7nygIjIcPnY3Hnpbbeid14rdr7WEzkqgu2QlhaUa0blF3z7nL9p2k6g%3D%3D" rel="nofollow">布隆过滤器</a></p><h2>4. 业务可靠性处理</h2><p>如开头所说,缓存指 Redis。</p><ul><li>提高 Redis 可用性:Redis 要么用集群架构,要么用主从 + 哨兵。保证 Redis 的可用性。</li></ul><p>没有哨兵的主从不能自动故障转移,所以只有主从,万一高峰期或者在关键的活动时间节点挂了。</p><p>那么等出现线上告警、定位问题、沟通信息、等运维解决,一套操作下来,估计黄花菜都凉了。</p><ul><li>减少对缓存的依赖</li></ul><p>对于热点数据,是不是可以考虑加上本地缓存,比如:Guava、Ehcache,更简单点,hashMap、List 什么也可以。</p><p>减少对 Redis 压力的同时,还能提高性能,一举两得。</p><ul><li>业务降级</li></ul><p>从保护下游(接口或数据库)的角度考虑,针对大流量场景是不是可以做下限流。这样即使缓存崩了,也不至于把下游全部拖垮。</p><p>以及该降级的功能是不是可以降级,提前写好降级开关和降级逻辑,关键时候全靠它稳住。</p><blockquote>原创不易,如果觉得文章不错,希望能关注下我的公号:<strong>七淅在学Java</strong></blockquote>
简历准备和面试技巧,你所应该知道的一切
https://segmentfault.com/a/1190000041485982
2022-03-02T21:47:44+08:00
2022-03-02T21:47:44+08:00
七淅在学Java
https://segmentfault.com/u/qixijava
6
<p>hello,大家好,我是七淅。</p><p>最近金三银四,借此机会和大家分享我在简历、投递策略和面试上的经验。</p><p>先介绍自己的情况,我本人的履历相当普通,普通本科毕业,毕业后去了一家小厂,3 个月后因寒冬被裁。</p><p>以这样的劣势开局,用了 2 年半时间成功进入某大厂。期间没少被各大中小公司虐过,幸好收获了一些经验。</p><p>我会把自己行之有效的准备方法、技巧和注意点都和大家说一说,这些内容其实不管是在面大厂还是小厂,都是通用的。</p><p>本文一共 6k+ 字,希望对大家有所帮助。</p><h2>1. 简历</h2><p>下面,我就开始说下第一块内容 —— 简历。这里我默认大家都写过这个东西,所以这里主要是说 7 点小建议。</p><h3>1.1 模板</h3><p>推荐大家选择简洁一点模板,不要太花哨。只要能够让 hr、面试官清楚、快速知道你的关键信息就可以了,太花哨容易分散别人注意力。</p><h3>1.2 格式</h3><p>简历一定要用 PDF,不要用 word 文档。因为不同电脑看 word 文档容易出现样式问题</p><h3>1.3 照片</h3><p>要不要贴照片的疑问,这应该是刚准备进职场的同学常见的问题。对技术岗来说,一般不贴照片。当然了,帅哥美女请随意。</p><h3>1.4 文件名</h3><p>关于简历的文件名格式,我个人是这样的 —— 姓名_学历_岗位_手机号。</p><p>坦白讲,尽管这个都快说烂了,至今来找我内推的小伙伴,也还有人把文件名写得相当随意,有的文件名直接写简历 2 个字,或者是姓名_岗位(1),这个括号 1 就有点秀,太不细心了。</p><p>说到这可能有的小伙伴会想,我名校毕业的,只写个学历会不会太亏了。</p><p>这里我想说 —— 确实有点,如果是这种情况,你可以自信写上 985 本或 211 本。如果你是清华北大等名校,你把学校名写上都行,这是你的优势,自信就可以了。</p><p>如果你学历不太好,又有一定工作年限,那也可以把工作年限代替学历那个位置。</p><h3>1.5 专业技能</h3><p>描述专业技能这块,如果只是看过几篇文章这种的,可以写了解。接着是熟悉和掌握,最后就是精通了。</p><p>之后,我的建议是慎重写精通。除非真的研究得很深入,否则都不建议写。</p><p>原因的话很好理解,是写精通的技能,除非这个专业技能在你面试的公司完全不用,否则面试时候一定是「关照」你的地方(嗯,这里的关照是加双引号的)。所以如果答不出来,或者回答得不是很好的话,就可能有点尴尬了。</p><h3>1.6 专业名词</h3><p>专业名词一定要写对,比如 MySQL,大小写要注意,如果你实在不确定,那可以去他们的官网,然后我们就知道它的正确写法了。</p><p>虽然这个写对写错完全不影响我们对这个知识的掌握,但从我接触的人来看,有人觉得不所谓,但也有人会在在意。那既然如此,我们也没必要去试踩这个雷了。</p><p>反正专业名词写对没有坏处,而写错可能有坏处,所以就没必要去赌这个雷是不是哑雷了。</p><h3>1.7 工作经历和项目经历</h3><p>这是最后一点,也是我认为简历中最重要一部分。</p><p>从我接触过的简历来看,很多人都会在工作经历这写一些大而全的描述。</p><p>什么「大而全」的描述呢?比如:参与 xxx 产品的日常开发和线上问题处理、负责 xxx 系统的开发和维护、实现了 xx 功能。</p><p>就这里大家有没有发现,这些内容一般都会出现在你的项目经历上吧,所以这里是不是就重复了呀。</p><p>因此我个人建议,在你的简历篇幅足够多的情况下,上述这些内容是可以不用写的。</p><p>大家都知道,我们简历一般都是 1-2 页,并且为了美观,内容能刚好占满这 1-2 页是最好了。</p><p>所以如果上面那些大而全的描述不写刚好能占满,写上反而要多一页了,那这种情况就可以不用写了。</p><p>进一步来说,我也更推荐大家去写新的简历内容,而不是去写重复内容。</p><p>以我自己来说,简历中工作经历的篇幅是特别少的。我只写公司名、所属行业、任职时间、岗位就没了。一家公司占 1 行内容,2、3 行就搞定了,把更多空间留给项目经历。</p><p>对于项目经历,这一块我的简历是由以下 4 部分组成的。分别是:项目名称、项目描述、涉及技术和负责内容。</p><p>如果你们也是这么写的话,那写在涉及技术里面的技术栈一定要好好地复习一下,毕竟写在那其实也是在告诉面试官 —— 你可以来问我这些技术点,我都用过的。</p><p>对于怎么写负责内容这一部分,应该是最让人头秃的地方了。</p><p>网上有人说用 star 法则来描述,就是在 xx 背景、你的任务是什么,最后通过 xx 方式达到了 xx 的效果。其实大家思路都是的。我是觉得这样要写的内容太多了,过于占用简历的篇幅,所以就提炼了一下。</p><p>这部分我是用以下结构来组织的 —— <strong>用 xx + yy 技术,实现了 xx 效果</strong>。</p><p>其中这个效果就最好有数据支撑。如果没有,那看看能不能用一些专业术语来描述,比如:流量削峰、稳定性、幂等之类的。</p><p>毕竟面试官不知道我们做的业务是什么,如果就只写做了什么功能,那面试官真不一定知道要问我们什么问题。</p><p>所以换位思考一下,用<strong>技术点 + 数据(若有) + 专业术语</strong>来给面试官一些提问的机会,同时也体现我们所做的技术价值。</p><p><strong>另外数据这个东西,假如实在没有就不用强求了</strong>,毕竟也没办法瞎编,而且有些技术内容的确是不会有数据的,比如用了分布式锁来防止重复处理,你说它有什么数据对比呢,是吧。</p><p>对于能用数据衡量的场景,我们才希望有数据会更好。</p><p>所以当你平时在做优化的时候,记得要记录下优化前后的数据对比。毕竟你要看优化有没有用,那肯定有个前后对比吧。</p><p>因此希望大家有在遇到可以有数据量化的场景,不要错过了。就算你不跳槽,以后写晋升材料的时候也是可以用上的。</p><p>那到了这里,道理我都懂,数据怎么来呢?之前没记录,已经错过了怎么办?</p><p>如果是这种情况,那大家可以试试有些数据能不能推算出来。</p><p>像从接口设计本身能知道的数据,比如:我们对一个高频接口做了请求合并的优化,程序在合并后一次最多只能处理 100 个请求,而该接口之前最高有 500 QPS。</p><p>那我们就可以说「xxx 功能使用请求合并,从 500QPS 降到 100 QPS」</p><p>再举个例子:「xx 功能使用了策略模式和工厂模式,提高了一定的开发效率」。这个提高效率其实是可以评估出来的,即用和不用设计模式做这个需求,分别需要多少人天,这样比起写「提高一定效率」会更加直观一点。</p><h2>2. 投递策略</h2><p>好了,那简历这一大内容我就说完了,下面我们来聊下投递策略,这里有 3 个小策略和大家分享下。</p><h3>2.1 先找练手公司,后目标公司</h3><p>第一个:先找 1、2 家公司练练手,找下面试感觉,接着才去投你想去的公司</p><p>PS:</p><ol><li>练手公司:拿了 offer 也不会去,或者是你一批目标公司中,比较垫底的</li><li>面试感觉:熟悉自我介绍和面试节奏、了解你的项目一般会被问什么问题、强化自己对基础原理的记忆和正式表达</li></ol><h3>2.2 根据个人情况,决定先后投哪些公司</h3><p>第二个:根据个人情况,决定先后投哪些公司。</p><p>比如字节和某厂都是我的目标公司,但我算法不太行,那我去面字节这种必考算法的公司,翻车的可能性就比较高。</p><p>那为了节省精力,可以把字节放到后面再投,先去面那些不考算法,或者考得不难的公司,这样上岸的可能性就会高一些。</p><p>那至于哪家公司喜欢考什么内容,这个就要发挥大家的人脉和搜索能力了。这里就涉及到第三点 —— 能内推就内推</p><h3>2.3 能内推就内推</h3><p>首先内推是一件双赢的事情,你面试成功通过了,内推人都会有奖励的,所以不用担心让别人内推自己会麻烦到他,别人巴不得有人找他内推。</p><p>至于内推的好处,像帮忙跟进度、反馈结果都是可以的。</p><p>以我目前的了解,面初中高级岗位,基本上猎头能做的事情,我们找内推的人一样也能做到。</p><p>如果是更高级的岗位,主管总监之类的,那这是我的知识盲区了,就不敢多说什么了。</p><p>那除了上面说的好处之外,我们还可以问问内推人这个部门氛围/业务怎样、leader 如何、面试有哪些常考的。有时甚至你都不用问,帮你内推的人都会主动和你说要准备什么东西,或者某某部门加班怎么样。</p><p>很多人都关心投的公司/部门氛围怎样,关于这点我自己是会去问人,或者去脉脉、看准网上看的评价。不过小厂的信息大概率非常少,甚至没有,毕竟人本来就不多,会出来发声的就更少了。</p><p>最后,如果你没有认识这家公司的人,但又想找人内推。那可以去一些求职软件上找,比如 boss、拉钩、脉脉这类,或者问问自己朋友有没认识的人。</p><p>如果还是没有,那去刚才说的那些招聘软件找猎头或 hr 帮推也是可以的。</p><p>最后对于实习和校招同学来说,推荐去牛客或脉脉上找人内推。</p><h2>3. 面试</h2><p>ok,我们接下来来说下最后一个大点 —— 面试。</p><h3>3.1 自我介绍</h3><p>首先是自我介绍,这个我个人认为是 1-2min 就差不多了,但如果是找实习和校招,本身能写的就不多,那这种情况特殊可以根据自己情况来哈。</p><p>在面试前,请一定要先熟悉自己的自我介绍,用<strong>面试的状态</strong>练习说几遍,在脑子想没用。</p><p>千万不要在面试时候还吞吞吐吐,说不清楚,这样真的很影响印象分。</p><p>并且自我介绍被打断的情况也是会发生的,不用慌,我自己面某大厂总监面时,因对方面试环境问题,就被打断了 3 次,所以这种情况就更要求熟悉自己的自我介绍了。</p><h3>3.2 技术面</h3><p>关于技术面,<strong>不管你去面小厂还是大厂,基础和项目都是要重点准备的</strong>,基本每轮面试都会问。</p><p>中大厂至少是 3 轮技术面,高职级岗位则会更多。</p><p>最后一轮通常是总监这类管理人员来面,问的问题也比较有差异。以我的经历来说,被问的内容有技术、项目、业务和聊人生都有。</p><p>前两轮都会问基础原理(所谓的八股文)、项目和场景设计,如果有考算法的话,那这里肯定也会让你写。</p><p>如果你是面管理岗的话,那自然会少问一点基础,多问一点业务、团队管理或者聊下人生。</p><p>这里和大家分享一些技巧和小建议,大家可以参考下,具体执行大家根据自身情况来:</p><h4>3.2.1 算法</h4><p>第一个,刚才说到了算法,关于算法,大家都知道去 leetcode 刷题。如果你时间有限,个人建议只刷剑指 offer 和热门前 100 道就好了,一共是 100 多道题。</p><p>还没完,接下来是学会放弃。<strong>因为时间有限,所以我们需要追求的是效率,要在越短时间掌握越多越有可能被考察的题目</strong>。所以</p><pre><code>- hard 难度可以忽略
- 解法只能用数学公式解决的忽略
- 题解代码量很多的忽略
- 看了好几遍题解都不知道在说什么的忽略
</code></pre><p>再强调下,上面都是<strong>为了面试,时间有限</strong>的应对方法。</p><p>如果你时间充足,除了上面说的剑指 offer 和热门前 100,你可以看你需要练习什么题型来选择性刷,leetcode 上都有标签分类,还是很方便的。</p><h4>3.2.2 场景设计</h4><p>第二个,关于场景设计题目。首先要明确,<strong>场景设计不一定都有最佳答案,因为脱离业务场景谈设计都是在耍流氓。这里主要是想考察我们解决问题的能力</strong>。</p><p>当大家被问到的时候,千万不要想着一下子拿出完美的方案,不现实,时间也不允许。</p><p>如果你是面试官,你单看着候选人 1、2min 不说一句话,你会怎么想?特别是电话面这种,别人还看不到你的脸,就更加不好判断了。</p><p>所以个人建议,你可以先思考一下,给出一个不完善的方案先,然后和面试官说:「我刚想了下可以这么做,但时间有限,可能有一些不合理的地方。然后 blablabla 就说想的方案了」</p><p>后面就是你和面试官一来一回的 pk 了,原本不完善的方案自然也在这个过程中被不断完善。<strong>先完成再完美</strong>。</p><p>当然,想得出方案,这是一种好的结果。还有一种是想不出,或者有多种实现方式但不知道选哪个的时候。</p><p>这种情况我建议是想到多少说多少,甚至只是实现了部分功能也没关系。</p><p>说完之后,不会的地方就坦诚和面试官说「还有 xx 部分,因为 xx 地方还没想到,可不可以给点提示」,不要不懂装懂。</p><p>不知道选哪种方案的情况也是一样,大胆说出来,包含你选择困难的原因。</p><p>而对于这块内容的准备,一方面来自于大家平时工作中的积累,另一方面就是网上的面经大家可以看一看。</p><p>校招生对这块要求就不会太高了,有的公司甚至都不会问,主要还是社招的同学需要注意。</p><p>像我自己遇到过的就有:</p><ul><li>短链服务怎么设计</li><li>动态怎么设计,比如微信朋友圈或微博</li><li>秒杀怎么设计</li><li>大流量场景下,服务撑不住了,可以怎么优化</li><li>RPC 系统怎么设计,有哪些关键地方</li></ul><p>还是那句话,重点考察解决问题的能力,方案不是最正确的也没关系。</p><p>毕竟我怎么可能知道微信朋友圈是怎么设计的,要是我光看产品功能就能懂别人一个团队做的事情,那岂不是要上天了。</p><h4>3.2.3 遇到不懂/不确定的题目</h4><p>第三个技巧是当我们被问到不懂,或者不确定的题目时可以怎么应对。</p><p>如果是没听清的话,可以让面试官复述一遍;</p><p>如果你不知道面试官在问什么的,我通常会反问回去,和他确定是不是想听 xx 方面的内容。 </p><p>比如面试官问说下线程池的原理,这个可以说构造参数含义和合理使用、线程怎么复用、线程池出现异常的处理等等。可以说很多东西,时间不太允许。</p><p>这时可以问他,「不好意思,你是指线程复用这块内容,还是 xxx 内容」。总之就是需要把问题给明确下来,给面试官做选择题,缩小问题范围。</p><p>当然了,如果你不会的点就不要说出这个选项啦。</p><p>之后对于不懂的知识点,我们可以迂回一点,尽量不要直接说不会,这直接就躺平了。</p><p>我们可以说「这个我不太了解,但我猜是这样的,blabla」,或者说「xxx 我没怎么了解,但我知道 yyy 和它差不多,也能做到 zzz 的目的」</p><p>像这样挽救一下,说不定面试官就顺你刚说的新东西去追问你呢,而这又是你熟悉的内容,它不香吗?</p><h4>3.2.4 项目</h4><p>ok,3 个技巧就说完了,剩下一个重点戏就是项目了,有 2 点我希望大家可以提前准备的,属于高频问题。</p><ol><li>梳理自己项目的难点或亮点是什么?</li><li>项目中,为什么用 xx 技术点,用 yy 的可以吗?或者为什么这么设计?</li></ol><p>关于第一点,这个内容即使面试官没问,我们也可以在自我介绍时候表述出来</p><p>如果你觉得自己的项目的确没什么厉害的东西,都是业务的 curd。那就挑一个值得说过的优化,或者设计方案也行。</p><p>毕竟高大上的东西的确只有少数人接触到,都是理解的。</p><p>接下来关于第二点,这是我经常被问的一个问题,目的是考察对自己项目的理解是不是真的知其所以然,还是说自己只是一个无情的 curd 机器。</p><h4>3.2.5 惯例</h4><p>按照惯例,当面试结束时,面试官一般都会问我们还有什么问题想问没。</p><p>我不知道大家都准备了什么问题哈,我自己目前会有下面几个回答。这个大家根据自身情况,<strong>仅供参考就好,肯定随着经历、面试情况的不同而不同的</strong>。</p><ol><li>如果 jd 没写部门或业务,我会问这个岗位具体是做什么业务的,团队规模多少人</li><li>就刚才的面试,你觉得我还有哪些地方需要加强一下</li><li>团队氛围怎么样,平时上下班是几点</li><li>没有了,谢谢(狗头保命)</li></ol><h4>3.2.6 不懂的问题要去解决</h4><p>最后这里补充一下,如果面试时你遇到了不懂,或者回答得不好的问题,记得面试结束后要去弄懂它,万一下次面试又被重复问到了呢?我自己是有这样的经历过。</p><h3>3.3 hr 面</h3><p>如果技术面都通过,最后到了 hr 面,基本就是聊人生、对方公司情况和谈薪资。</p><p>薪资这块,行业一般就是卡涨幅 30%。</p><p>但这不是绝对的,特别是对于 base 低的朋友,如果你的薪资在这家公司就算涨 30% 也够不着对方的下限,那没必要委屈自己,按照对方公司的薪资水平来提就好了。</p><p>如果你觉得对方给的不够,并且你还有其他 offer。那你完全可以说已经有了其他 offer,对方给了 xxx,但自己更想来贵公司,希望薪资可以给到 yyy。</p><p>另外,如果这家公司是你的目标公司之一,那也请关心下和自己利益相关的东西。比如:公司的薪酬结构、上下班和午休时间、公积金基础和比例等等。</p><p>以上都是些基本信息,如果你有多个 offer 的情况,这些都是你做选择的依据。像年终奖这些浮动的东西,记得看保底水平,而不是平均水平。</p><p>因为我们需要管理自己的心理预期,不然你接了 offer,到时又没拿到这么多,就非常影响自己的心态了。</p><h2>4. 最后唠叨</h2><p>面试成功与否其实就是实力 + 运气。</p><p>像公司有没有开放 hc、面试官面试那天心情好不好、问的问题是不是都是自己刚准备的。</p><p>这些都是运气,我们改变不了。我们能改变的只有实力,实力越高,运气的影响程度越低。</p><p>最后,希望大家都能顺利拿到自己理想的 offer,谢谢。</p><blockquote>“本文参与了 SegmentFault 思否征文「<a href="https://segmentfault.com/a/1190000041558580">如何“反杀”面试官?</a>」,欢迎正在阅读的你也加入。”</blockquote>
你应该知道的Redis事务
https://segmentfault.com/a/1190000018066133
2019-01-30T12:39:38+08:00
2019-01-30T12:39:38+08:00
七淅在学Java
https://segmentfault.com/u/qixijava
8
<p>前两篇 Redis 文章都大几千字,今天我们换个小清新点的,篇幅不多也容易理解。</p><p>如果你也了解过关系型数据库事务的话,相信这篇文章对你来说是很容易理解的了。具体什么是事务我就不说不多了,直接讲 Redis 事务相关的部分。</p><p>首先,我们先来看下,Redis 是怎么执行事务的。</p><h2>1. 事务执行过程</h2><p>show code:</p><pre><code>127.0.0.1:6379> multi
OK
127.0.0.1:6379> set a test1
QUEUED
127.0.0.1:6379> set b test2
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK
127.0.0.1:6379> get a
"test1"
127.0.0.1:6379> get b
"test2"</code></pre><p>一个事务的开始到结束会经过以下 3 个过程</p><ol><li>事务开始</li><li>命令入队</li><li>事务执行</li></ol><p>结合上面的例子,用人话介绍这 3 个过程就是:</p><p>Redis 执行 multi 命令标志事务开始。</p><p>当客户端切换至事务状态后,服务端会将除了 exec、discard(取消事务,放弃执行事务块内的所有命令)、watch 和 multi 以外的命令放进一个先进先出的事务队列中。即上面例子的 2 个 set 命令会被放进队列,并返回 QUEUED 给客户端。</p><p>当客户端发送 exec 命令时,服务端会立即执行该命令。遍历这个客户端的事务队列,执行队列保存的所有命令。最后将执行命令所得结果返回给客户端。</p><h2>2. Redis 事务和关系型事务的区别</h2><p>两者最大区别就是 <strong>Redis 事务不支持回滚</strong>。即使事务队列中某个命令在执行期间发生了错误,事务也会继续执行,直到事务队列中所有命令执行完成。</p><p>文字貌似不够直观,没事,看下面的例子你就马上明白了。</p><pre><code>127.0.0.1:6379> multi
OK
127.0.0.1:6379> set msg test
QUEUED
127.0.0.1:6379> lpop msg
QUEUED
127.0.0.1:6379> exec
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
127.0.0.1:6379> get msg
"test" // 不受后面错误命令影响</code></pre><p>PS:如果客户端向事务入列一个错误的命令(比如输入一个不存在的命令,像:sett 命令),那么该事务将不被服务端执行。该情况是入队错误,上面例子是执行错误的情况。</p><h2>3. watch</h2><p>提到 redis 事务,就不得不提 watch 命令了。</p><p>该命令是一个乐观锁,只能在客户端进入事务状态之前执行。</p><p>作用是 exec 命令执行之前,监视任何数量个键,并在 exec 命令执行时,检查被监视的键是否至少有一个已经被修改过。若是则拒绝执行事务,否则执行。</p><p>当 exec 执行完成后,这次事务也就结束了。</p><p>我们依旧来看一个简单的栗子:</p><pre><code>127.0.0.1:6379> SET msg test //设置 msg 的值
OK
127.0.0.1:6379> WATCH msg //监视 msg
OK
127.0.0.1:6379> SET msg test2 //修改 msg 的值(或其他客户端在该客户端执行 exec 命令之前修改该值)
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET msg test3
QUEUED
127.0.0.1:6379> GET msg
QUEUED
127.0.0.1:6379> EXEC
(nil) //执行失败</code></pre><blockquote>如果觉得文章不错,希望能得到你的关注:<a href="https://link.segmentfault.com/?enc=slWx90L9sUgJ2t%2B0UT8YXw%3D%3D.NMfDKdQ%2BVVqXI4tdfl5xPnzLcpUKYVKlJrEmIvAgTUeHJRsvqQ%2Bz2XGr6O6uTyi%2F" rel="nofollow">七淅在学Java</a></blockquote>
一文让你明白Redis主从同步
https://segmentfault.com/a/1190000017993362
2019-01-23T13:05:20+08:00
2019-01-23T13:05:20+08:00
七淅在学Java
https://segmentfault.com/u/qixijava
17
<p>今天想和大家分享有关 Redis 主从同步(也称「复制」)的内容。</p><p>我们知道,当有多台 Redis 服务器时,肯定就有一台主服务器和多台从服务器。一般来说,主服务器进行写操作,从服务器进行读操作。</p><p>那么这里有存在一个问题:从服务器如何和主服务器进行数据同步的呢?</p><p>这个问题,就是通过今天的内容:主从同步来解决的。</p><p>文章内容依旧比较干,建议大家静下心来专心看,文末会给大家做个简单总结归纳。</p><h2>1. 如何进行主从同步</h2><p>假如,现在有 2 台 Redis 服务器,地址分别是 127.0.0.1:6379 和 127.0.0.1:12345</p><p>我们在 127.0.0.1:12345 的客户端输入命令:</p><pre><code>127.0.0.1:12345> SLAVEOF 127.0.0.6379</code></pre><p>如此 127.0.0.1:12345 服务器就会去复制 127.0.0.1:6379 的数据。即前者是从服务器,后者为主服务器。</p><p>除了以上方式进行复制之外,还可以通过配置文件中的 slaveof 选项进行设置。</p><p>可能,求知欲爆棚的你会想知道,Redis 是怎么进行主从同步的?</p><p>ok,下面我们继续了解一下。</p><h2>2. 主从同步的实现过程</h2><p><strong>主从同步分为 2 个步骤:同步和命令传播</strong></p><ul><li>同步:将从服务器的数据库状态更新成主服务器当前的数据库状态。(数据库状态在这篇文章开头有提到是什么意思,不清楚的小伙伴可以先看下:《持久化》)</li><li>命令传播:当主服务器数据库状态被修改后,导致主从服务器数据库状态不一致,此时需要让主从数据同步到一致的过程。</li></ul><p>上面就是主从同步 2 个步骤的作用,下面我打算稍微细说这两个步骤的实现过程。</p><p>这里需要提前说明一下:在 Redis 2.8 版本之前,进行主从复制时一定会顺序执行上述两个步骤,而从 2.8 开始则可能只需要执行命令传播即可。在下文也会解释为什么会这样?</p><h3>2.1 同步</h3><p>从服务器对主服务的同步操作,需要通过 sync 命令来实现,以下是 sync 命令的执行步骤:</p><ol><li>从服务器向主服务器发送 sync 命令</li><li>收到 sync 命令后,主服务器执行 bgsave 命令,用来生成 rdb 文件,并在一个缓冲区中记录从现在开始执行的写命令。</li><li>bgsave 执行完成后,将生成的 rdb 文件发送给从服务器,用来给从服务器更新数据</li><li>主服务器再将缓冲区记录的写命令发送给从服务器,从服务器执行完这些写命令后,此时的数据库状态便和主服务器一致了。</li></ol><p>用图表示就是这样的:</p><p><img src="/img/remote/1460000017993365" alt="image" title="image"></p><h3>2.2 命令传播</h3><p>经过同步操作,此时主从的数据库状态其实已经一致了,但这种一致的状态的并不是一成不变的。</p><p>在完成同步之后,也许主服务器马上就接受到了新的写命令,执行完该命令后,主从的数据库状态又不一致。</p><p><strong>为了再次让主从数据库状态一致,主服务器就需要向从服务器执行命令传播操作 ,即把刚才造成不一致的写命令,发送给从服务器去执行。从服务器执行完成之后,主从数据库状态就又恢复一致了</strong>。</p><p>这里插播一个疑问: </p><p>不知道有没有的读者觉得,当发生上述不一致的情况后,Redis 再执行同步操作不就 ok 了吗?</p><p>从效果上来说,的确是可以恢复同步,但其实没有必要。原因是实现同步的 sync 命令是一个非常消耗资源的操作,看完下图的说明,相信你肯定理解的。</p><p><img src="/img/remote/1460000017993366" alt="image" title="image"></p><p>既然同步是一个非常消耗资源的操作,那 Redis 有没有什么优化方法呢?答案当然是有的。</p><h3>2.3 优化版同步操作</h3><p>还记得上面说的内容吗 —— 2.8 版本开始,进行主从同步可能只需要执行命令传播即可。这个也是因为 sync 比较耗资源,从而采取的优化。</p><p>那什么时候可以这么做呢?我们先看下前提条件:</p><p>主从同步实际分 2 种情况:</p><ul><li>初次复制:从服务器第一次复制当前主服务器(PS:主服务器是有可能更换的)</li><li>断线后重复制:处于命令传播阶段的主从服务器,因为网络问题而中断复制,从服务器通过自动重连,重新连接上主服务器并继续复制。</li></ul><p>在断线后重复制的情况下,在 2.8 版本之前,会再次执行同步(sync 命令)和命令传播。</p><p>如果说,在断线期间,主服务器(已有上万键值对)只执行了几个写命令,为了让从服务器弥补这几个命令,却要重新执行 sync 来生成新的 rdb 文件,这也是非常低效的。</p><p>为了解决这个问题,2.8 开始就使用 psync 命令来代替 sync 命令去执行同步操作。</p><p><strong>psync 具有完整重同步和部分重同步两种模式</strong>:</p><ul><li>完整重同步:用于初次复制情况,执行过程同 sync,在这不赘述了。</li><li>部分重同步:用于断线后重复制情况,如果满足一定条件,主服务器只需要将断线期间执行的写命令发送给从服务器即可。</li></ul><p>因此很明显,当主从同步出现断线后重复制的情况,psync 的部分重同步模式可以解决 sync 的低效情况。</p><p>上面的介绍中,出现了「满足一定条件」,那又是鬼什么条件呢?—— 其实就是一个偏移量的比较,具体可以继续往下看。</p><h3>2.4 部分重同步的实现</h3><p>部分重同步功能由以下 3 部分组成:</p><ul><li>主从服务器的复制偏移量</li><li>主服务器的复制积压缓冲区</li><li>服务器的运行 id(run id)</li></ul><h4>2.4.1 复制偏移量</h4><p>执行复制的主从服务器都会分别维护各自的复制偏移量:</p><ul><li>主服务器每次向从服务器传播 n 个字节数据时,都会将自己的复制偏移量加 n。</li><li>从服务器接受主服务器传来的数据时,也会将自己的复制偏移量加 n</li></ul><p>举个例子:</p><p>若当前主服务器的复制偏移量为 10000,此时向从服务器传播 30 个字节数据,结束后复制偏移量为 10030。</p><p>这时,从服务器还没接收这 30 个字节数据就断线了,然后重新连接上之后,该从服务器的复制偏移量依旧为 10000,说明主从数据不一致,此时会向主服务器发送 psync 命令。</p><p>那么主服务器应该对从服务器执行完整重同步还是部分重同步呢?如果执行部分重同步的话,主服务器又如何知道同步哪些数据给从服务器呢?</p><p>以下答案都和复制积压缓冲区有关</p><h4>2.4.2 复制积压缓冲区</h4><p>首先,复制积压缓冲区是一个固定长度,先进先出的队列,默认 1MB。</p><p>当主服务器进行命令传播时,不仅会将命令发送给从服务器,还会发送给这个缓冲区。</p><p>因此复制积压缓冲区的构造是这样的:</p><p><img src="/img/remote/1460000017993367" alt="image" title="image"></p><p>当从服务器向主服务器发送 psync 命令时,还需要将自己的复制偏移量带上,主服务器就可以通过这个复制偏移量和复制积压缓冲区的偏移量进行对比。</p><p><strong>若复制积压缓冲区存在从服务器的复制偏移量 + 1 后的数据,则进行部分重同步,否则进行完整重同步</strong>。</p><h4>2.4.3 run id</h4><p>运行 id 是在进行初次复制时,主服务器将会将自己的运行 id 发送给从服务器,让其保存起来。</p><p>当从服务器断线重连后,从服务器会将这个运行 id 发送给刚连接上的主服务器。</p><p>若当前服务器的运行 id 与之相同,说明从服务器断线前复制的服务器就是当前服务器,主服务器可以尝试执行部分同步;若不同则说明从服务器断线前复制的服务器不是当前服务器,主服务器直接执行完整重同步。</p><p>花了很多笔墨,终于把部分重同步的实现写完了,最后补充一个辅助功能</p><h3>2.5 心跳检测</h3><p>刚才提到,主从同步有同步和命令传播 2 个步骤。</p><p>当完成了同步之后,主从服务器就会进入命令传播阶段,此时从服务器会以每秒 1 次的频率,向主服务器发送命令:<code>REPLCONF ACK <replication_offset></code> 其中 replication_offset 是从服务器当前的复制偏移量</p><p>发送这个命令主要有三个作用:</p><ul><li>检测主从服务器的网络状态</li><li>辅助实现 min-slaves 选项</li><li>检测命令丢失(若丢失,主服务器会将丢失的写命令重新发给从服务器)</li></ul><h2>3. 总结</h2><p>终于写完了最后内容,几个小时又过去了,我们来总结下本文内容吧:</p><ul><li>发送 SLAVEOF 命令可以进行主从同步,比如:SLAVEOF 127.0.0.6379</li><li><p>主从同步有同步和命令传播 2 个步骤。</p><ul><li>同步:将从服务器的数据库状态更新成主服务器当前的数据库状态(一个消耗资源的操作)</li><li>命令传播:当主服务器数据库状态被修改后,导致主从服务器数据库状态不一致,此时需要让主从数据同步到一致的过程</li></ul></li><li><p>主从同步分初次复制和断线后重复制两种情况</p><ul><li>从 2.8 版本开始,在出现断线后重复制情况时,主服务器会根据复制偏移量、复制积压缓冲区和 run id,来确定执行完整重同步还是部分重同步</li></ul></li><li>2.8 版本使用 psync 命令来代替 sync 命令去执行同步操作。目的是为了解决同步(sync 命令)的低效操作</li></ul><blockquote>如果觉得文章不错,希望能得到你的关注:<a href="https://link.segmentfault.com/?enc=tU4uRUa4PDwWmQcw%2BA%2FHvQ%3D%3D.vCMGjVebKDSTSWypDeyMAE8i4k2bFoUfwvuPo9nhe%2BNW%2B34vR78kV71AlwidRoAg" rel="nofollow">七淅在学Java</a></blockquote>
一文让你明白Redis持久化
https://segmentfault.com/a/1190000017948297
2019-01-19T10:08:19+08:00
2019-01-19T10:08:19+08:00
七淅在学Java
https://segmentfault.com/u/qixijava
42
<p>网上虽然已经有很多类似的介绍了,但我还是自己总结归纳了一下,自认为内容和细节都是比较齐全的。</p><p>文章篇幅有 4k 多字,货有点干,断断续续写了好几天,希望对大家有帮助。不出意外地话,今后会陆续更新 Redis 相关的文章,和大家一起学习。</p><p>好了,下面开始回归正文:</p><p>Redis 一共有 2 种持久化方式,分别是 RDB 和 AOF,下面我来详细介绍两种方式在各个过程所做的事情,特点等等。</p><h2>1. RDB 持久化</h2><p>RDB 持久化是 Redis 默认的持久化方式。</p><p>它所生成的 RDB 文件是一个压缩的二进制文件,通过该文件可以还原生成 RDB 文件时的数据库状态</p><p>PS:数据库状态是指 Redis 服务器的非空数据库以及他们键值对的统称</p><h3>1.1 RDB 文件的创建</h3><p>有两个命令可以生成 RDB 文件,一个是 SAVE、另一个是 BGSAVE。</p><p>两者的区别在于:前者会阻塞 Redis 服务器进程,直到 RDB 文件创建完毕为止。 </p><p>而在服务器进程阻塞期间,服务器是不能处理任何命令请求的。</p><p>后者则不会阻塞服务器进程,因为是通过 fork 一个子进程,并让其去创建 RDB 文件,而服务器进程(父进程)继续则继续处理命令请求。</p><p>当写完数据库状态后,新 RDB 文件就会<strong>原子地</strong>替换旧的 RDB 文件。</p><blockquote><p>此处小提问:如果在执行 BGSAVE 期间,客户端发送 SAVE、BGSAVE 或 BGREWRITEAOF 命令给服务端,服务端会如何处理呢?</p><p>答案:在执行 BGSAVE 期间,上述三个命令都不会被执行。 </p><p>详细原因:前两个会被直接拒绝,原因是为了避免父子进程同时执行两个 rdbSave 调用,防止产生竞争条件。 <br>而 BGREWRITEAOF 命令则是会被延迟到 BGSAVE 命令执行之后再执行。 <br>但如果是 BGREWRITEAOF 命令正在执行,此时客户端发送 BGSAVE 命令则会被拒绝。 <br>因为 BGREWRITEAOF 和 BGSAVE 都是由子进程执行的,所以在操作方面没有冲突的地方,不能同时执行的原因是性能上的考虑——并发出两个子进程,并且这两个子进程都会同时执行大量 io(磁盘写入)操作</p></blockquote><h3>1.2 RDB 文件的载入</h3><p>RDB 文件的载入是在服务器启动时自动执行的,所以没有用于载入的命令,期间阻塞主进程。</p><p><strong>只要没有开启 AOF 持久化功能</strong>,在启动时检测到有 RDB 文件,就会自动载入。</p><p>当服务器有开启 AOF 持久化功能时,服务器将会优先使用 AOF 文件来还原数据库状态。原因是 AOF 文件的更新频率通常比 RDB 文件的更新频率高。</p><h3>1.3 自动间隔性保存</h3><p>对于 RDB 持久化而言,我们一般都会使用 BGSAVE 来持久化,毕竟它不会阻塞服务器进程。</p><p>在 Redis 的配置文件,有提供设置服务器每隔多久时间来执行 BGSAVE 命令。</p><p>Redis 默认是如下配置: <br>save 900 1 // 900 秒内,对数据库至少修改 1 次。下面同理 <br>save 300 10 <br>save 60 10000 </p><p>只要满足其中一种情况,服务器就会执行 BGSAVE 命令。</p><h2>2. AOF 持久化</h2><p>我们从上面的介绍知道,<strong>RDB 持久化通过保存数据库状态来持久化。而 AOF 与之不同,它是通过保存对数据库的写命令来记录数据库状态</strong>。</p><p>比如执行了 <code>set key 123</code>,Redis 就会将这条写命令保存到 AOF 文件中。</p><p>在服务器下次启动时,就可以通过载入和执行 AOF 文件中保存的命令,来还原服务器关闭前的数据库状态了。</p><p>总体流程和 RDB 持久化一样 —— 都是创建一个 xxx 文件、在服务器下次启动时就载入这个文件来还原数据</p><p>那么,AOF 持久化具体是怎么实现的呢?</p><h3>2.1 AOF 持久化实现</h3><p>AOF 持久化功能的实现可以分为 3 个步骤:命令追加、文件写入、文件同步</p><p>其中命令追加很好理解,就是将写命令追加到 AOF 缓冲区的末尾。</p><p>那文件写入和文件同步怎么理解呢?刚开始我也一脸懵逼,终于在网上找到了答案,参考见文末,有兴趣的读者可以去看看。</p><p>先不卖关子了,简单一句话解释就是:<strong>前者是缓冲区内容写到 AOF 文件,后者是将 AOF 文件保存到磁盘</strong>。</p><p>ok,明白什么意思之后,我们稍微详细看下这两个东西是什么鬼。</p><p>在《Redis设计与实现》中提到,Redis 服务器进程就是一个事件循环,这个循环中的文件事件(socket 的可读可写事件)负责接收客户端的命令请求,以及向客户端发送命令结果。</p><p>因为服务器在处理文件事件时,可能会发生写操作,使得一些内容会被追加到 AOF 缓冲区末尾。所以,在服务器每次结束一个事件循环之前 ,都会调用 flushAppendOnlyFile 方法。</p><p>这个方法执行以下两个工作:</p><ul><li>WRITE:根据条件,将缓冲区内容写入到 AOF 文件。</li><li>SAVE:根据条件,调用 fsync 或 fdatasync 函数,将 AOF 文件保存到磁盘中。</li></ul><p>两个步骤都需要根据一定的条件来执行,而这些条件由 Redis 配置文件中的 appendfsync 选项来决定的,一共有三个选择:</p><ol><li>appendfsync always:每执行一个命令保存一次</li><li>appendfsync everysec(默认,推荐):每一秒钟保存一次</li><li>appendfsync no:不保存</li></ol><p>下面说下三个的区别:</p><ul><li>appendfsync always:每次执行完一个命令之后, WRITE 和 SAVE 都会被执行</li><li>appendfsync everysec:SAVE 原则上每隔一秒钟就会执行一次。</li><li><p>appendfsync no:每次执行完一个命令之后, WRITE 会执行,SAVE 都会被忽略,只会在以下任意一种情况中被执行:</p><ul><li>Redis 被关闭</li><li>AOF 功能被关闭</li><li>系统的写缓存被刷新(可能是缓存已经被写满,或者定期保存操作被执行。完成依赖 OS 的写入,一般为 30 秒左右一次)</li></ul></li></ul><p>而对于操作特性来分析的话,则是如下情况:</p><table><thead><tr><th>模式</th><th>WRITE 是否阻塞主进程</th><th>SAVE 是否阻塞主进程</th><th>停机时丢失的数据量</th></tr></thead><tbody><tr><td>appendfsync always</td><td>阻塞</td><td>阻塞</td><td>最多只丢失一个命令的数据</td></tr><tr><td>appendfsync everysec</td><td>阻塞</td><td>不阻塞</td><td>一般情况下不超过 2 秒钟的数据</td></tr><tr><td>appendfsync no</td><td>阻塞</td><td>阻塞</td><td>操作系统最后一次对 AOF 文件触发 SAVE 操作之后的数据</td></tr></tbody></table><p>既然 AOF 持久化是通过保存写命令到文件的,那随着时间的推移,这个 AOF 文件记录的内容就越来越多,文件体积也就越来越大,对其进行数据还原的时间也就越来越久。</p><p>针对这个问题,Redis 提供了 AOF 文件重写功能。</p><h3>2.2 AOF 重写</h3><p>通过该功能来创建一个新的 AOF 文件来代替旧文件。并且两个文件所保存的数据库状态一样,但新文件不会包含任何冗余命令,所以新文件要比旧文件小得多。</p><p>而为什么新文件不会包含任何冗余命令呢?</p><p>那是因为这个重写功能是通过读取服务器当前的数据库状态来实现的。虽然叫做「重写」,但实际上并没有对旧文件进行任何读取修改。</p><p>比如旧文件保存了对某个 key 有 4 个 set 命令,经过重写之后,新文件只会记录最后一次对该 key 的 set 命令。因此说新文件不会包含任何冗余命令</p><p>因为重写涉及到大量 IO 操作,所以 Redis 是用子进程来实现这个功能的,否则将会阻塞主进程。该子进程拥有父进程的数据副本,可以避免在使用锁的情况下,保证数据的安全性。</p><p>那么这里又会存在一个问题,子进程在重写过程中,服务器还在继续处理命令请求,新命令可能会对数据库进行修改,这会导致<strong>当前数据库状态和重写后的 AOF 文件,所保存的数据库状态不一致</strong>。</p><p>为了解决这个问题,Redis 设置了一个 AOF 重写缓冲区。在子进程执行 AOF 重写期间,主进程需要执行以下三个步骤:</p><ol><li>执行客户端的请求命令</li><li>将执行后的写命令追加到 AOF 缓冲区</li><li>将执行后的写命令追加到 AOF 重写缓冲区</li></ol><p>当子进程结束重写后,会向主进程发送一个信号,主进程接收到之后会调用信号处理函数执行以下步骤:</p><ol><li>将 AOF 重写缓冲区内容写入新的 AOF 文件中。此时新文件所保存的数据库状态就和当前数据库状态一致了</li><li>对新文件进行改名,<strong>原子地</strong>覆盖现有 AOF 文件,完成新旧文件的替换。</li></ol><p>当函数执行完成后,主进程就继续处理客户端命令。</p><p>因此,<strong>在整个 AOF 重写过程中,只有在执行信号处理函数时才会阻塞主进程,其他时候都不会阻塞</strong>。</p><h2>3. 选择持久化方案的官方建议</h2><p>到目前为止,Redis 的两种持久化方式就介绍得差不多了。可能你会有疑惑,在实际项目中,我到底要选择哪种持久化方案呢?下面,我贴下官方建议:</p><p>通常,<strong>如果你要想提供很高的数据保障性,那么建议你同时使用两种持久化方式。如果你可以接受灾难带来的几分钟的数据丢失,那么你可以仅使用 RDB</strong>。</p><p>很多用户仅使用了 AOF,但是我们建议,既然 RDB 可以时不时的给数据做个完整的快照,并且提供更快的重启,所以最好还是也使用 RDB。</p><p>在数据恢复方面: <br>RDB 的启动时间会更短,原因有两个:</p><ol><li>RDB 文件中每一条数据只有一条记录,不会像 AOF 日志那样可能有一条数据的多次操作记录。所以每条数据只需要写一次就行了。</li><li>RDB 文件的存储格式和 Redis 数据在内存中的编码格式是一致的,不需要再进行数据编码工作,所以在 CPU 消耗上要远小于 AOF 日志的加载。</li></ol><p>注意: </p><p>上面说了 RDB 快照的持久化,需要注意:在进行快照的时候(save),fork 出来进行 dump 操作的<strong>子进程会占用与父进程一样的内存</strong>,真正的 copy-on-write,对性能的影响和内存的耗用都是比较大的。比如机器 8G 内存,Redis 已经使用了 6G 内存,这时 save 的话会再生成 6G,变成 12G,大于系统的 8G。这时候会发生交换;要是虚拟内存不够则会崩溃,导致数据丢失。所以在用 redis 的时候一定对系统内存做好容量规划。 </p><p>目前,通常的设计思路是利用复制(Replication)机制来弥补 aof、snapshot 性能上的不足,达到了数据可持久化。即 Master 上 Snapshot 和 AOF 都不做,来保证 Master 的读写性能,而 Slave 上则同时开启 Snapshot 和 AOF 来进行持久化,保证数据的安全性。</p><h2>总结</h2><p>文章知识点有点多和杂,我总结一下,帮助他们回顾内容:</p><ul><li>RDB 持久化是 Redis 默认持久化方式,通过保存数据库键值对来记录状态来持久化,由 SAVE 和 BGSAVE 命令来创建 RDB 文件。前者阻塞 Redis 主进程,后者不会。</li><li>RDB 可以在配置文件设置每隔多久时间来执行 BGSAVE 命令</li><li>AOF 通过追加写命令来保存当前数据库状态。其持久化功能的实现可以分为 3 个步骤:命令追加(到 AOF 缓冲区)、文件写入(缓冲区内容写到 AOF 文件)、文件同步(AOF 文件保存磁盘)</li><li>其中文件同步和保存可以通过配置文件的 appendfsync 选项来决定</li><li>为了解决 AOF 文件越来越大的问题,Redis 提供了 AOF 重写功能,并且不会阻塞主进程。</li><li>为了解决 AOF 重写过程中,新 AOF 文件所保存的数据库状态和当前数据库状态可能不一致的问题,Redis 引入了 AOF 重写缓冲区,用于保存子进程在重写 AOF 文件期间产生的新的写命令。</li><li>最后是官方对于两种持久化方式选择的一些建议</li></ul><blockquote>参考: <br>《redis设计与实现》<br><a href="https://link.segmentfault.com/?enc=ScgLmBjVqX1EWETAXRMgiQ%3D%3D.Bd5Ct5jvZImLsTHNerZJq1fF8xDvBx8JmHtlOJ3eynu3SY%2FIDik2MwqSOJ%2F8oU%2BM51kj%2Fso%2F%2BLU1dHFVcRknHfRfCffy%2Fdgh69wpUrQSYnQ%3D" rel="nofollow">https://www.cnblogs.com/zhouj...</a> <br><a href="https://link.segmentfault.com/?enc=zExmElPf4Ow3plikmNQKdQ%3D%3D.xwe%2BbrhskFpJ0mClx7TnTOcnt%2B5aWa01qB%2FEzHeP65s0iI6myl8hXg54kkvHuRZJRfxVfD0TXAFT0hLPeO7fow%3D%3D" rel="nofollow">https://redisbook.readthedocs...</a></blockquote><blockquote>如果觉得文章不错,希望能得到你的关注:<a href="https://link.segmentfault.com/?enc=4P7n6fW79VzgZIRQDpU5mg%3D%3D.8iJVebS8BSzYTJh78KXWYjpMBQuBt0dd6FUb6UVfmGkwOFnTsqgv3CRbMAd9ocKQ" rel="nofollow">七淅在学Java</a></blockquote>
你应该知道的Redis过期键和过期策略
https://segmentfault.com/a/1190000017776475
2019-01-05T17:08:58+08:00
2019-01-05T17:08:58+08:00
七淅在学Java
https://segmentfault.com/u/qixijava
2
<p>今天,我和大家分享一篇关于 Redis 有关过期键的内容,主要有四个内容:</p><ol><li>如何设置过期键</li><li>如何取消设置的过期时间</li><li>过期键的过期策略是怎样的</li><li>RDB、AOF 和复制对过期键的处理又是怎样的</li></ol><h2>设置键的生存时间或过期时间</h2><p>redis 一共有 4 个命令来设置键的生存时间(可以存活多久)或过期时间(什么时候被删除)</p><ul><li>expire <key> <ttl>:将 key 的生存时间设置为 ttl 秒</li><li>pexpire <key> <ttl>:将 key 的生存时间设置为 ttl 毫秒</li><li>expireat <key> <timestamp>:将 key 的过期时间设置为 timestamp 所指定的秒数时间戳</li><li>pexpireat <key> <ttl>:将 key 的过期时间设置为 timestamp 所指定的毫秒数时间戳</li></ul><p>上述四种命令本质上都是通过 pexpireat 命令来实现的。</p><pre><code>例子:
127.0.0.1:6379> set a test
OK
127.0.0.1:6379> EXPIRE a 5
(integer) 1
127.0.0.1:6379> get a // 距离设置生存时间命令的 5 秒内执行
"test"
127.0.0.1:6379> get a // 距离设置生存时间命令的 5 秒后执行
(nil)
127.0.0.1:6379> set b 12
OK
127.0.0.1:6379> EXPIREAT b 1545569500
(integer) 1
127.0.0.1:6379> time
1) "1545569486"
2) "108616"
127.0.0.1:6379> get b // 距离设置 1545569500 所指定的秒数时间戳内执行
"12"
127.0.0.1:6379> time
1) "1545569506"
2) "208567"
127.0.0.1:6379> get b // 距离设置 1545569500 所指定的秒数时间戳后执行
(nil)</code></pre><p>如果自己不小心设置错了过期时间,那么我们可以删除先前的过期时间</p><h2>移除过期时间</h2><p>persist <key> 命令可以移除一个键的过期时间,举个栗子:</p><pre><code>127.0.0.1:6379> EXPIRE c 1000
(integer) 1
127.0.0.1:6379> ttl c // 有过期时间
(integer) 9996
127.0.0.1:6379> PERSIST c
(integer) 1
127.0.0.1:6379> ttl c // 无过期时间
(integer) -1
PS:ttl 是以秒为单位,返回键的剩余生存时间;同理还有 pttl 命令是以毫秒为单位,返回键的剩余生存时间</code></pre><p>此时,如果我们没有移除过期时间,那么如果一个键过期了,那它什么时候会被删除呢?</p><p>这个问题就会有以下三种答案了,它们分别代表三种不同的删除策略</p><h2>过期键的删除策略</h2><h3>定时删除</h3><p>在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除操作。</p><p>优点:对内存最友好的。可以及时释放键所占用的内存。</p><p>缺点:对 CPU 不友好。特别在过期键比较多的情况下,删除过期键会占用相当一部分 CPU 时间。同时在内存不紧张,CPU 紧张的情况下,将 CPU 用在删除和当前任务不想关的过期键上,无疑会对服务器响应时间和吞吐量造成影响。</p><h3>惰性删除</h3><p>放任键过期不管,但是每次从键空间中读写键时,都会检查取得的键是否过期。如果过期就删除该删,否则就返回该键。(PS:键空间是一个保存了数据库所有键值对的数据结构)</p><p>优点:对 CPU 最友好。只有在操作的时候进行过期检查,删除的目标仅限于当前需要处理的键,不会在删除其他无关本次操作的过期键上花费任何 CPU 时间。</p><p>缺点:对内存不友好。这个十分容易理解了,键过期了,但因为一直没有被访问到,所以一直保留着(除非手动执行 flushdb 操来于清空当前数据库中的所有 key。),相当于内存泄漏。</p><h3>定期删除</h3><p>每隔一段时间,程序就对数据库进行检查,删除里面的过期键。至于要删除多少过期键,以及检查多少数据库,则有算法决定。 </p><p>该策略是上述两种策略的折中方案,需要通过实际情况,来设置删除操作的执行时长和频率。</p><p>明白了过期键的删除策略后,那 redis 服务器又是采用什么策略来删除过期键的呢?</p><p>实际上,Redis 服务器使用的是惰性删除和定期删除两种策略,通过配合使用,服务器可以很好的平衡 CPU 和内存。</p><p>其中惰性删除为 redis 服务器内置策略。而定期删除可以通过以下两种方式设置:</p><ol><li>配置 redis.conf 的 hz 选项,默认为10 (即 1 秒执行 10 次,值越大说明刷新频率越快,对 Redis 性能损耗也越大)</li><li>配置 redis.conf 的 maxmemory 最大值,当已用内存超过 maxmemory 限定时,就会触发主动清理策略</li></ol><h2>RDB 对过期键的处理</h2><h3>生成 RDB 文件</h3><p>程序会被数据库中的键进行检查,过期的键不会被保存到新创建的 RDB 文件中。因此<strong>数据库中的过期键不会对生成新的 RDB 文件造成影响</strong></p><h3>载入 RDB 文件</h3><p>这里需要分情况说明:</p><ol><li>如果服务器以主服务器模式运行,则在载入 RDB 文件时,程序会对文件中保存的键进行检查,过期键不会被载入到数据库中。<strong>所以过期键不会对载入 RDB 文件的主服务器造成影响</strong>。</li><li>如果服务器以从服务器模式运行,则在载入 RDB 文件时,不论键是否过期都会被载入到数据库中。但由于主从服务器在进行数据同步时,从服务器的数据会被清空。所以一般来说,<strong>过期键对载入 RDB 文件的从服务器也不会造成影响</strong>。</li></ol><h2>AOF 对过期键的处理</h2><h3>AOF 文件写入</h3><p>当服务器以 AOF 持久化模式运行时,如果数据库某个过期键还没被删除,那么 AOF 文件不会因为这个过期键而产生任何影响,依旧保留。 </p><p>而当过期键被删除后,那么程序会向 AOF 文件追加一条 DEL 命令来显式地记录该键被删除。</p><h3>AOF 重写</h3><p>执行 AOF 重写过程中,也会被数据库的键进行检查,已过期的键不会被保存到重写后的 AOF 文件中。因此<strong>不会对 AOF 重写造成影响</strong></p><h2>复制对过期键的处理</h2><p>当服务器运行在复制模式下,由主服务器来控制从服务器的删除过期键动作,目的是保证主从服务器数据的一致性。</p><p>那到底是怎么控制的呢?</p><ol><li>主服务器删除一个过期键后,会向所有从服务器发送一个 DEL 命令,告诉从服务器删除这个过期键</li><li>从服务器接受到命令后,删除过期键</li></ol><p>PS:从服务器在接收到客户端对过期键的读命令时,依旧会返回该键对应的值给客户端,而不会将其删除。</p><blockquote>参考《Redis设计与实现》,公众号后台回复「redis」可自取该书电子版</blockquote><blockquote>如果觉得文章不错,希望能得到你的关注:<a href="https://link.segmentfault.com/?enc=ilLBZ4Ak6VLI5juqqWWZ6Q%3D%3D.%2FgpyHXuq1Oq2tQUHtZrZAs6u5DzMVjEMM7od5mnunKExQhm2OaQv0jO3s9rzzp0W" rel="nofollow">七淅在学Java</a></blockquote>
一文让你明白平均负载
https://segmentfault.com/a/1190000017776080
2019-01-05T16:51:27+08:00
2019-01-05T16:51:27+08:00
七淅在学Java
https://segmentfault.com/u/qixijava
0
<h2>1. 什么是平均负载</h2><p>首先,我们先理解下什么是平均负载。</p><p>平均负载是指单位时间内,系统处于<strong>可运行状态</strong>和<strong>不可中断状态</strong>的平均进程数,也就是<strong>平均活跃进程数</strong>,它和 CPU 使用率并没有直接关系。(为什么和 CPU 使用率没直接关系,这个我后面说明)</p><p>那么问题来了,可运行状态和不可中断状态又是什么东西呢?</p><p>所谓可运行状态的进程,是指正在使用 CPU 或者正在等待 CPU 的进程,也就是我们常用 ps 命令看到的,处于 R 状态(Running 或 Runnable)的进程。</p><p>而不可中断状态的进程,则是正处于内核态关键流程中的进程,并且这些流程是不可打断的,比如最常见的是等待硬件设备的 I/O 响应,也就是我们在 ps 命令中看到的 D 状态(Uninterruptible Sleep,也称为 Disk Sleep)的进程。</p><p>比如,当一个进程向磁盘读写数据时,为了保证数据的一致性,在得到磁盘回复前,它是不能被其他进程或者中断打断的,这个时候的进程就处于不可中断状态。如果此时的进程被打断了,就容易出现磁盘数据与进程数据不一致的问题。</p><p>所以,不可中断状态实际上是系统对进程和硬件设备的一种保护机制。</p><p>明白了什么是平均负载后,那么自然就是要知道怎么用了。</p><h2>2. 如何查看平均负载</h2><p>当我们使用 uptime 命令时,会出现以下结果(这是我本机的结果,每个人的机器情况不一样)</p><pre><code>$ uptime
02:34:03 up 2 days, 20:14, 1 user, load average: 0.63, 0.83, 0.88</code></pre><p>对应解释:</p><pre><code>02:34:03 // 当前时间
up 2 days, 20:14 // 系统运行时间
1 user // 正在登录用户数
最后三个数字呢,依次则是过去 1 分钟、5 分钟、15 分钟的平均负载(Load Average)。</code></pre><p>明白了怎么看平均负载后,那么平均负载到底是多少才是合理的呢?</p><h2>3. 平均负载的合理值</h2><p><strong>当平均负载高于 CPU 数量 70% 的时候</strong>。就应该分析排查负载高的问题了。一旦负载过高,就可能导致进程响应变慢,进而影响服务的正常功能。</p><p>但 70% 这个数字并不是绝对的,最推荐的方法,还是把系统的平均负载监控起来,然后根据更多的历史数据,判断负载的变化趋势(可从 uptime 得到的三个数字分析)。当发现负载有明显升高趋势时,比如说负载翻倍了,你再去做分析和调查。</p><p><strong>平均负载在最理想的情况下,就是每个 CPU 上都刚好运行着一个进程</strong>,这样每个 CPU 都得到了充分利用。</p><p>比如当平均负载为 2 时,意味着什么呢?</p><ul><li>在只有 2 个 CPU 的系统上,意味着所有的 CPU 都刚好被完全占用。</li><li>在 4 个 CPU 的系统上,意味着 CPU 有 50% 的空闲。</li><li>而在只有 1 个 CPU 的系统中,则意味着有一半的进程竞争不到 CPU。</li></ul><p>好了,关于平均负载的基本知识差不多就是这样。现在,我来填下开头的坑 —— 为什么和 CPU 使用率没直接关系呢?</p><h2>4. 平均负载与 CPU 使用率</h2><p>在文章最开始的时候就有提到,平均负载是指单位时间内,处于可运行状态和不可中断状态的平均进程数。所以,它不仅包括了<strong>正在使用 CPU</strong>的进程,还包括<strong>等待 CPU</strong>和<strong>等待 IO</strong>的进程。</p><p>而 CPU 使用率,是单位时间内 CPU 繁忙情况的统计,跟平均负载并不一定完全对应。比如:</p><ul><li>CPU 密集型进程,使用大量 CPU 会导致平均负载升高,此时这两者是一致的。</li><li>I/O 密集型进程,等待 I/O 也会导致平均负载升高,但 CPU 使用率不一定很高。</li><li>大量等待 CPU 的进程调度也会导致平均负载升高,此时的 CPU 使用率也会比较高</li></ul><p>以上,就是文章的全部内容了,如果觉得有帮助的话,那就点个赞吧</p><blockquote>本文整理自极客时间:《Linux性能优化实战》</blockquote><blockquote>如果觉得文章不错,希望能得到你的关注:<a href="https://link.segmentfault.com/?enc=0tifxi1TUUCmnqew3a%2FoAQ%3D%3D.Vo6FIhuJrevFey9PnqtMsE045Tu%2BAcYAJcPbNCHtLcglLM5mZe9t0Wqg6JUY92tE" rel="nofollow">七淅在学Java</a></blockquote>
一文让你明白CPU上下文切换
https://segmentfault.com/a/1190000017457234
2018-12-20T13:08:22+08:00
2018-12-20T13:08:22+08:00
七淅在学Java
https://segmentfault.com/u/qixijava
8
<p>我们都知道,Linux 是一个多任务操作系统,它支持远大于 CPU 数量的任务同时运行。当然,这些任务实际上并不是真的在同时运行,而是因为系统在很短的时间内,将 CPU 轮流分配给它们,造成多任务同时运行的错觉。</p><p>而在每个任务运行前,CPU 都需要知道任务从哪里加载、又从哪里开始运行,也就是说,需要系统事先帮它设置好<strong>CPU 寄存器和程序计数器</strong></p><h2>什么是 CPU 上下文</h2><p>CPU 寄存器和程序计数器就是 CPU 上下文,因为它们都是 CPU 在运行任何任务前,必须的依赖环境。</p><ul><li>CPU 寄存器是 CPU 内置的容量小、但速度极快的内存。</li><li>程序计数器则是用来存储 CPU 正在执行的指令位置、或者即将执行的下一条指令位置。</li></ul><h2>什么是 CPU 上下文切换</h2><p>就是先把前一个任务的 CPU 上下文(也就是 CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。</p><p>而这些保存下来的上下文,会存储在系统内核中,并在任务重新调度执行时再次加载进来。这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行。</p><h2>CPU 上下文切换的类型</h2><p>根据任务的不同,可以分为以下三种类型</p><ul><li>进程上下文切换</li><li>线程上下文切换</li><li>中断上下文切换</li></ul><h3>进程上下文切换</h3><p>Linux 按照特权等级,把进程的运行空间分为内核空间和用户空间,分别对应着下图中, CPU 特权等级的 Ring 0 和 Ring 3。</p><ul><li>内核空间(Ring 0)具有最高权限,可以直接访问所有资源;</li><li>用户空间(Ring 3)只能访问受限资源,不能直接访问内存等硬件设备,必须通过系统调用陷入到内核中,才能访问这些特权资源。</li></ul><p><img src="/img/remote/1460000017457237" alt="来自极客时间" title="来自极客时间"></p><p>进程既可以在用户空间运行,又可以在内核空间中运行。进程在用户空间运行时,被称为进程的用户态,而陷入内核空间的时候,被称为进程的内核态。</p><h4>系统调用</h4><p>从用户态到内核态的转变,需要通过<strong>系统调用</strong>来完成。比如,当我们查看文件内容时,就需要多次系统调用来完成:首先调用 open() 打开文件,然后调用 read() 读取文件内容,并调用 write() 将内容写到标准输出,最后再调用 close() 关闭文件。</p><p>在这个过程中就发生了 CPU 上下文切换,整个过程是这样的: <br>1、保存 CPU 寄存器里原来用户态的指令位 <br>2、为了执行内核态代码,CPU 寄存器需要更新为内核态指令的新位置。 <br>3、跳转到内核态运行内核任务。 <br>4、当系统调用结束后,CPU 寄存器需要恢复原来保存的用户态,然后再切换到用户空间,继续运行进程。 </p><p>所以,<strong>一次系统调用的过程,其实是发生了两次 CPU 上下文切换</strong>。(用户态-内核态-用户态)</p><p>不过,需要注意的是,<strong>系统调用过程中,并不会涉及到虚拟内存等进程用户态的资源,也不会切换进程</strong>。这跟我们通常所说的进程上下文切换是不一样的:<strong>进程上下文切换,是指从一个进程切换到另一个进程运行;而系统调用过程中一直是同一个进程在运行。</strong></p><p>所以,<strong>系统调用过程通常称为特权模式切换,而不是上下文切换。系统调用属于同进程内的 CPU 上下文切换</strong>。但实际上,系统调用过程中,CPU 的上下文切换还是无法避免的。</p><h4>进程上下文切换跟系统调用又有什么区别呢</h4><p>首先,<strong>进程是由内核来管理和调度的,进程的切换只能发生在内核态</strong>。所以,进程的上下文不仅包括了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的状态。</p><p>因此,<strong>进程的上下文切换就比系统调用时多了一步:在保存内核态资源(当前进程的内核状态和 CPU 寄存器)之前,需要先把该进程的用户态资源(虚拟内存、栈等)保存下来;而加载了下一进程的内核态后,还需要刷新进程的虚拟内存和用户栈</strong>。</p><p>如下图所示,保存上下文和恢复上下文的过程并不是“免费”的,需要内核在 CPU 上运行才能完成。</p><p><img src="/img/remote/1460000017457238" alt="来自极客时间" title="来自极客时间"></p><h4>进程上下文切换潜在的性能问题</h4><p>根据 Tsuna 的测试报告,每次上下文切换都需要几十纳秒到数微秒的 CPU 时间。这个时间还是相当可观的,特别是在进程上下文切换次数较多的情况下,很容易导致 CPU 将大量时间耗费在寄存器、内核栈以及虚拟内存等资源的保存和恢复上,进而大大缩短了真正运行进程的时间。这也正是导致平均负载升高的一个重要因素。</p><p>另外,我们知道, Linux 通过 TLB(Translation Lookaside Buffer)来管理虚拟内存到物理内存的映射关系。当虚拟内存更新后,TLB 也需要刷新,内存的访问也会随之变慢。特别是在多处理器系统上,缓存是被多个处理器共享的,刷新缓存不仅会影响当前处理器的进程,还会影响共享缓存的其他处理器的进程。</p><h4>发生进程上下文切换的场景</h4><ol><li>为了保证所有进程可以得到公平调度,CPU 时间被划分为一段段的时间片,这些时间片再被轮流分配给各个进程。这样,当某个进程的时间片耗尽了,就会被系统挂起,切换到其它正在等待 CPU 的进程运行。</li><li>进程在系统资源不足(比如内存不足)时,要等到资源满足后才可以运行,这个时候进程也会被挂起,并由系统调度其他进程运行。</li><li>当进程通过睡眠函数 sleep 这样的方法将自己主动挂起时,自然也会重新调度。</li><li>当有优先级更高的进程运行时,为了保证高优先级进程的运行,当前进程会被挂起,由高优先级进程来运行</li><li>发生硬件中断时,CPU 上的进程会被中断挂起,转而执行内核中的中断服务程序。</li></ol><h3>线程上下文切换</h3><p>线程与进程最大的区别在于:<strong>线程是调度的基本单位,而进程则是资源拥有的基本单位</strong>。说白了,所谓内核中的任务调度,实际上的调度对象是线程;而进程只是给线程提供了虚拟内存、全局变量等资源。</p><p>所以,对于线程和进程,我们可以这么理解:</p><ul><li>当进程只有一个线程时,可以认为进程就等于线程。</li><li>当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源。这些资源在上下文切换时是不需要修改的。</li><li>另外,线程也有自己的私有数据,比如栈和寄存器等,这些在上下文切换时也是需要保存的。</li></ul><h4>发生线程上下文切换的场景</h4><ol><li>前后两个线程属于不同进程。此时,因为资源不共享,所以切换过程就跟进程上下文切换是一样。</li><li>前后两个线程属于同一个进程。此时,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据</li></ol><h3>中断上下文切换</h3><p>为了快速响应硬件的事件,<strong>中断处理会打断进程的正常调度和执行,转而调用中断处理程序,响应设备事件</strong>。而在打断其他进程时,就需要将进程当前的状态保存下来,这样在中断结束后,进程仍然可以从原来的状态恢复运行。</p><p><strong>跟进程上下文不同,中断上下文切换并不涉及到进程的用户态</strong>。所以,即便中断过程打断了一个正处在用户态的进程,也不需要保存和恢复这个进程的虚拟内存、全局变量等用户态资源。中断上下文,其实只包括内核态中断服务程序执行所必需的状态,包括 CPU 寄存器、内核堆栈、硬件中断参数等。</p><p><strong>对同一个 CPU 来说,中断处理比进程拥有更高的优先级</strong>,所以中断上下文切换并不会与进程上下文切换同时发生。同样道理,由于中断会打断正常进程的调度和执行,所以大部分中断处理程序都短小精悍,以便尽可能快的执行结束。</p><p>另外,跟进程上下文切换一样,中断上下文切换也需要消耗 CPU,切换次数过多也会耗费大量的 CPU,甚至严重降低系统的整体性能。所以,当你发现中断次数过多时,就需要注意去排查它是否会给你的系统带来严重的性能问题。</p><blockquote>本文整理自极客时间:《Linux性能优化实战》</blockquote><blockquote>如果觉得文章不错,希望能得到你的关注:<a href="https://link.segmentfault.com/?enc=HaygjObbwZFqOdhAvKSMWA%3D%3D.T%2BLKUZ4RpitCs0h%2FqvLzwdA8vlqqOFEpk5a2%2BAD0XME%2FLBkjkoCszZRdNBzonMr9" rel="nofollow">七淅在学Java</a></blockquote>
stash —— 一个极度实用的Git操作
https://segmentfault.com/a/1190000017384199
2018-12-14T23:23:18+08:00
2018-12-14T23:23:18+08:00
七淅在学Java
https://segmentfault.com/u/qixijava
0
<p>今天要介绍的 Git 操作就是 stash,毫不夸张地说,每个用 Git 的开发人员都一定要会懂怎么使用。</p><p>在介绍之前,不知道你有没有和我一样的经历:某一天,我正在一个 feature 分支上高高兴兴地写着(ba)代(a)码(ge)。突然线上环境报错了,是我负责的部分,此时当然是救火要紧哈,准备停下手中的工作准备切 master 分支 checkout 个 hotfix 分支出来。</p><p>脑袋正闪出这个想法的时候,咦,发现有点不对劲了 —— 此时我的 feature 分支功能还没做完,comment 上去没意义呀!将修改全部删掉更是不可能,这辈子都是不可能的,那这要怎么办呢?</p><p>如果这时能把这个 feature 分支中,还没写好的代码找个地方先藏起来,等到要用的时候再拿出去就完美了。</p><p>好了,今天要介绍的主角就能实现我们的需求。我们来看下 stash 这个功能到底是怎么使用的。</p><p>假如我现在的代码是这样的:</p><pre><code>public static void main(String[] args) {
System.out.println("我是 feature 分支原有的代码");
// ...
System.out.println("我是正在开发的代码");
}</code></pre><p>接着上面的情景,我需要把正在开发的代码给藏起来,那么直接使用 git stash 命令即可,使用后就会变成这样的效果:</p><pre><code>public static void main(String[] args) {
System.out.println(我是 feature 分支原有的代码");
// ...
}</code></pre><p>好了,正在 feature 分支还没写完的代码已经被藏起来了,此时,好奇心满满的你想着,它是被藏到哪里去呢?一顿谷歌之后,你发现可以通过这个命令查看 git stash list,如图:</p><p><img src="/img/remote/1460000017384202" alt="在这里插入图片描述" title="在这里插入图片描述"></p><p>图中的 stash@(0) 就是被藏起来的记录了,知道真相的你这下可以放心地去处理线上问题了。</p><p>你很牛皮,线上问题没一会功夫就搞定了,此时你再次切回刚才的 feature 分支,想要把刚才藏起来的代码拿出来。好了,一顿谷歌之后,你发现有两种拿的方法,分别是:</p><p>1、git stash pop</p><p>2、git stash apply</p><p>那这两者有什么不同呢?还记得刚才提交到 git stash list 命令显示的结果吗?—— stash@(0)</p><p>git stash pop 的是恢复刚才被藏起来的代码,同时删除 stash@(0) 这条记录也删了,此时你再使用 git stash list 命令就没有结果了:</p><p><img src="/img/remote/1460000017384203" alt="在这里插入图片描述" title="在这里插入图片描述"></p><p>明白 git stash pop 的作用后,那 git stash apply 命令也很好理解了,它们唯一的不同就是 git stash apply 命令不会删除stash@(0) 这条记录。</p><p>最后,如果你在一个分支上使用了 n 次 git stash 命令,那么就会有 stash@(0)、stash@(1)、...、stash@(n),对应一共有 n 条记录。</p><p>那我们要这么多条记录有什么用呢?</p><p>答案就是我们可以指定 git stash pop/apply 哪条记录。假如我想要恢复 stash@(1) 记录。那么对应的命令是 git stash pop stash@(1) 或 git stash apply stash@(1) </p><p>OK,以上就是全部内容了,希望对你有帮助。</p><p>PS:前几天我被极客时间的 Git 教程刷屏。坦白讲,Git 的确是一项必备技能。除了极客时间的教程之外,我也非常推荐廖雪峰老师的 Git 教程,链接如下:</p><p><a href="https://link.segmentfault.com/?enc=K2L4ZEu76e282QYP2qjdIg%3D%3D.W%2BiDM1XEBOVYSt6GVnMZQM10m5iEjoKuzn9tFB%2FT3AKqgoiSa0pqraq9CMXWFHjjAGX5CqAvOIgvI1s1QMMVMVQo%2F40HoDZuofVTjwgJXo7ipS0Sohmve91xL6VucpDK" rel="nofollow">https://www.liaoxuefeng.com/w...</a></p><blockquote>如果觉得文章不错,希望能得到你的关注:<a href="https://link.segmentfault.com/?enc=q2t5ZVPUthQDYyQDt7p3UQ%3D%3D.2wacko6khQsi%2BbnmeB%2BnB2goaArTJfmOHPDzZMAnAU2rO9ARS9BTgC6ri%2BuqllCx" rel="nofollow">七淅在学Java</a></blockquote>
你应该知道的数据库数据类型及其设计原则
https://segmentfault.com/a/1190000017213227
2018-11-30T21:41:58+08:00
2018-11-30T21:41:58+08:00
七淅在学Java
https://segmentfault.com/u/qixijava
4
<h2>1. 整数类型</h2><p>整数类型有:tinyint、smallint、mediumint、int、bigint,分别使用 8、16、24、32、64 位存储空间。它们可以存储的值范围从 -2 的 (n-1) 次方到 2 的 (n-1) 次方 -1,n 是存储空间的位数。</p><p>整数有可选的 unsigned 属性(无符号类型),表示不允许有负值,因此可以使正数上限提高一倍。</p><p>有符号和无符号类型使用相同的存储空间,并具有相同的性能,因此可以根据实际情况选择合适的类型。</p><h2>2. 实数类型</h2><p>实数类型有:FLOAT、DOUBLE ,分别占用 4,8 字节。</p><p>如果插入值的精度(即:数字总位数)高于实际定义的精度,系统会自动进行四舍五入处理,使值的精度达到要求。</p><p>其中 DECIMAL 也可以用来指定精度,并且它比 FLOAT 和 DOUBLE 更适合做精确计算。在本文就不做详细介绍了,如果有人想了解的话可以给我留言,我下次再写。</p><h2>3. 字符串类型</h2><p>字符串类型有:</p><ul><li>VARCHAR</li><li>CHAR</li><li>BLOB</li><li>TEXT</li></ul><p>由于 BLOB 和 TEXT 不常用且由于篇幅问题,就不展开描述了。本文主要对 VARCHAR 和 CHAR 进行介绍,它们的区别如下表:</p><table><thead><tr><th>对比内容</th><th>VARCHAR</th><th>CHAR</th></tr></thead><tbody><tr><td>是否固定长度</td><td>否</td><td>是</td></tr><tr><td>存储上限字节</td><td>65535</td><td>255</td></tr><tr><td>保存或检索值时,是否删除字符串末尾空格</td><td>否</td><td>是</td></tr><tr><td>超过设置的范围后,字符串是否会被截断</td><td>否</td><td>是</td></tr></tbody></table><p>除了以上不同之外,VARCHAR 还需要额外使用 1 个或 2 个字节来记录字符串长度。如果列的最大长度小于或等于 255 字节,则使用 1 个字节,否则使用 2 个字节。</p><p>由于 VARCHAR 是变长的,所以在 update 时,可能使行变得比原来更长,这就导致需要进行额外的工作。如果一个行占用的空间增加,并且在页内没有更多空间可以存储,在这种情况下,不同存储引擎的处理方式不一样的。例如:MyISAM 会将行拆分为不同的片段存储,InnoDB 则需要分裂页来使行可以放进页内。</p><p>在选择使用场景上,重点要抓住 VARCHAR 是变长,CHAR 是定长的特点。</p><p>比如在这些情况更适合使用 VARCHAR:</p><ul><li>字符串的最大长度比平均长度大很多;</li><li>字段更新次数少(所以碎片不是问题);</li><li>使用了像 UTF-8 这样复杂的字符集,每个字符都使用不同的字节数进行存储。</li></ul><p>而在这些情况则更适合使用 CHAR:</p><ul><li>存储很短的字符串(而 VARCHAR 还要多一个字节来记录长度,本来打算节约存储的现在反而得不偿失)</li><li>定长的字符串(如 MD5、uuid);</li><li>需要频繁修改的字段。因为 VARCHAR 每次存储都要有额外的计算,得到长度等工作;</li></ul><p>这里抛出一个小问题:使用 VARCHAR(5) 和 VARCHAR(200) 来存储 ‘hello’ 的空间开销是一样的。那么使用更短的列有什么好处呢?(思考几秒钟?)</p><p>答案:节约内存,因为更长的列会消耗更多的内存。MySQL 通常会分配固定大小的内存块来保存内部值。尤其是使用内存临时表进行排序或操作时会特别糟糕。在利用磁盘临时进行排序时也同样糟糕。</p><p>所以最好的策略是只分配真正需要的空间。</p><h2>4. 日期和时间类型</h2><p>下面表格是 TIMESTAMP 和 DATETIME 的一些对比:</p><table><thead><tr><th>对比内容</th><th>TIMESTAMP</th><th>DATETIME</th></tr></thead><tbody><tr><td>占用字节</td><td>4</td><td>8</td></tr><tr><td>时间范围</td><td>1970-01-01 08:00:01 ~ 2038-01-19 11:14:07</td><td>1000-01-01 00:00:00 ~ 9999-12-31 23:59:59</td></tr><tr><td>存储的数据是否随时区变化</td><td>是</td><td>否</td></tr></tbody></table><p>如果在插入数据时,没有指定第一个 TIMESTAMP 列的值,MySQL 则将这个列设置为当前时间,同时 TIMESTAMP 比 DATETIME 的空间效率更高。</p><p>最后,网上有很多讨论,时间到底要使用 INT、TIMESTAMP、DATETIME 哪种类型更适合。我认为这没有一个固定答案,具体可以参考文章:《选择合适的 MySQL 日期时间类型来存储你的时间》,我放在原文链接里面了。</p><h2>5. 设计合理的数据类型</h2><p>提供给大家 3 点设计原则:</p><ul><li>更小的通常更好</li><li>简单就好</li><li>尽量避免 NULL</li></ul><p>下面对其详细说明一下:</p><ol><li>一般情况下,应该选择可以正确存储数据的最小数据类型,因为它们占用更少的磁盘、内存和 CPU 缓存,并且处理时需要的 CPU 周期也更少。</li></ol><ol start="2"><li>简单数据类型的操作需要更少的 CPU 周期。例如,整型比字符操作代价更低,因为字符集和校对规则(排序规则)使字符比较比整型比较更复杂。</li></ol><ol start="3"><li>通常情况下,最好指定列为 NOT NULL,除非真的需要存储 NULL 值。因为可为 NULL 的列会使索引、索引统计和值比较都更复杂。可为 NULL 的列会使用更多的存储空间,在 MySQL 里也需要特殊处理。</li></ol><ol start="4"><li>当可为 NULL 的列被索引时,每个索引需要一个额外的字节,在 MyISAM 里甚至还可能导致固定大小的索引变成可变大小的索引。通常把可为 NULL 的列改为 NOT NULL 带来的性能比较小,所以在优化时没有必要先在现有表里修改这种情况。</li></ol><p>参考:<br>《高性能 MySQL》</p><blockquote>如果觉得文章不错,希望能得到你的关注:<a href="https://link.segmentfault.com/?enc=nAIflMNg5ZMtBsRGKdZlug%3D%3D.pqe2BlkphmfqJGe2Pq%2FgTHhzakMvNgduSjq5xf12fSGYp6W%2FK%2BjtVWp0uKuIsEAq" rel="nofollow">七淅在学Java</a></blockquote>
这也许是你不曾留意过的 Mybatis 细节
https://segmentfault.com/a/1190000017213198
2018-11-30T21:39:11+08:00
2018-11-30T21:39:11+08:00
七淅在学Java
https://segmentfault.com/u/qixijava
1
<p><img src="/img/remote/1460000017213201" alt="image" title="image"></p><p>Mybatis 可以说是 Java 后端的必备技能,可能你和我一样经常使用到它。但有时 cv 多了,会忘记了一些细节处理,比如为什么要加上这个注解?它的作用是什么等等。</p><p>这篇文章是我以前写的一些关于 Mybatis 细节,希望对各位有起到查漏补缺的作用。</p><h2>1. 配置文件</h2><p>SqlMapConfig.xml 文件各参数介绍:</p><pre><code>?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 加载属性文件 -->
<properties resource="jdbc.properties">
<!--properties中还可以配置一些属性名和属性值 -->
<!-- <property name="jdbc.driver" value=""/> -->
</properties>
<!-- 全局配置参数,需要时再设置 -->
<settings>
<!-- 打开延迟加载的开关,默认为false -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 将积极加载改为消极加载即按需要加载,默认为true -->
<setting name="aggressiveLazyLoading" value="false"/>
<!-- 开启二级缓存,默认为true -->
<setting name="cacheEnabled" value="true"/>
<!-- 使用jdbc的getGeneratedKeys获取数据库自增主键值 -->
<setting name="useGeneratedKeys" value="true"/>
</settings>
<!-- 别名定义 -->
<typeAliases>
<!-- 针对单个别名定义
type:类型的路径
alias:别名
-->
<!-- <typeAlias type="com.iot.mybatis.po.User" alias="user"/> -->
<!-- 批量别名定义
指定包名,mybatis自动扫描包中的po类,自动定义别名,别名就是类名(首字母大写或小写都可以)
-->
<package name="com.iot.mybatis.po"/>
</typeAliases>
<!-- 和spring整合后 environments配置将废除-->
<environments default="development">
<environment id="development">
<!-- 使用jdbc事务管理,事务控制由mybatis-->
<transactionManager type="JDBC"/>
<!-- 数据库连接池,由mybatis管理-->
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<!--加载映射文件有 3 种方法,如下所示-->
<mappers>
<mapper resource="sqlmap/User.xml"/>
<!--1. 通过 resource 方法一次加载一个映射文件 -->
<!-- <mapper resource="mapper/UserMapper.xml"/> -->
<!-- 2. 通过 mapper 接口加载单个映射文件
遵循一些规范:需要将 mapper 接口类名和 mapper.xml 映射文件名称保持一致,且在一个目录中
-->
<!-- <mapper class="com.iot.mybatis.mapper.UserMapper"/> -->
<!-- 3. 批量加载 mapper
指定 mapper 接口的包名,mybatis 自动扫描包下边所有 mapper 接口进行加载
遵循一些规范:需要将 mapper 接口类名和 mapper.xml映射文件名称保持一致,且在一个目录中
-->
<package name="com.iot.mybatis.mapper"/>
<mappers/></code></pre><h2>2. # 和 $ 的区别</h2><p>使用 #{parameterName} 引用参数的时候,Mybatis 会把这个参数认为是一个字符串,例如在下面的 sql 传入参数 “Smith”,<br>Select from emp where name = #{employeeName}</p><p>就会被转换为: <br>Select from emp where name = ‘Smith’;</p><p>同理在下面 sql 传入参数 “Smith”:<br>Select from emp where name = ${employeeName}</p><p>使用的时候就会被转换为 :<br>Select from emp where name = Smith;</p><p>简单来说, #{} 是经过预编译的,是安全的,而 ${} 是未经过预编译的,仅仅是取变量的值,是非安全的,存在 sql 注入的危险。# 将传入的数据都当成一个字符串,会对自动传入的数据加一个双引号。</p><p>使用 ${} 的情况,order by、like 语句只能用 ${} 了,用 #{} 会多个 ’ ’ 导致 sql 语句失效.此外动态拼接 sql,模糊查询时也要用 ${}。</p><p>举个栗子,假如 name=陈,那么该 sql 就是 ... LiKE '%陈%',<br>DELETE FROM stu WHERE <code>name</code> LIKE '%${name}%'</p><p>上面话有点长,这里小小总结下:<br>\#{} :编译好 SQL 语句再取值 <br>${} :取值以后再去编译SQL语句</p><h2>3. SQL 语句中的列名与关键字冲突时怎么办</h2><p>比如:name 字段。</p><p>解决方法:在列名两边加上两个 ` 即可,如下:<br>DELETE FROM stu WHERE <code>name</code> LIKE '%${name}%'</p><h2>4. 列名和 bean 属性名不一样,导致获取不到数据时怎么办</h2><p>那么为什么会导致查询的 SQL 语句无法得到正确结果呢?</p><p>因为 mybatis 会通过反射得到 bean ,由于字段名和属性名不一样,导致无法将查询到的表字段数据 set 到 bean 属性中。</p><p>解决方法: <br>【不推荐】修改表字段名称或 bean 属性名称,使它们同名即可</p><p>【推荐】修改 标签中的相应内容,如下图所示 </p><p><img src="/img/remote/1460000017213202" alt="image" title="image"></p><h2>5. @Param 参数的作用</h2><p>作用:相当于给其修饰的参数指定一个别名。</p><p>若接口只有一个参数则可以不用指定别名,List 参数除外。当有多个参数时一定要指定,否则 mybatis 映射不到对应的字段。</p><p>如下代码:<br>List<Book> queryAll(@Param("offset")int offset, @Param("limit")int limit);</p><p>该接口方法对应的 mapper 实现如下:</p><pre><code><select id="queryAll" resultType="Book">
SELECT
book_id,
`name`,
`number`
FROM
book
ORDER BY
book_id
LIMIT #{offset}, #{limit}
</select></code></pre><p>可以看到 #{} 里面的值是和 @Param("value") 保持一致的,也一定要一致,否则 mybatis 会找不到这个值而导致报错。另外,@Param("value") 中的 value 可以是任意的。</p><p>参考:</p><ul><li><a href="https://link.segmentfault.com/?enc=PgA20Xq159goQbLz%2BXL02w%3D%3D.nkmxj5auXuh1yWlg0XjUVrA0NHYJFPXL7M3%2FVGGS00c9Yb5Z4FQ8%2BVzrXO%2BtJpWf" rel="nofollow">https://github.com/liyifeng19...</a></li><li><a href="https://link.segmentfault.com/?enc=q3VW7suGJA3AcAQc9Sansg%3D%3D.nKzDYe6jV9xQTUg1DbfO5kxr10GH7ChZc1%2BI1IMBvVoeYrIsQP80AU9o8X1hIPiXZGwTPJ4b0bgsOi6HdPXHMpp4VbeFE8CSlexmuNdM5yRVAO07%2F1s3RpQ%2Fkg0lfMNYYQB%2Bp3VUFwdhG7wpdExf3Q%3D%3D" rel="nofollow">https://github.com/brianway/s...</a></li></ul><blockquote>如果觉得文章不错,希望能得到你的关注:<a href="https://link.segmentfault.com/?enc=M7hRP4IRvukYpc0QvSLz5A%3D%3D.vII1HU5Bkv4K07ThBe%2FA%2BacDEyfXcB4JFDFTTEdCuFuhoXKXH5NvWocWkhfdXx%2F2" rel="nofollow">七淅在学Java</a></blockquote>
浅谈布隆过滤器
https://segmentfault.com/a/1190000017213169
2018-11-30T21:36:35+08:00
2018-11-30T21:36:35+08:00
七淅在学Java
https://segmentfault.com/u/qixijava
0
<h2>1. 问题情景</h2><p>如果面试官问你,一个网站有 100 亿 url 存在一个黑名单中,每条 url 平均 64 字节。问这个黑名单要怎么存?若此时随便输入一个 url,如何判断该 url 是否在这个黑名单中?</p><p>对于第一个问题,如果把黑名单看成一个集合,将其存在 hashmap 中,貌似太大了,需要 640G,明显不科学。</p><p>那该怎么办?ok,现在该介绍今天的主角了 —— 布隆过滤器就可以解决这样的问题。</p><p>首先,布隆过滤器是什么?维基百科是这样解释的:</p><blockquote>布隆过滤器(英语:Bloom<br>Filter)是1970年由布隆提出的。它实际上是一个很长的二进制矢量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。</blockquote><p>官方说法看下就好,如果不理解没关系,其实不会难,下面我们讲人话慢慢来。</p><h2>2. 具体介绍</h2><blockquote>布隆过滤器实际上是一个很长的二进制矢量和一系列随机映射函数。</blockquote><p>「很长的二进制矢量」:这是一个长度很长的数组,什么类型的数组呢?bit 类型的数组,也是我们说的「位」,(1Byte = 8bit,1KB = 1024Byte)。</p><p>「一系列随机映射函数」:有多个哈希函数。那什么是哈希函数呢?JDK 里面有计算得到哈希值的方法,那就是一个哈希函数。</p><p>布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难</p><p>这个不就可以解决我们最开始的问题吗?那它是怎么解决的呢?</p><h2>3. 解决过程</h2><p>下面我说下大体的过程,细节部分可先不理解,重要的是明白流程,细节我后面补充。</p><p>假设,bit 类型数组的长度为 m,每个元素值为 0,有 k 个哈希函数。</p><p>首先,当输入一个 url 的时候,此时这个 url 会经过 k 个哈希函数处理,得到多个哈希值(v1,v2,...,vk)。之后得到这些哈希值对应在数组的下标位置,最后将这些下标的元素都置为 1。</p><p>那么如何判断一个 url 在黑名单里面呢?输入一条 url,它经过上述处理之后,会得到多个数组的下标位置。如果这些下标的元素值都已经为 1 了,说明该在黑名单里面,否则不在。</p><p>总体就是这样的流程,下面说下大家可能存在的疑问:</p><p>1、bit 类型的数组如何构建</p><p>2、得到 v1,v2,...,vk 这些哈希值后,如何得到其在数组的下标位置,并将其设置为 1 呢?</p><p>两个问题我一起说下,Java 里面没有 bit 这样的类型,怎么构建呢?—— 不难,我们可以使用 int,一个 int 是 32 位。</p><pre><code>//创建了一个 100 * 32bit 的数组
int[] arr = new int[100];
// 代表 bit 数组 0-31 位的元素
arr[0];
</code></pre><p>因此上面再会说「分别将这些哈希值除以数组的长度 m,和对 m 取模,得到这些哈希值对应在数组的下标位置」。</p><p>具体我们可以拿一个哈希值 data 来举个栗子,假设 int 数组长度为 100。</p><pre><code>void Set(int data) {
// ByteNO 是表示在 table 数组中那个元素
int ByteNo = data / 32;
// bitNo 是表示在 32 位 bit 中哪个 bit 位。
int BitNo = data % 32;
// 置 1
_table[ByteNo] |= (1 << BitNo);
}
</code></pre><h2>4. 使用效果</h2><p>最开始我们提到,如果将 100 亿 url 放到 HashMap 中需要 640GB,那么使用布隆过滤器后又需要多少空间呢?答案是约等于 23 GB。相比之下,这个空间大小是不是就可以接受很多了。</p><h2>5. 缺点</h2><p>布隆过滤器有宁可错杀一百,也不能放过一个的性质。讲人话就是属于黑名单的 url 一定能够正确判断它在黑名单中,但不属于黑名单中的 url 也可能会被认为在黑名单中,存在一定的失误率。</p><p>至于失误率要保持在多少,数组长度,哈希函数的个数分别要设置多少就需要根据实际情况来选择了,网上有对应的数学公式计算,这里就不展开讲了。</p><p>参考:<br><a href="https://link.segmentfault.com/?enc=vgqwrwdxwO8FSlrPYt8xVQ%3D%3D.GBRrimXj%2BdQK5Ple76IXXF%2BDGyVDhtE%2FRAfMIoeKVkBhC2vuqDQqIQywulKa5wECPyGhzBf8KojgqqAlRy3PAw%3D%3D" rel="nofollow">https://blog.csdn.net/wenqian...</a></p><blockquote>如果觉得文章不错,希望能得到你的关注:<a href="https://link.segmentfault.com/?enc=uwmiawKG1Jy73SIpsAQxaw%3D%3D.v4%2FmwB7q4ZHtkVdbiFQI0s9wct81q2%2FSwIcn%2FdzRaAzdyxKmu6M1CWxQ6W%2FcSKJy" rel="nofollow">七淅在学Java</a></blockquote>
interrupt(),interrupted() 和 isInterrupted() 的区别
https://segmentfault.com/a/1190000017213152
2018-11-30T21:33:44+08:00
2018-11-30T21:33:44+08:00
七淅在学Java
https://segmentfault.com/u/qixijava
1
<h2>1. 结论先行</h2><blockquote><p>interrupt():将调用该方法的对象所表示的线程标记一个停止标记,并不是真的停止该线程。</p><p>interrupted():获取<strong>当前线程</strong>的中断状态,并且会清除线程的状态标记。是一个是静态方法。</p><p>isInterrupted():获<strong>取调用该方法的对象所表示的线程</strong>,不会清除线程的状态标记。是一个实例方法。</p></blockquote><p>现在对各方法逐一进行具体介绍:</p><h2>2. interrupt()</h2><p>首先我们来使用一下 interrupt() 方法,观察效果,代码如下:</p><pre><code>public class MainTest {
@Test
public void test() {
try {
MyThread01 myThread = new MyThread01();
myThread.start();
myThread.sleep(2000);
myThread.interrupt();
} catch (Exception e) {
System.out.println("main catch");
e.printStackTrace();
}
}
}
public class MyThread01 extends Thread {
@Override
public void run() {
super.run();
for (int i = 0; i < 500; i++) {
System.out.println("i= " + i);
}
}
}</code></pre><p>输出结果:</p><p><img src="/img/remote/1460000017213155" alt="image" title="image"></p><p>可以看出,子线程已经执行完成了。说明 interrupt() 方法是不能让线程停止,和我们一开始所说的那样,它仅仅是在当前线程记下一个停止标记而已。</p><p>那么这个停止标记我们又怎么知道呢?——此时就要介绍下面的 interrupted() 和 isInterrupted() 方法了。</p><h2>3. interrupted() 和 isInterrupted()</h2><ul><li>interrupted() 方法的声明为 <code>public static boolean interrupted()</code></li><li>isInterrupted() 方法的声明为 <code>public boolean isInterrupted()</code></li></ul><p>这两个方法很相似,下面我们用程序来看下使用效果上的区别吧</p><p>先来看下使用 interrupted() 的程序。</p><pre><code>@Test
public void test() {
try {
MyThread01 myThread = new MyThread01();
myThread.start();
myThread.sleep(1000);
// 7行: Thread.currentThread().interrupt(); // Thread.currentThread() 这里表示 main 线程
myThread.interrupt();
// myThread.interrupted() 底层调用了 currentThread().isInterrupted(true); 作用是判断当前线程是否为停止状态
System.out.println("是否中断1 " + myThread.interrupted());
System.out.println("是否中断2 " + myThread.interrupted());
} catch (InterruptedException e) {
System.out.println("main catch");
}
System.out.println("main end");
}</code></pre><p>输出结果:</p><p><img src="/img/remote/1460000017213156" alt="image" title="image"></p><p>由此可以看出,线程并未停止,同时也证明了 interrupted() 方法的解释:测试当前线程是否已经中断,这个当前线程就是 main 线程,它从未中断过,所以打印结果都是 false。</p><p>那么如何使 main 线程产生中断效果呢?将上面第 8 行代码注释掉,并将第 7 行代码的注释去掉再运行,我们就可以得到以下输出结果:</p><p><img src="/img/remote/1460000017213157" alt="image" title="image"></p><p>从结果上看,方法 interrupted() 的确判断出了当前线程(此例为 main 线程)是否是停止状态了,但为什么第二个布尔值为 false 呢?我们在最开始的时候有说过——interrupted() 测试当前线程是否已经是中断状态,执行后会将状态标志清除。</p><p>因为执行 interrupted() 后它会将状态标志清除,底层调用了 isInterrupted(true),此处参数为 true 。所以 interrupted() 具有清除状态标记功能。</p><p>在第一次调用时,由于此前执行了 <code>Thread.currentThread().interrupt()</code>;,导致当前线程被标记了一个中断标记,因此第一次调用 interrupted() 时返回 true。因为 interrupted() 具有清除状态标记功能,所以在第二次调用 interrupted() 方法时会返回 false。</p><p>以上就是 interrupted() 的介绍内容,最后我们再来看下 isInterrupted() 方法吧。</p><p>isInterrupted() 和 interrupted() 有两点不同:一是不具有清除状态标记功能,因为底层传入 isInterrupted() 方法的参数为 false。二是它判断的线程调用该方法的对象所表示的线程,本例为 MyThread01 对象。</p><p>我们修改一下上面的代码,看下运行效果:</p><pre><code>@Test
public void test() {
try {
MyThread01 myThread = new MyThread01();
myThread.start();
myThread.sleep(1000);
myThread.interrupt();
// 修改了下面这两行。
// 上面的代码是 myThread.interrupted();
System.out.println("是否中断1 " + myThread.isInterrupted());
System.out.println("是否中断2 " + myThread.isInterrupted());
} catch (InterruptedException e) {
System.out.println("main catch");
e.printStackTrace();
}
System.out.println("main end");
}</code></pre><p>输出结果:</p><p><img src="/img/remote/1460000017213158" alt="image" title="image"></p><p>结果很明显,因为 isInterrupted() 不具有清除状态标记功能,所以两次都输出 true。</p><p>参考文章:<a href="https://link.segmentfault.com/?enc=EUaJC25%2BC6e8Am%2B%2FxuzWJQ%3D%3D.6inKrGrcJJ3N9S5UfxID74r5FPB5cGnHMFbsGS3YTTLG1R4mfuzvC6vbrTFoHAi0" rel="nofollow">http://www.cnblogs.com/hapjin...</a></p><blockquote>如果觉得文章不错,希望能得到你的关注:<a href="https://link.segmentfault.com/?enc=xcPWddglUyFa2R%2F84ZfWSg%3D%3D.5ICRZw1MYj3tQ9fFHOMERtxnbECAzmVfiHfHlPVKfucX9BKzKkkBLitq0i%2BUDRoa" rel="nofollow">七淅在学Java</a></blockquote>
记一次狂怼多线程的面经
https://segmentfault.com/a/1190000017213015
2018-11-30T21:15:27+08:00
2018-11-30T21:15:27+08:00
七淅在学Java
https://segmentfault.com/u/qixijava
1
<p>最近面试一家有直播业务的公司,明显感觉到对多线程的理解有一些要求。第一轮面试大概就面了 70 分钟左右,一轮下来口干舌燥。</p>
<p>下面对面试题做了下简单分类,分享给大家。</p>
<h3>多线程</h3>
<ul>
<li>有什么方法可以监控线程的状态</li>
<li>synchronized 的作用</li>
<li>synchronized 底层是怎么实现的</li>
<li>synchronized 有哪几个使用方式</li>
<li>修饰方法和修饰代码块有什么不同</li>
<li>synchronized 为什么要添加对象锁,即 synchronized () 中,为什么要有这个括号里面的内容</li>
<li>说下并发包和 synchronized 的区别</li>
<li>说下 Java 内存模型是什么</li>
<li>volatile 作用是什么</li>
<li>volatile 底层是怎么实现的,从指令层面分析</li>
<li>为什么不用 volatile 修饰每个变量呢</li>
<li>ThreadLocal 了解吗</li>
<li>CountDownLatch 了解吗</li>
<li>列举有哪些线程池</li>
<li>只有一个线程的线程池有什么作用,是来搞笑的吗</li>
<li>ThreadPoolExecutor 类的构造参数有哪些</li>
<li>如果程序一直在添加任务,线程池的处理过程是怎样的,结合构造参数来解释</li>
</ul>
<h3>jvm</h3>
<ul>
<li>有哪些 gc 算法</li>
<li>什么对象刚创建的时候直接进入老年代,非字符串对象</li>
<li>什么对象可以进入老年代</li>
<li>新生代的 s2 区有什么作用</li>
<li>什么情况下会 OOM</li>
<li>说下一个对象从创建到销毁的整个过程</li>
</ul>
<h3>redis</h3>
<ul>
<li>排行榜怎么设计</li>
<li>有序集合增加元素的时间复杂度是多少</li>
<li>AOF 和 RDB 的区别</li>
<li>
<p>redis 有大量任务需要消费,现在有两种消费方案,有什么不同?</p>
<ul>
<li>由一个开了 30 个线程的线程池不断消费</li>
<li>3 个线程不断获取 redis 任务,并将任务传给一个开了 30 个线程的线程池处理</li>
</ul>
</li>
</ul>
<h3>其他</h3>
<ul>
<li>hashtable 和 ConcurrentHashMap 有什么不同</li>
<li>ConcurrentHashMap 如何保证线程安全,1.7 和 1.8 都说下</li>
<li>NIO 和 BIO 的区别</li>
<li>NIO 的经典实现是怎样的(这个问题我不太确定,当时有点懵)</li>
</ul>
<h3>mysql</h3>
<ul>
<li>explain 作用</li>
<li>一般看 explain 输出信息的哪些内容</li>
<li>表有 a、b、c 字段,其他a、b独立建个索引,则下面的 sql 经过 explain 后会输出什么信息,哪些字段会走索引,哪些不会<br>select * from table where a = xx and b = yy order by c;</li>
</ul>
<h3>场景题</h3>
<ul><li>秒杀场景如何设计</li></ul>
<p><strong>欢迎关注微信公众号「不只Java」,后台回复「电子书」,送说不定有你想要的呢</strong><br><img src="/img/bVbjmMF?w=344&h=344" alt="图片描述" title="图片描述"></p>
面试官:快排会写吗?
https://segmentfault.com/a/1190000017006421
2018-11-14T10:04:41+08:00
2018-11-14T10:04:41+08:00
七淅在学Java
https://segmentfault.com/u/qixijava
0
<p>快排可以说是一道必知的常见面试题,同时也有多种实现方式。在这篇文章中,我使用的是随机三路快排。</p>
<p>之所以使用随机快速排序而不是普通的快排。是因为前者可以使得数列有序的概率降低,从而使随机快速排序平均速度是比快速排序要快的。具体的两者的性能差别可以看下这篇文章:</p>
<p><a href="https://link.segmentfault.com/?enc=54b1Wfl2KjdUqBR3R1JHZQ%3D%3D.TZrAUysyrF%2BRRLCVp3fVuue%2FM22qDAhC92k45Bua%2F4PjGKaIe6qv4k%2FIQPtEVjErWGgkaH3oWw38WYH%2BiBA%2FIw%3D%3D" rel="nofollow">https://blog.csdn.net/haelang...</a></p>
<p>talk id cheap,show the code。一共 20+ 行代码,每行代码都有注释。其中交换数组元素位置,打印元素的方法我就没贴了,代码太长你们也不方便看。</p>
<p>PS:代码下面有执行流程图,结合代码来看比较容易理解。</p>
<pre><code>public static void main(String[] args) {
// 测试数据
int[] arr = new int[]{5, 3, 6, 4};
// 执行快排
quickSort(arr, 0, arr.length - 1);
// 打印数组元素
printArray(arr);
}
private static void quickSort(int[] arr, int l, int r) {
if (l < r) {
// 随机取需要排序的数组中的一个元素和数组的最后一个元素交换,作为划分值
swap(arr, l + (int) (Math.random() * (r - l + 1)), r);
// 得到数组元素中等于划分值的区域
int[] part = partition(arr, l, r);
// 小于等于划分值的区域
quickSort(arr, l, part[0] - 1);
// 大于划分值的区域
quickSort(arr, part[1] + 1, r);
}
}
private static int[] partition(int[] arr, int l, int r) {
// 初始化小于等于划分值区域的当前下标,默认是数组第一个元素的前一个位置
int less = l - 1;
// 初始化大于划分值区域的当前下标,默认是数组最后一个元素的位置,同时也是划分值的位置,但该值并不属于大于划分值的区域,所以要在最后进行移动
int more = r;
// 当前下标小于大于划分值区域的下标时
while (l < more) {
// 当前值比划分值小,当前值和小于等于划分值区域的右边第一个值进行交换,小于等于划分值区域右移1个下标,当前下标+1
if (arr[l] < arr[r]) {
swap(arr, l++, ++less);
// 当前值比划分值大,当前值和大于划分值区域的左边第一个值进行交换,大于划分值的区域左移1个下标
} else if (arr[l] > arr[r]) {
swap(arr, l, --more);
// 当前值等于划分值,当前下标+1
} else {
// 当前下标+1
l++;
}
}
// 将划分值和大于划分值区域中,最接近划分值区域的元素交换。至此完成所有值的区域划分
swap(arr, more, r);
// 返回等于划分值的区域
return new int[]{less + 1, more};
} </code></pre>
<p>下面我会画个流程图帮大家理解一下,测试数据和代码一样。</p>
<p>假设代码执行完 13 行后,测试数据的顺序依旧不变,即为 {5,3,6,4}。</p>
<p>接下来在执行 partition() 方法的过程中,数组元素的情况如下图所示(灵魂写手求轻喷)<br><img src="/img/remote/1460000017006424" alt="" title=""></p>
<p>好了,以上就是本文的全部内容,我们下篇文章再见,捂脸逃~</p>
<p><strong>PS:本文原创发布于微信公众号「不只Java」,后台回复「Java」,送你 13 本 Java 经典电子书。公众号专注分享 Java 干货、读书笔记、成长思考</strong></p>
<p><img src="/img/remote/1460000017006425?w=300&h=300" alt="" title=""></p>
面试官:说说快速失败和安全失败是什么
https://segmentfault.com/a/1190000016969753
2018-11-10T11:17:09+08:00
2018-11-10T11:17:09+08:00
七淅在学Java
https://segmentfault.com/u/qixijava
1
<p>什么是快速失败(fail-fast)和安全失败(fail-safe)?它们又和什么内容有关系。以上两点就是这篇文章的内容,废话不多话,正文请慢用。</p><p>我们都接触 HashMap、ArrayList 这些集合类,这些在 java.util 包的集合类就都是快速失败的;而 java.util.concurrent 包下的类都是安全失败,比如:ConcurrentHashMap。</p><h2>1. 快速失败(fail-fast)</h2><p>在使用迭代器对集合对象进行遍历的时候,如果 A 线程正在对集合进行遍历,此时 B 线程对集合进行修改(增加、删除、修改),或者 A 线程在遍历过程中对集合进行修改,都会导致 A 线程抛出 ConcurrentModificationException 异常。</p><p>具体效果我们看下代码:</p><pre><code> HashMap hashMap = new HashMap();
hashMap.put("不只Java-1", 1);
hashMap.put("不只Java-2", 2);
hashMap.put("不只Java-3", 3);
Set set = hashMap.entrySet();
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
hashMap.put("下次循环会抛异常", 4);
System.out.println("此时 hashMap 长度为" + hashMap.size());
}
</code></pre><p>执行后的效果如下图:</p><p><img src="/img/remote/1460000016969807" alt="" title=""></p><p>为什么在用迭代器遍历时,修改集合就会抛异常时?</p><p>原因是迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变 modCount 的值。</p><p>每当迭代器使用 hashNext()/next() 遍历下一个元素之前,都会检测 modCount 变量是否为 expectedModCount 值,是的话就返回遍历;否则抛出异常,终止遍历。</p><h2>2. 安全失败(fail-safe)</h2><p>明白了什么是快速失败之后,安全失败也是非常好理解的。</p><p>采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。</p><p>由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,故不会抛 ConcurrentModificationException 异常</p><p>我们上代码看下是不是这样</p><pre><code>ConcurrentHashMap concurrentHashMap = new ConcurrentHashMap();
concurrentHashMap.put("不只Java-1", 1);
concurrentHashMap.put("不只Java-2", 2);
concurrentHashMap.put("不只Java-3", 3);
Set set = concurrentHashMap.entrySet();
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
concurrentHashMap.put("下次循环正常执行", 4);
}
System.out.println("程序结束");</code></pre><p>运行效果如下,的确不会抛异常,程序正常执行。<br><div align=center><br><img src="/img/remote/1460000016969808" alt="" title=""></p><p>最后说明一下,快速失败和安全失败是对迭代器而言的。并发环境下建议使用 java.util.concurrent 包下的容器类,除非没有修改操作。</p><p>我是七淅 (xī),后台回复「Java」,送你 13 本 Java 经典电子书。公众号专注分享 Java 干货、读书笔记、成长思考。</p><p>参考文章:<br><a href="https://link.segmentfault.com/?enc=PEx9R12cD3Rp1GGQx9ftWw%3D%3D.4p9%2BVY5aOYUytoxXpOShFAYt53NxCJ5PV4Sw%2F48%2FnJhjBfVh3Fpo4jU1PAP%2F94k514ip0Cm1YmKwGN98VItYxw%3D%3D" rel="nofollow">https://blog.csdn.net/qq_3178...</a></p><blockquote>如果觉得文章不错,希望能得到你的关注:<a href="https://link.segmentfault.com/?enc=tDwvAnEnbHg4Dzri92kR%2Bg%3D%3D.fEvKhSFLje6jR1d0dJUZA%2FyQaPl7WZLQXY7rhEOHaJ3Sx%2FZmEdtolj%2BDg5q7XHDR" rel="nofollow">七淅在学Java</a></blockquote>