JVM字符串常量池及String的intern方法详解?

thbcm阅读(202)

文章转载自公众号:程序新视界

本篇文章基于字符串常量池的存储及在使用 intern方法 时所引起的内存变化进行一步深层次的讲解。

重点内容:当字符串调用 intern方法 方法后,再进行字符串的比较,会发生什么变化?

本文内容均以HotSpot虚拟机为基础讲解。

面试题

先通过一个面试题形象的了解一下我们本篇文章要讲的内容的呈现形式:

String s1 = new String("he") + new String("llo");
String s2 = new String("h") + new String("ello");


String s3 = s1.intern();
String s4 = s2.intern();
System.out.println(s1 == s3);
System.out.println(s1 == s4);

执行上面的代码,会发现打印的结果都是 true 。那么,为什么本来不相等的字符串,调用了intern方法之后便相等了呢?下面我们就来逐步分析这其中的底层实现。

intern方法的作用

intern()方法的功能定义:

(1)如果当前字符串内容存在于字符串常量池(即equals()方法为true,也就是内容一样),那直接返回此字符串在常量池的引用;

(2)如果当前字符串不在字符串常量池中,那么在常量池创建一个引用并指向堆中已存在的字符串,然后返回常量池中的引用。

简单说intern方法就是判断并将字符串是否存在于字符串常量池,如果不存在则创建,存在则返回。

字符串常量池

HotSpot中实现字符串常量池功能的是一个StringTable类,它是一个Hash表,默认值大小长度是1009。在每个HotSpot虚拟机的实例中只有一份,被所有的类共享。字符串常量由一个个字符组成,放在了StringTable上。

JDK6及之前版本,字符串常量池是放在Perm Gen区(方法区)中。StringTable的长度是固定的,长度是1009,当String字符串过多时会造成hash冲突,导致链表过长,性能大幅度下降。此时字符串常量池里面放的全部是字符串常量(字面值)。

由于永久代的空间有限且固定,JDK6的存储模式很容易造成OutOfMemoryError

JDK7时正在着手去永久代的工作,因此字符串常量池被放在了堆中。此时,即使堆的大小也是固定的,但对于应用调优工作,只需要调整堆大小就行了。

JDK7中字符串常量池不仅仅可以存放字符串常量,还可以存放字符串的引用。也就是说,堆中的字符串的引用可以作为常量池的值而存在。

字符串池化流程分析

在了解了上面的基础理论,我们下面以图文相结合的形式来逐步演示字符串池化的流程和分类。以下实例以JDK8版本为基础来进行分析讲解。

当我们通过双引号声明一个字符串:

String wechat = "程序新视界";

此时,双引号内的字符串会被直接存储在字符串常量池中。

关于上面的存储结构,我们已经在之前文章中提到,不再过多解释。下面如果我们再声明同样的字符串看看会有什么样的变化。

String wechat = "程序新视界";
String wechat1 = "程序新视界";

上述代码中声明wechat1时,会发现常量池中已经存在了对应的字符串,则不会再重新创建,只是把对应的引用返回给wechat1。对应结构图如下:

此时,如果直接用双等号比较wechatwechat1肯定是相等的,因为它们的引用和字面值都是相同的。

上面是直接双引号赋值的情况,那么如果通过 new 的形式创建字符串对应的流程又是如何呢?前面文章已经讲到这分两种情况:常量池存在对应的值和不存在对应的值。

String wechat2 = new String("程序新视界");

如果存在对应的值,此时会先在堆中创建一个针对wechat2变量的对象引用,然后将这个对象引用指向字符串常量池中已经存在的常量。

此时直接使用双等号比较wechatwechat2变量肯定是不相等的,而通过equals方法进行对比字面值则是相等的。

另外一种情况就是通过 new 创建时,字符串常量池中并不存在对应的常量。这种情况会现在字符串常量池中创建一个字符串常量,然后再在堆中创建一个字符串,持有常量池中对应字符串的引用。并把堆中对象的地址返回给wechat2。最终效果图依旧如上图。

在此时,如果不是直接new字符串赋值,而是通过+号操作,情况就有所不同。

String s1 = "程序";
String wechat3 = new String(s1 + "新视界");

上述代码 s1 会存入常量池,而wechat3的值则由于JVM编译时采用了StringBuilder进行加号的拼接,只会在堆中创建一个String对象,并不会在常量池中存储对应的字符串。

此时的情况已经涉及到我们面试题中创建字符串的情况了。那么,下面我们就通过intern方法进行池化操作,看看字符串常量池的具体变化。

还以上面的代码为例,此时wechatwechat1wechat2三个变量和wechat3直接用双等号比较肯定是不相等的。下面对wechat3进行intern池化处理。

String s1 = "程序";
String wechat3 = new String(s1 + "新视界");
wechat3 = wechat3.intern();

此时会发现wechatwechat1两个变量与wechat3的值相等了。由于wechatwechat1其实是一个,这里只以wechatwechat3的比较为例来分析一下这个流程。

在没有调用intern方法之前内存的状态是下图(忽略掉s1部分)这样的:

看上图它们的值不相等也就不奇怪了。下面对wechat3进行池化处理,并把池化的结果赋值给wechat3,就是上面的代码。内存结构会发生如下变化:

此时,再判断对应的两个值,因为引用和字面值全部相同,因此便相等了。具体intern的判断规则我们上面已经知道,如果常量池中存在对应的值,则直接返回引用。

那还有另外一种情况,就是常量池中不存在对应的值会是如何处理的呢?先看如下代码:

String s2 = "关注";
String wechat4 = new String(s2 + "公众号");
wechat4 = wechat4.intern();

在调用intern之前的操作我们前面已经说过,会在堆中创建一个String对象,而常量池中并不会存储一份,与wechat3的图一样。

此时常量池中并未存在对应的字符串,此时调用intern方法之后,内存结构如下:

intern方法之后,常量池中存了堆中对应字符串的引用。对照上面说的,JDK7及之后字符串常量池中可以存储引用了。

需要注意的是,当字符串常量池中并不存在对应字符串时,调用intern方法返回的地址为堆中的地址,对应图中的0x99。而wechat4本来地址指向的就是堆中的地址,因此不会发生变化。

此时如果再定义一个双引号赋值的wechat5,如下代码:

String s2 = "关注";
String wechat4 = new String(s2 + "公众号");
wechat4 = wechat4.intern();


String wechat5 = "关注公众号";
System.out.println(wechat4 == wechat5);

变量wechat5初始化时发现字符串常量池中已经存在了一个引用,那么wechat5会直接指向这个引用,也就是wechat5wechat4一样,都指向内存中的String对象。

小结

上面这个演示实例时需要注意的重点是intern方法返回的引用地址。如果字符串常量池中已经存在对应的字符串时,此时返回的是字符串常量的地址【常量池中存储的是字符串】,如果字符串常量池中不存在对应的字符串,此时会把堆中的引用放在常量池对应的位置【常量池中存储的是堆中字符串的引用】,此时intern返回的是堆中字符串对应的引用。

搞清楚了上面的返回逻辑再看最初的代码:

String s1 = new String("he") + new String("llo");
String s2 = new String("h") + new String("ello");


String s3 = s1.intern();
String s4 = s2.intern();
System.out.println(s1 == s3);
System.out.println(s1 == s4);

其中 s1 为堆中字符串“hello”的地址;s2 为堆中另外一个“hello”字符串的地址。当s1.intern(),常量池中存储了 s1 的地址,此时s1.intern()返回的也是 s1 的地址,因此s1=s3,都是同一个地址嘛。

然后执行s2.intern(),此时常量池中已经有 hello 字符串,类型为引用且指向 s1 的地址,执行之后返回的便是 s1 的地址,赋值给 s4 ,因此 s1 和 s4 也指向同一个地址,因此相等。

通过上面的更深层次的分析,想必大家对字符串常量、字符串常量池以及intern方法有了更加深刻的理解。相关的面试题如果按照这个思路分析,基本上都可以进行准确解答了。

以上就是W3Cschool编程狮关于JVM字符串常量池及String的intern方法详解?的相关介绍了,希望对大家有所帮助。

react hooks线上 bug后复盘

thbcm阅读(179)

文章转载自公众号:前端之露

最近团队内有同学,由于写 react hooks 引发了一些 bug,甚至有 1 例是线上问题。团队内也因此发起了一些争执,到底要不要写 hooks?到底要不要加 lint?到底要不要加 autofix?争论下来结论如下:

  1. 写还是要写的;
  2. 写 hooks 前一定要先学习 hooks;
  3. 团队再出一篇必读文档,必须要求每位同学,先读再写。

因此便有了此文。

本文主要讲两大点:

  1. 写 hooks 前的硬性要求;
  2. 写 hooks 常见的几个注意点。

硬性要求

1. 必须完整阅读一次 React Hooks 官方文档

英文文档:https://reactjs.org/docs/hooks-intro.html

中文文档:https://zh-hans.reactjs.org/docs/hooks-intro.html

其中重点必看 hooks: useStateuseReduceruseEffectuseCallbackuseMemo 另外推荐阅读:

  1. Dan的《useEffect完全指南》
  2. 衍良同学的《React Hooks完全上手指南》

2. 工程必须引入 lint 插件,并开启相应规则

lint 插件:https://www.npmjs.com/package/eslint-plugin-react-hooks 必开规则:

{
  "plugins": ["react-hooks"],
  "rules": {
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn"
  }
}

其中, react-hooks/exhaustive-deps 至少 warn,也可以是 error。建议全新的工程直接配 “error”,历史工程配 “warn”。

切记,本条是硬性条件。

如果你的工程,当前没开启 hooks lint rule,请不要编写任何 hooks 代码。如果你 CR 代码时,发现对方前端工程,没有开启相应规则,并且提交了 hooks 代码,请不要合并。该要求适应于任何一个 React 前端工程。

这两条规则会避免我们踩坑。虽然对于 hooks 新手,这个过程可能会比较“痛苦”。不过,如果你觉得这两个规则对你编写代码造成了困扰,那说明你还未完全掌握 hooks。

如果对于某些场景,确实不需要「exhaustive-deps」,可在代码处加: // eslint-disable-next-line react-hooks/exhaustive-deps

切记只能禁本处代码,不能偷懒把整个文件都禁了。

3. 如若有发现 hooks 相关 lint 导致的 warning,不要全局 autofix

除了 hooks 外,正常的 lint 基本不会改变代码逻辑,只是调整编写规范。但是 hookslint 规则不同,exhaustive-deps 的变化会导致代码逻辑发生变化,这极容易引发线上问题,所以对于 hookswaning,请不要做全局 autofix 操作。除非保证每处逻辑都做到了充分回归。

另外公司内部有个小姐姐补充道:eslint-plugin-react-hooks 从2.4.0版本开始,已经取消了 exhaustive-deps 的autofix。所以,请尽量升级工程的lint插件至最新版,减少出错风险

然后建议开启 vscode 的「autofix on save」。未来无论是什么问题,能把 error 与 warning 尽量遏制在最开始的开发阶段,保证自测跟测试时就是符合规则的代码。

常见注意点

依赖问题

依赖与闭包问题是一定要开启exhaustive-deps 的核心原因。最常见的错误即:mount 时绑定事件,后续状态更新出错。

错误代码示例:(此处用 addEventListener 做 onclick 绑定,只是为了方便说明情况)

function ErrorDemo() {
  const [count, setCount] = useState(0);
  const dom = useRef(null);
  useEffect(() => {
    dom.current.addEventListener('click', () => setCount(count + 1));
  }, []);
  return <div ref={dom}>{count}</div>;
}

这段代码的初始想法是:每当用户点击 domcount 就加1。理想中的效果是一直点,一直加。但实际效果是 {count} 到「1」以后就加不上了。

我们来梳理一下, useEffect(fn, []) 代表只会在 mount 时触发。也即是首次 render 时,fn 执行一次,绑定了点击事件,点击触发 setCount(count + 1) 。乍一想,count 还是那个 count,肯定会一直加上去呀,当然现实在啪啪打脸。

状态变更触发页面渲染的本质是什么?本质就是 ui = fn(props, state, context) 。props、内部状态、上下文的变更,都会导致渲染函数(此处就是ErrorDemo)的重新执行,然后返回新的 view。

那现在问题来了, ErrorDemo 这个函数执行了多次,第一次函数内部的 count 跟后面几次的 count 会有关系吗?这么一想,感觉又应该没有关系了。那为什么第二次又知道 count 是1,而不是 0 了呢?第一次的setCount 跟后面的是同一个函数吗?这背后涉及到 hooks 的一些底层原理,也关系到了为什么 hooks 的声明需要声明在函数顶部,不允许在条件语句中声明。在这里就不多讲了。

结论是:每次 count 都是重新声明的变量,指向一个全新的数据;每次的setCount 虽然是重新声明的,但指向的是同一个引用。

回到正题,我们知道了每次 render,内部的 count 其实都是全新的一个变量。那我们绑定的点击事件方法,也即:setCount(count + 1) ,这里的 count,其实指的一直是首次 render 时的那个 count,所以一直是 0 ,因此 setCount,一直是设置 count 为1。

那这个问题怎么解?

首先,应该遵守前面的硬性要求,必须要加 lint 规则,并开启 autofix on save。然后就会发现,其实这个 effect 是依赖 count 的。autofix 会帮你自动补上依赖,代码变成这样:

useEffect(() => {
  dom.current.addEventListener('click', () => setCount(count + 1));
}, [count]);

那这样肯定就不对了,相当于每次 count 变化,都会重新绑定一次事件。所以对于事件的绑定,或者类似的场景,有几种思路,我按我的常规处理优先级排列:

思路1:消除依赖

在这个场景里,很简单,我们主要利用 setCount 的另一个用法 functional updates。这样写就好了: () => setCount(prevCount => ++prevCount) ,不用关心什么新的旧的、什么闭包,省心省事。

思路2:重新绑定事件

那如果我们这个事件就是要消费这个 count 怎么办?比如这样:

dom.current.addEventListener('click', () => {
  console.log(count);
  setCount(prevCount => ++prevCount);
});

我们不必执着于一定只在 mount 时执行一次。也可以每次重新 render 前移除事件,render 后绑定事件即可。这里利用 useEffect 的特性,具体可以自己看文档:

useEffect(() => {
  const $dom = dom.current;
  const event = () => {
    console.log(count);
    setCount(prev => ++prev);
  };
  $dom.addEventListener('click', event);
  return () => $dom.removeEventListener('click', event);
}, [count]);

思路3

如果嫌这样开销大,或者编写麻烦,也可以用 useRef 其实用 useRef 也挺麻烦的,我个人不太喜欢这样操作,但也能解决问题,代码如下:

const [count, setCount] = useState(0);
const countRef = useRef(count);
useEffect(() => {
  dom.current.addEventListener('click', () => {
    console.log(countRef.current);
    setCount(prevCount => {
      const newCount = ++prevCount;
      countRef.current = newCount;
      return newCount;
    });
  });
}, []);

useCallback 与 useMemo

这两个 api,其实概念上还是很好理解的,一个是「缓存函数」, 一个是缓存「函数的返回值」。但我们经常会懒得用,甚至有的时候会用错。

从上面依赖问题我们其实可以知道,hooks对「有没有变化」这个点其实很敏感。如果一个 effect 内部使用了某数据或者方法。若我们依赖项不加上它,那很容易由于闭包问题,导致数据或方法,都不是我们理想中的那个它。如果我们加上它,很可能又会由于他们的变动,导致 effect 疯狂的执行。真实开发的话,大家应该会经常遇到这种问题。

所以,在此建议:

  1. 在组件内部,那些会成为其他 useEffect 依赖项的方法,建议用 useCallback 包裹,或者直接编写在引用它的useEffect中。
  2. 己所不欲勿施于人,如果你的 function 会作为 props传递给子组件,请一定要使用 useCallback 包裹,对于子组件来说,如果每次render都会导致你传递的函数发生变化,可能会对它造成非常大的困扰。同时也不利于 react 做渲染优化。

不过还有一种场景,大家很容易忽视,而且还很容易将 useCallbackuseMemo 混淆,典型场景就是:节流防抖。

举个例子:

function BadDemo() {
  const [count, setCount] = useState(1);
  const handleClick = debounce(() => {
    setCount(c => ++c);
  }, 1000);
  return <div onClick={handleClick}>{count}</div>;
}

我们希望防止用户连续点击触发多次变更,加了防抖,停止点击1秒后才触发 count + 1 ,这个组件在理想逻辑下是OK的。但现实是骨感的,我们的页面组件非常多,这个 BadDemo 可能由于父级什么操作就重新 render 了。现在假使我们页面每500毫秒会重新 render 一次,那么就是这样:

function BadDemo() {
  const [count, setCount] = useState(1);
  const [, setRerender] = useState(false);
  const handleClick = debounce(() => {
    setCount(c => ++c);
  }, 1000);
  useEffect(() => {
    // 每500ms,组件重新render
    window.setInterval(() => {
      setRerender(r => !r);
    }, 500);
  }, []);
  return <div onClick={handleClick}>{count}</div>;
}

每次 render 导致 handleClick 其实是不同的函数,那么这个防抖自然而然就失效了。这样的情况对于一些防重点要求特别高的场景,是有着较大的线上风险的。

那怎么办呢?自然是想加上 useCallback :

const handleClick = useCallback(debounce(() => {
  setCount(c => ++c);
}, 1000), []);

现在我们发现效果满足我们期望了,但这背后还藏着一个惊天大坑。

假如说,这个防抖的函数有一些依赖呢?比如 setCount(c => ++c); 变成了 setCount(count + 1) 。那这个函数就依赖了 count 。代码就变成了这样:

const handleClick = useCallback(
  debounce(() => {
    setCount(count + 1);
  }, 1000),
  []
);

大家会发现,你的 lint 规则,竟然不会要求你把 count 作为依赖项,填充到 deps 数组中去。这进而导致了最初的那个问题,只有第一次点击会 count++。这是为什么呢?

因为传入 useCallback 的是一段执行语句,而不是一个函数声明。只是说它执行以后返回的新函数,我们将其作为了 useCallback 函数的入参,而这个新函数具体是个啥,其实 lint 规则也不知道。

更合理的姿势应该是使用 useMemo :

const handleClick = useMemo(
  () => debounce(() => {
    setCount(count + 1);
  }, 1000),
  [count]
);

这样保证每当 count 发生变化时,会返回一个新的加了防抖功能的新函数。

总而言之,对于使用高阶函数的场景,建议一律使用 useMemo

有些网友提供了宝贵的反馈,我继续补充:刚使用useMemo,依旧存在一些问题。

问题1useMemo「将来」并不「稳定」

react 的官方文档中提到:你可以把 useMemo 作为性能优化的手段,但不要把它当成语义上的保证。将来,React 可能会选择“遗忘”以前的一些 memoized 值,并在下次渲染时重新计算它们,比如为离屏组件释放内存。先编写在没有 > useMemo 的情况下也可以执行的代码 —— 之后再在你的代码中添加 > useMemo,以达到优化性能的目的。也就是说,在将来的某种特殊情况下,这个防抖函数依旧会失效。当然,这种情况是发生在「将来」,且相对比较极端,出现概率较小,即使出现,也不会“短时间内连续”出现。所以对于不是 「前端防不住抖就要完蛋」的场景,风险相对较小。

问题2useMemo 并不能一劳永逸解决所有高阶函数场景

在示例的场景中,防抖的逻辑是:「连续点击后1秒,真正执行逻辑,在这过程中的重复点击失效」。而如果业务逻辑改成了「点击后立即发生状态变更,再之后的1秒内重复点击无效」,那么我们的代码可能就变成了。

const handleClick = useMemo( 
  () => throttle(() => { setCount(count + 1); }, 1000), [count] );

然后发现又失效了。原因是点击以后,count 立即发生了变化,然后 handleClick 又重复生成了新函数,这个节流就失效了。

所以这种场景,思路又变回了前面提到的,「消除依赖」 或 「使用ref」

当然啦,也可以选择自己手动实现一个 debouncethrottle 。我建议可以直接使用社区的库,比如 react-use,或者参考他们的实现自己写两个实现。

以上就是W3Cschool编程狮关于react hooks线上 bug后复盘的相关介绍了,希望对大家有所帮助。

纯CSS实现吸附效果

thbcm阅读(168)

文章转载自公众号:IQ前端

前言

「吸附效果」就是网页滚到到某个位置,元素固定在该位置,后续不随网页滚动而滚动。吸附效果很常见,譬如吸顶效果吸底效果,经常用在跟随导航移动广告悬浮提示等场景中。

原理

jQuery 时代就有很多吸附效果插件了,现在常用的三大前端框架也有自身第三方的吸附效果组件。它们都有着共通的实现原理:监听scroll事件,判断scrollTop目标元素的位置范围,符合条件则将目标元素position声明为fixed,令目标元素相对于浏览器窗口进行定位,让用户看上去就像钉在浏览器指定位置上。

javascript 实现吸附效果的代码在网上一搜一大堆,更何况笔者喜欢耍 CSS ,在此就不贴相关的JS代码了。本文为各位同学推荐一个很少见很少用的 CSS 属性:position:sticky。简单的「两行CSS核心代码」就能完成「十多行JS核心代码」的功能,何乐而不为呢。

实现

简单回顾position常用的值,怎样用就不说了,各位同学应该都熟透了。

取值 功能 版本
「inherit」 继承 2
「static」 标准流 2
「relative」 相对定位 2
「absolute」 绝对定位 2
「fixed」 固定定位 2
「sticky」 粘性定位 3

当值为sticky时将元素变成粘性定位「粘性定位」相对定位固定定位的合体,元素在特定阈值跨越前为相对定位,跨越后为固定定位。

主要是为了推广知识点,直接上代码,样式就不细磨了,将就看吧。

<div class="ads-position">
    <ul>
        <li>Top 1</li>
        <li>Top 2</li>
        <li>Normal</li>
        <li>Bottom 1</li>
        <li>Bottom 2</li>
    </ul>
</div>
.ads-position {
    overflow: auto;
    position: relative;
    width: 400px;
    height: 280px;
    outline: 1px solid #3c9;
    ul {
        padding: 200px 0;
    }
    li {
        position: sticky;
        height: 40px;
        line-height: 40px;
        text-align: center;
        color: #fff;
        &:nth-child(1) {
            top: 0;
            z-index: 9;
            background-color: #f66;
        }
        &:nth-child(2) {
            top: 40px;
            z-index: 9;
            background-color: #66f;
        }
        &:nth-child(3) {
            background-color: #f90;
        }
        &:nth-child(4) {
            bottom: 0;
            z-index: 9;
            background-color: #09f;
        }
        &:nth-child(5) {
            bottom: 40px;
            z-index: 9;
            background-color: #3c9;
        }
    }
}

最终效果如下。两行CSS核心代码分别是position:stickytop/bottom:npx。上述Demo里5个节点都声明了position:sticky,但由于top/bottom赋值有所不同就产生了不同的吸附效果。

细心的同学可能发现这些元素在某些滚动时刻处于相对定位,在特定滚动时刻就处于固定定位

  • 第1个<li>top0px,滚动到容器顶部就固定
  • 第2个<li>top40px,滚动到距离容器顶部40px就固定
  • 第3个<li>:没有声明top/bottom,就一直保持相对定位
  • 第4个<li>bottom40px,滚动到距离容器底部40px就固定
  • 第5个<li>bottom0px,滚动到容器底部就固定

当然,换成leftright也一样能实现横向的吸附效果

注意

粘性定位的参照物并不一定是position:fixed

目标元素的任意祖先元素都未声明position:relative|absolute|fixed|sticky,才与position:fixed表现一致。

当离目标元素最近的祖先元素声明了position:relative|absolute|fixed|sticky目标元素就相对该祖先元素进行粘性定位

确认参照物跟position:absolute一致。

兼容

兼容性勉强还行,近2年发版的浏览器都能支持,SafariFirefox的兼容性还是挺赞的。有吸附效果需求的同学建议一试,要兼容IExplorer就算了。

以上就是W3Cschool编程狮关于纯CSS实现吸附效果的相关介绍了,希望对大家有所帮助。

在 JavaScript 中使用 Promises 时最常见的 3 个错误

thbcm阅读(166)

文章转载自公众号:印记中文

原文链接:dev.to/mpodlasin/3-most-common-mistakes-when-using-promises-in-javascript-oab

译者:Shopee 金融前端团队 张铁山

本文对开发者编写 Promise 时常出现的几种错误进行了总结,剖析的一针见血,来看看是不是你平时所写?

时至今日,即使有 async / await 的引入,JavaScriptPromises 的编写规则对于所有的 JS 开发者来说仍然是必不可少的知识。

JavaScript 在处理异步问题上和其它编程语言不同。因此,即使具有丰富经验的开发人员有时也会陷入误区。我亲身看到过优秀的 PythonJava 程序员在为 Node.js 或浏览器编码时犯了非常愚蠢的错误。

为了避免这些错误,JavaScript 中的 Promises 有许多编写细节需要考虑。其中有一些纯粹是语言风格问题,但也有许多是实际引入、难以跟踪的错误。因此,我决定编写一个清单,列出开发人员在使用 Promises 编程时遇到的三个最常见的错误。

将所有内容包装在 Promise 构造函数中

第一个错误也是最为明显的错误之一,但是我发现开发者犯这个错误的频率出奇的高。

当第一次学习 Promises 时,你会了解到 Promise 的构造函数,这个构造函数可以用来创建一个新的 Promises 对象。

也许因为人们通常是通过将一些浏览器 API(例如 setTimeout)包装在 Promise 构造函数中这种方式来开始学习的,所以在他们心中根深蒂固地认为创建 Promise 对象的唯一方法是使用构造函数。

因此,通常会这样写:

const createdPromise = new Promise(resolve => {
  somePreviousPromise.then(result => {
    // 对 result 进行一些操作
    resolve(result);
  });
});

可以看到,为了对 somePreviousPromise 的结果 result 进行一些操作,有些人使用了 then,但是后来决定将其再次包装在一个 Promise 的构造函数中,为的是将该操作的结果存储在 createdPromise 的变量中,大概是为了稍后对该 Promise 进行更多操作。

这显然是没有必要的。then 方法的全部要点在于它本身会返回一个 Promise,它表示的是执行 somePreviousPromise 后再执行then 中的的回调函数,then 的参数是 somePreviousPromise成功执行返回的结果。

所以,上一段代码大致等价于:

const createPromise = somePreviousPromise.then(result => {
  // 对 result 进行一些操作
  return result
})

如此编写,会简洁很多。

但是,为什么我说它只是大致等价呢?区别在哪里?

经验不足且不细心观察的话可能很难发现,实际上两者在错误处理上存在巨大的差异,这种差异比第一段代码的冗余问题更为重要。

假设 somePreviousPromise 出于某些原因失败了且抛出错误。例如,这个 Promise 里发送了一个 HTTP 请求,而 API 响应 500 错误。

事实证明,在上一段代码中,我们将一个 Promise 包装到另一个 Promise 中,我们根本无法捕获该错误。为了解决此问题,我们必须进行以下更改:

const createdPromise = new Promise((resolve, reject) => {
  somePreviousPromise.then(result => {
    // 对 result 进行一些操作
    resolve(result);
  }, reject);
});

我们简单的在回调函数中添加了一个 reject 参数,然后通过将其作为第二个参数传递给 then 的方式来使用它。请务必记住,then 方法接受第二个可选参数来进行错误处理,这一点非常重要。

现在如果 somePreviousPromise 出于某些原因失败了,reject函数将会被调用,并且我们将能够一如往常地处理 createdPromise上的错误。

这样是否解决了所有问题?抱歉,并没有。

我们处理了 somePreviousPromise 自身可能发生的错误,但是我们仍然无法控制作为 then 方法第一个参数的回调函数中发生的情况。在注释区域 // 对 result 进行一些操作 执行的代码可能会有一些错误,如果这块地方的代码抛出任何错误,那 then 方法的第二个参数reject 依旧捕获不到这些错误。

这是因为作为 then 方法的第二个参数的错误处理函数只对 Promise 链上当前 then 之前发生的错误作出响应。

因此,最合适的(也是最终的)解决方案应该如下:

const createdPromise = new Promise((resolve, reject) => {
  somePreviousPromise.then(result => {
    // 对 result 进行一些操作
    resolve(result);
  }).catch(reject);
});

注意,这次我们使用了 catch 方法 —— 因为它将在第一个 then之后被调用,它将捕获到 Promise 链上抛出的所有错误。无论是 somePreviousPromise 还是 then 中的回调失败了,Promise 都将按预期处理这些情况。

从上述示例可以发现,在 Promise 的构造函数中包装代码时,有很多细节问题需要处理。这就是为什么最好使用 then 方法创建新的 Promises 的原因,如第二段代码所示。它不仅看起来优雅,并且还可以帮助我们避免那些极端情况。

串行调用 then 与并行调用 then 的比较

由于许多程序员都有着面向对象的编程背景,因此对他们来说,调用一个方法会更改一个对象,而非创建一个新的对象,这很稀松平常。

这或许也是我看到有人对于「在 Promise 上调用 then 方法时」到底发生了什么会感到困惑的原因。

比较下面两段代码:

const somePromise = createSomePromise();


somePromise
  .then(doFirstThingWithResult)
  .then(doSecondThingWithResult);
const somePromise = createSomePromise();


somePromise
  .then(doFirstThingWithResult);


somePromise
  .then(doSecondThingWithResult);

它们所做之事是否相同?看起来似乎相同,毕竟,两段代码都在 somePromise 上调用了两次 then,对吗?

不,这又是一个非常普遍的误区。实际上,这两段代码做的事情完全不同。如果不完全理解两段代码中正在做的事情,可能会导致出现非常棘手的错误。

正如我们在之前的章节中所说,then 方法会创建一个完全新的、独立的 Promise。这意味着在第一段代码中,第二个 then 方法不是在 somePromise 上调用,而是在一个新的 Promise 对象上调用,这段代码表示等待 somePromise 的状态变为成功后立刻调用 doFirstThingWithResult。然后给新返回的 Promise 实例添加一个回调操作 doSecondThingWithResult

实际上,这两个回调将会一个接着一个地执行 —— 可以确保只有在第一个回调执行完成且没有任何问题之后,才会调用第二个回调。此外,第一个回调将会接收 somePromise 返回的值作为参数,但是第二个回调函数将接收 doFirstThingWithResult 函数返回的值作为参数。

另一方面,在第二段代码中,我们在 somePromise 上调用两次then 方法,基本上忽略了从该方法返回的两个新的 Promises 对象。因为 then 在完全相同的 Promise 实例上被调用了两次,因此我们无法确定首先执行哪个回调,这里的执行顺序是不确定的。

从某种意义上说,这两个回调应该是独立的,并且不依赖于任何先前调用的回调,我有时将其视为 “并行” 的执行。但是,当然,实际上,JS 引擎同一时刻只能执行一个功能 —— 你根本无法知道它们将以什么顺序调用。

两段代码的第二个不同之处是,在第二段代码中 doFirstThingWithResultdoSecondThingWithResult 都会接收到同样的参数 —— somePromise 成功执行返回的结果,两个回调函数的返回值在这个示例中被完全忽略掉了。

创建后立即执行 Promise

这个误区出现的原因也是因为大部分程序员有着丰富的面向对象编程经验。

在面向对象编程的思想中,确保对象的构造函数自身不执行任何操作通常被认为是一种很好的实践。举个例子,一个代表数据库的对象在使用new 关键字调用其构造函数时不应该启动与数据库的链接。

相反,应该提供一个特定的方法,如调用一个名为 init 的方法 —— 它将显式地创建连接。这样,一个对象不会因为已被创建而执行任何期望之外的操作。它会按照程序员的明确要求来执行。

但这「不是 Promises 的工作方式」

考虑如下示例:

const somePromise = new Promise(resolve => {
  // 创建 HTTP 请求
  resolve(result);
});

你可能会认为发出 HTTP 请求的函数未在此处调用,因为它包装在 Promise 构造函数中。实际上,许多程序员希望 somePromise 上执行 then 方法之后它才被调用。

但事实并非如此。创建该 Promise 后,回调将立即执行。这意味着当您在创建 somePromise 变量后进入下一行时,你的 HTTP 请求可能已被执行,或者说已存在执行队列里。

我们说 Promise 是 “eager” 的,因为它尽可能快地执行与其关联的动作。相反,许多人期望 Promises 是 “lazy” 的,即仅在必要时调用(例如,当 then 方法在 Promise 上首次被调用)。这是一个误区,Promise 永远是 eager 的,而非 lazy 的。

但是,如果您想要延迟执行 Promise,应该怎么做?如果您希望延迟发出该 HTTP 请求怎么办?Promises 中是否内置了某种奇特的机制,可以让您执行类似的操作?

答案有时会超出开发者们的期望。函数是一种 lazy 机制。仅当程序员使用 () 语法显式调用它们时,才执行它们。仅仅定义一个函数实际上并不能做任何事情。因此,要使 Promise 成为 “lazy”, 最佳方法是将其简单地包装在函数中!

具体代码如下:

const createSomePromise = () => new Promise(resolve => {
  // 创建 HTTP 请求
  resolve(result);
});

现在,我们将 Promise 构造函数的调用操作包装在一个函数中。事实上它还没有真正被调用。我们还将变量名从 somePromise 更改为 createSomePromise,因为它不再是一个 Promise 对象 —— 而是一个创建并返回 Promise 对象的函数。

Promise 构造函数(以及带有 HTTP 请求的回调函数)仅在执行该函数时被调用。因此,现在我们有了一个 lazy 的 Promise,只有在我们真正想要它执行时才去执行它。

此外,请注意,它还附带提供了另一种功能。我们可以轻松地创建另一个可以执行相同操作的 Promise 对象。

如果出于某些奇怪的原因,我们希望进行两次相同的 HTTP 请求并同时执行这些请求,则只需要两次调用 createSomePromise 函数。又或者,如果请求由于任何原因失败了,我们可以使用相同的函数重新请求。

这表明将 Promises 包装在函数(或方法)中非常方便,因此对于 JavaScript 开发人员来说,使用这种模式开发应该要变得很自然而然。

而讽刺的是,如果你阅读过我写的文章 Promises vs Observables ,你就会知道编写 Rx.js 的程序员经常会犯一个与此相反的错误。他们对 Observable 进行编码,就好像它们是 “eager”(与 Promises 一致),而实际上它们是 ”lazy“ 的。因此,将 Observables 封装在函数或方法中通常没有任何意义,实际上甚至是有害的。

结语

本文展示了我经常看到开发者使用 Promise 时所犯的三种类型的错误,因为他们对 JavaScript 中的 Promises 的理解仅停留在表面。

以上就是W3Cschool编程狮关于在 JavaScript 中使用 Promises 时最常见的 3 个错误的相关介绍了,希望对大家有所帮助。

高效终端命令行工具 – 给你的终端美个容

thbcm阅读(169)

文章转载自公众号:若川视野

在我的微信交流群中听闻很多前端没有买mac电脑(比如我),也没有用过ohmyzsh

这篇文章没啥难度,很快就能看完,主要还是希望读者你看完后可以安装开始使用高效终端工具。有了这么强大的终端工具后我发现Windows很好用呀,瞬间感觉都不需要买mac了。

主要就是:

  • 利用WindowsLinux 子系统功能
  • 安装Ubuntu系统,安装ohmyzsh和一些插件
  • 安装windows Terminal工具
  • 安装vscode remote-wsl插件
  • 安装 tig 查看 git 记录

先看下效果。反正我用了ohmyzsh后,离不开了。

windows 安装 Ubuntu 子系统 and 安装 windows Terminal

搜索启用或关闭 windows 功能,勾选适用于 LinuxWindows 子系统,确定后重启电脑。

搜索 Ubuntuwindows Terminal 并安装(windows Terminal 可能要求win10系统比较高的版本,一般更新到最新版本即可)

Ubuntu安装好后会要求设置用户名和密码。

建议安装最新版 Ubuntu 20.04

win10安装了ubuntu子系统和oh my zsh后,则可以通过/mnt/f/访问win10下的f盘,或者其他盘。也就有上面的pwd效果图。

安装 vscode remote-wsl 插件

下载安装vscode,并且安装remote-wsl插件。

安装 oh my zsh

简述下oh my zshoh my zsh官网的安装方法。github ohmyzsh

安装oh my zsh部分适用于Ubuntumac系统。

echo $SHELL
# /bin/bash 默认是bash
# 查看下有哪些shells
cat /etc/shells
# 安装 zsh
sudo apt-get install zsh -y
# 查看zsh版本
zsh --version
# 5.1.1
# 安装后zsh 后cat /etc/shells 才有/bin/zsh /usr/bin/zsh


# 切换成zsh
chsh -s $(which zsh)
# 或者这条命令
# chsh -s /usr/bin/zsh
# 三种安装方案可供选择:
# Via curl
$ sh -c "$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"


# Via Wget
$ sh -c "$(wget https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh -O -)"


# 如果上面两种情况还安装不了,可以使用如下方式。


# via git clone
git clone https://github.com/ohmyzsh/ohmyzsh.git ~/.oh-my-zsh
cp ~/.oh-my-zsh/templates/zshrc.zsh-template ~/.zshrc
chsh -s $(which zsh)
# 打开新的终端会应用新的zshrc配置

如果以上三种方式还装不了,可以查看oh my zsh文档

安装成功后,配置信息会在~/.zshrc。可以通过code ~/.zshrc(如果没安装vscode,可以用vim ~/.zshrc)打开编辑zsh配置信息,可以看到默认配置了git 插件Plugin:git。也就是说,可以更加简便的使用git 相关的一些别名。

# 比如
`git status`时,只需要输入`gst`。
`git pull` 时,只需要输入`gl`。
`git push` 时,只需要输入`gp`等等。

是因为配置里有这些简写,还有更多可以查看这里oh my zsh plugin git.plugin.zsh文档配置,github oh my zsh plugin git.plugin.zsh文件地址。安装成功后,同时会创建~/.oh-my-zsh的文件夹,其实就是.oh-my-zshgit 仓库master分支。可以发现文件夹中有一个plugins文件夹,内置了很多插件。可以自定义主题theme,具体可以看example.zsh-theme。custom自定义相关。插件相关会安装在这里,更多可以查看example.plugin.zsh

我暂时安装了这几个插件。

plugins=(
  # 内置插件,启用即可
  git
  cp
  mv
  # 按两下esc键,用sudo权限
  sudo
  # 记录历史输入的zsh命令,自动提示,快速使用
  zsh-autosuggestions
  # zsh 命令高亮
  zsh-syntax-highlighting
)

顺带讲下后面两个插件的安装方法 安装 zsh-autosuggestions

git clone git://github.com/zsh-users/zsh-autosuggestions $ZSH_CUSTOM/plugins/zsh-autosuggestions

安装zsh-syntax-highlighting

git clone https://github.com/zsh-users/zsh-syntax-highlighting.git $ZSH_CUSTOM/plugins/zsh-syntax-highlighting

$ZSH_CUSTOM 其实是个变量,代表这个路径~/.oh-my-zsh/custom, 也就是安装到了这个目录下。还有很多高效插件,等您发掘~oh my zsh官网,不过插件安装多了,感觉会有些卡顿~

# 每次修改了这个`.zshrc`配置文件,需要重载一下,才能生效。
source .zshrc
# 也可以封装成一个简写命令 alias
alias rl='source ~/.zshrc'

也可以在这个配置文件中配置更多alias

# ~/.zshrc
# 比如跳转到工作目录
alias dgg='cd /mnt/f/git-source/github'
# vscode 打开要编辑的文件或文件夹
code blog

这也就是开头的效果图了。

那不是win10,也想通过设置别名来提高效率,是不是就没有办法了呢,可以使用git bash设置alias,安装git,自带git bash

windows git bash 设置别名提高效率

windows 桌面或任意资源管理器位置,右击,选择Git Bash Here,即打开了git bash命令行。先设置下主题,右击选择options, Looks > theme > dracula我选择的是dracula主题,看起来比较舒适。还可以设置字体等。

# 跳转到根路径
cd ~
# 查看下是否有.bash_profile文件
la
# 如果没有.bash_profile文件,需要创建
touch .bash_profile
# 打开编辑 (我这里安装了vscode,所以直接用其打开文件)
code .bash_profile

可以根据修改设置一些别名。比如我们常用的git status命令,可以封装成gst。清屏clear命令封装成cls

alias gst='git status'
alias cls='clear'

每次修改这个文件需要输入source ~/.bash_profile重载这个文件,才会生效。code ~/.bash_profile,用vscode编辑这个配置文件,封装成一个命令。比如:

alias rl='source ~/.bash_profile'
alias bashconfig='code ~/.bash_profile'

这样每次修改保存后就只需要输入rl,即可重载生效了。还可以把一些工作目录封装,我的一些项目是放在/f/git-source/github文件下。

# github上的项目
alias dgg='cd F:/git-source/github'
# 周报相关
alias dcwk='cd F:/git-source/coding/weekly'

每次进入项目,就直接dgg,即可跳转到这个目录,然后选择相应的目录即可。比如dgg进入工作目录,cd analyse-vue-cli进入项目目录,(输入analtab键智能提示)

#  /f/git-source/github/analyse-vue-cli (dev)
# 查看状态 git status
gst
# 用vscode 打开这个文件夹,开始编辑~
code ./

git 相关的, 例举一些平时用的比较多的。

alias g='git'
alias ga='git add'
alias gaa='git add -all'
alias gp='git push'
alias gl='git pull'
alias gcmsg='git commit -m'
# 分支相关
alias gb='git branch'
alias gbr='git branch -r'
alias gba='git branch -a'
# checkout
alias gco='git checkout'
alias gcb='git checkout -b'
# merge
alias gm='git merge'
# diff
alias gd='git diff'
alias gdw='git diff --word-diff'

更多可以把oh my zsh的插件一些命令拷贝过来,留alias相关的即可,oh my zsh plugin git.plugin.zshPlugin:git wiki相当于解锁了oh my zshgit插件。还有很多插件,比如npm点击查看, node 等,都可以研究下。

git bash,有没有类似oh my zsh的插件呢,我暂时没发现,如果您知道,欢迎告诉我。目录相关的操作,也可以设置一些别名。比如:

# 回退到上一级
alias ..='cd ..'
alias ...='cd ../..'
alias ....='cd ../../..'
alias .....='cd ../../../..'
alias ......='cd ../../../../..'

更多别名可以自定义~windows上命令行工具还可以使用cmder点击查看cmder官网命令行工具。虽然我几年前就安装了,但发现还是git bash用的顺畅些,可能是我还不太习惯cmder。关于cmder配置相关,这里推荐晚晴幽草轩轩主的一篇文章:《Win下必备神器之Cmder》

也有cygwin,也是windows命令行工具,也可以安装on my zsh

当然,git也是可以设置别名的。

git设置别名,使用tig神器

# git status => git st
git config --global alias.st status

更多可以查看廖雪峰老师的这篇文章git 配置别名。不过可能大多数人不知道,他们可能用着可视化工具。我觉得可视化工具也是对git的一些封装,具体背后是什么命令,我们还是需要去了解熟悉的。命令行使用git,我推荐使用tiggit log增强版,性能很好。Ubuntulinuxmac可以直接安装,windows稍微麻烦些。具体安装方法查看tig github仓库中的官方安装文档,或者查看这篇文章:颠覆 Git 命令使用体验的神器 — tig

小结

磨刀不误砍柴工,花时间折腾研究工具,有利于提高开发效率。

以上就是W3Cschool编程狮关于高效终端命令行工具 – 给你的终端美个容的相关介绍了,希望对大家有所帮助。

阿里面试官:HashMap 熟悉吧?好的,那就来聊聊 Redis 字典吧!

thbcm阅读(181)

文章转载自公众号:Java极客技术 作者:鸭血粉丝

最近,阿粉的一个朋友出去面试,回来跟阿粉抱怨,面试官不按套路出牌,直接打乱了他的节奏。

事情是这样的,前面面试问了几个 Java 的相关问题,我朋友回答还不错,接下来面试官就问了一句:看来 Java 基础还不错,Java HashMap 你熟悉吧?

我朋友回答。工作经常用,有看过源码。

我朋友本来想着,你随便来吧,这个问题之前已经准备好了,随便问吧。

谁知道,面试官下面一句:

「那好的,我们来聊聊 Redis 字典吧。」

直接将他整蒙逼。

阿粉的朋友由于没怎么研究过 Redis 字典,所以这题就直接回答不知道了。

「当然,如果面试中真不知道,那就回答不了解,直接下一题,不要乱答。」

不过这一题,阿粉觉得还是很可惜,其实 Redis 字典基本原理与 HashMap 差不多,那我们其实可以套用这其中的原理,不求回答满分,但是怎么也可以得个及格分吧~

面试过程真要碰到这个问题,我们可以从下面三个方面回答。

  • 数据结构
  • 元素增加过程
  • 扩容

字典数据结构

说起字典,也许大家比较陌生,但是我们都知道 Redis 本身提供 KV 查询的方式,这个 KV 就是其实通过底层就是通过字典保存。

另外,Redis 支持多种数据类型,其中一种类型为 Hash 键,也可以用来存储 KV 数据。

阿粉刚开始了解的这个数据结构的时候,本来以为这个就是使用字典实现。其实并不是这样的,初始创建 Hash 键,默认使用另外一种数据结构-「ZIPLIST」(压缩列表),以此节省内存空间。

不过一旦以下任何条件被满足,Hash 键的数据结构将会变为字典,加快查询速度。

  • 哈希表中某个键或某个值的长度大于 server.hash_max_ziplist_value (默认值为 64 )。
  • 压缩列表中的节点数量大于 server.hash_max_ziplist_entries (默认值为 512 )。

Redis 字典新建时默认将会创建一个哈希表数组,保存两个哈希表。

其中 ht[0] 哈希表在第一次往字典中添加键值时分配内存空间,而另一个 ht[1] 将会在下文中扩容/缩容才会进行空间分配。

字典中哈希表其实就等同于Java HashMap,我们知道 Java 采用数组加链表/红黑树的实现方式,其实哈希表也是使用类似的数据结构。

哈希表结构如下所示:

其中 table 属性是个数组, 其中数组元素保存一种 dictEntry的结构,这个结构完全类似与 HashMap 中的 Entry 类型,这个结构存储一个 KV 键值对。

同时,为了解决 hash 碰撞的问题,dictEntry 存在一个 next 指针,指向下一个dictEntry ,这样就形成 dictEntry 的链表。

现在,我们回头对比 JavaHashMap,可以发现两者数据结构基本一致。

只不过 HashMap 为了解决链表过长问题导致查询变慢,JDK1.8 时在链表元素过多时采用红黑树的数据结构。

下面我们开始添加新元素,了解这其中的原理。

元素增加过程

当我们往一个新字典中添加元素,默认将会为字典中 ht[0] 哈希表分配空间,默认情况下哈希表 table 数组大小为 4(「DICT_HT_INITIAL_SIZE」)。

新添加元素的键值将会经过哈希算法,确定哈希表数组的位置,然后添加到相应的位置,如图所示:

继续增加元素,此时如果两个不同键经过哈希算法产生相同的哈希值,这样就发生了哈希碰撞。

假设现在我们哈希表中拥有是三个元素,:

我们再增加一个新元素,如果此时刚好在数组 3 号位置上发生碰撞,此时 Redis 将会采用链表的方式解决哈希碰撞。

「注意,新元素将会放在链表头结点,这么做目的是因为新增加的元素,很大概率上会被再次访问,放在头结点增加访问速度。」

这里我们在对比一下元素添加过程,可以发现 Redis 流程其实与 JDK 1.7 版本的 HashMap 类似。

当我们元素增加越来越多时,哈希碰撞情况将会越来越频繁,这就会导致链表长度过长,极端情况下 O(1) 查询效率退化成 O(N) 的查询效率。

为此,字典必须进行扩容,这样就会使触发字典 rehash 操作。

扩容

当 Redis 进行 Rehash 扩容操作,首先将会为字典没有用到 ht[1]哈希表分配更大空间。

画外音:ht[1] 哈希表大小为第一个大于等于ht[0].used*2 的 2^2(2的n 次方幂)

然后再将 ht[0] 中所有键值对都迁移到 ht[1] 中。

简单起见,忽略指向空节点

当节点全部迁移完毕,将会释放 ht[0]占用空间,并将 ht[1] 设置为 ht[0]

扩容 操作需要将 ht[0]所有键值对都 Rehashht[1] 中,如果键值过多,假设存在十亿个键值对,这样一次性的迁移,势必导致服务器会在一段时间内停止服务。

另外如果每次 rehash 都会阻塞当前操作,这样对于客户端处理非常不友好。

为了避免 rehash对服务器的影响,Redis 采用渐进式的迁移方式,慢慢将数据迁移分散到多个操作步骤。

这个操作依赖字典中一个属性 rehashidx,这是一个索引位置计数器,记录下一个哈希表 table 数组上元素,默认情况为值为 「-1」

假设此时扩容前字典如图所示:

当开始 rehash 操作,rehashidx将会被设置为 「0」

这个期间每次收到增加,删除,查找,更新命令,除了这些命令将会被执行以外,还会顺带将 ht[0]哈希表在 rehashidx 位置的元素 rehash 到 ht[1] 中。

假设此时收到一个 「K3」 键的查询操作,Redis 首先执行查询操作,接着 Redis 将会为 ht[0]哈希表上table 数组第 rehashidx索引上所有节点都迁移到 ht[1] 中。

当操作完成之后,再将 rehashidx 属性值加 1。

最后当所有键值对都 rehashht[1]中时,rehashidx将会被重新设置为 -1。

虽然渐进式的 rehash 操作减少了工作量,但是却带来键值操作的复杂度。

这是因为在渐进式 rehash 操作期间,Redis 无法明确知道键到底在ht[0]中,还是在 ht[1] 中,所以这个时候 Redis 不得不查找两个哈希表。

以查找为例,Redis 首先查询 ht[0] ,如果没找到将会继续查找 ht[1],除了查询以外,更新,删除也会执行如上的操作。

添加操作其实就没这么麻烦,因为ht[0]不会在使用,那就统一都添加到 ht[1] 中就好了。

最后我们再对比一下 Java HashMap 扩容操作,它是一个一次性操作,每次扩容需要将所有键值对都迁移到新的数组中,所以如果数据量很大,消耗时间就会久。

总结

Redis 字典使用哈希表作为底层实现,每个字典包含两个哈希表,一个平时使用,一个仅在 rehash 操作中使用。

哈希表总的来说,跟 Java HashMap 真的很类似,底层实现也是一个数组加链表数据结构。

最后,当对哈希表进行扩容操作时间,将会采用渐进性 rehash 操作,慢慢将所有键值对迁移到新哈希表中。

其实了解 Redis 字典的其中的原理,再去比较 Java HashMap ,其实可以发现这两者有如此多的相似点。

所以学习这类知识时,不要仅仅去背,我们要了解其底层原理,知其然知其所以然。

以上就是W3Cschool编程狮关于阿里面试官:HashMap 熟悉吧?好的,那就来聊聊 Redis 字典吧!的相关介绍了,希望对大家有所帮助。

Vue 3.0 有了它,Vuex或许可以提前退休了

thbcm阅读(139)

文章转载自公众号:code秘密花园

Vuex 是一个很棒的状态管理库。它很简单,并与 Vue 集成的非常好。为什么会有人放弃Vuex ? 原因可能是即将发布的 Vue3 版本公开了底层的响应式系统,并介绍了构建应用程序的新方法。新的响应式系统非常强大,它可以直接用于集中的状态管理。

你需要状态共享吗?

在某些情况下,多个组件之间的数据流转变得非常困难,因此需要集中的状态管理。这些情况包括:

  • 多个组件使用相同数据的
  • 组件深嵌套

如果以上情况都不成立,答案很简单,你不再需要状态共享了。

但是,如果你有以上一种情况呢?最直接的答案就是使用 Vuex 。这是一个久经考验的解决方案,而且效果不错。

但是,如果你不想添加其他依赖项或发现设置过于复杂怎么办?新的Vue3 版本以及 Composition API 可以通过其内置方法解决这些问题。

新的解决方案

共享状态必须符合两个条件:

  • 响应式:当状态改变时,使用它们的组件也应更新
  • 可用性:可以在任何组件中访问状态

响应式

Vue3 通过众多功能公开了其响应式系统。你可以使用 reactive 函数创建响应式变量(替代方法是 ref 函数)。

import { reactive } from 'vue';


export const state = reactive({ counter: 0 });

reactive 函数返回的 Proxy 对象是可以跟踪其属性更改的对象。在组件模板中使用时,当响应值发生更改时,组件都会重新渲染。

<template>
  <div>{{ state.counter }}</div>
  <button type="button" @click="state.counter++">Increment</button>
</template>


<script>
  import { reactive } from 'vue';


  export default {
    setup() {
      const state = reactive({ counter: 0 });
      return { state };
    }
  };
</script>

可用性

上面的示例对于单个组件非常有用,但是其他组件无法访问状态。为了克服这个问题,你可以使用 provideinject 方法,使 Vue 3 应用中任何指都能访问到。

import { reactive, provide, inject } from 'vue';


export const stateSymbol = Symbol('state');
export const createState = () => reactive({ counter: 0 });


export const useState = () => inject(stateSymbol);
export const provideState = () => provide(
  stateSymbol, 
  createState()
);

当您将 Symbol 作为键和值传递给 provide 方法时,该方法中的任何子组件都可以使用该值。Symbol 提供和检索值时,key 使用相同的名称。

这样,如果你在最顶层的组件上提供值,那么它将在所有组件中可用。另外,还可以在主应用程序实例上调用 provide

import { createApp, reactive } from 'vue';
import App from './App.vue';
import { stateSymbol, createState } from './store';


const app = createApp(App);
app.provide(stateSymbol, createState());
app.mount('#app');
<script>
  import { useState } from './state';


  export default {
    setup() {
      return { state: useState() };
    }
  };
</script>

让代码更加健壮

上面的解决方案有效,但有一个缺点:你不知道是谁修改了什么。状态可以直接更改,没有限制。

你可以使用 readonly 函数将状态包装起来,用以保护状态。它覆盖了在Proxy 对象中传递的变量,该代理对象阻止任何修改(在尝试修改时发出警告)。这些变化可以由能够访问可写存储的单独函数来处理。

import { reactive, readonly } from 'vue';


export const createStore = () => {
  const state = reactive({ counter: 0 });
  const increment = () => state.counter++;


  return { increment, state: readonly(state) };
}

外部将只能访问只读状态,并且只有导出的函数可以修改可写状态。

通过保护状态免受不必要的修改,新解决方案相对接近 Vuex

总结

通过使用 Vue 3 的响应式系统和依赖项注入机制,我们已经从本地状态转变为可以在较小的应用程序中替代 Vuex 的集中状态管理。

现在我们有;一个状态对象,该对象是只读的,并且可以对模板的更改作出响应。状态只能通过特定的方法来修改,比如 Vuex 中的 actions/mutations。可以使用 computed 函数定义其他 getter

Vuex 具有更多的功能,例如模块处理,但有时我们并不需要。

以上就是W3Cschool编程狮关于Vue 3.0 有了它,Vuex或许可以提前退休了的相关介绍了,希望对大家有所帮助。

3种SpringBoot时间格式化的方法,轻松减少你的代码量

thbcm阅读(180)

文章转载自公众号:程序员内点事

时间格式化在项目中使用频率是非常高的,当我们的 API 接口返回结果,需要对其中某一个 date 字段属性进行特殊的格式化处理,通常会用到 SimpleDateFormat 工具处理。

SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
Date stationTime = dateFormat.parse(dateFormat.format(PayEndTime()));

可一旦处理的地方较多,不仅 CV 操作频繁,还产生很多重复臃肿的代码,而此时如果能将时间格式统一配置,就可以省下更多时间专注于业务开发了。

可能很多人觉得统一格式化时间很简单啊,像下边这样配置一下就行了,但事实上这种方式只对 date 类型生效。

spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8

而很多项目中用到的时间和日期API 比较混乱, java.util.Datejava.util.Calendarjava.time LocalDateTime 都存在,所以全局时间格式化必须要同时兼容性新旧 API

看看配置全局时间格式化前,接口返回时间字段的格式。

@Data
public class OrderDTO {


    private LocalDateTime createTime;


    private Date updateTime;
}

很明显不符合页面上的显示要求(有人抬杠为啥不让前端解析时间,我只能说睡服代码比说服人容易得多~

一、@JsonFormat 注解

@JsonFormat 注解方式严格意义上不能叫全局时间格式化,应该叫部分格式化,因为@JsonFormat 注解需要用在实体类的时间字段上,而只有使用相应的实体类,对应的字段才能进行格式化。

@Data
public class OrderDTO {


    @JsonFormat(locale = "zh", timezone = "GMT+8", pattern = "yyyy-MM-dd")
    private LocalDateTime createTime;


    @JsonFormat(locale = "zh", timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
    private Date updateTime;
}

字段加上 @JsonFormat 注解后,LocalDateTimeDate 时间格式化成功。

二、@JsonComponent 注解(推荐)

这是我个人比较推荐的一种方式,前边看到使用 @JsonFormat 注解并不能完全做到全局时间格式化,所以接下来我们使用 @JsonComponent注解自定义一个全局格式化类,分别对 DateLocalDate 类型做格式化处理。

@JsonComponent
public class DateFormatConfig {


    @Value("${spring.jackson.date-format:yyyy-MM-dd HH:mm:ss}")
    private String pattern;


    /**
     * @author xiaofu
     * @description date 类型全局时间格式化
     * @date 2020/8/31 18:22
     */
    @Bean
    public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilder() {


        return builder -> {
            TimeZone tz = TimeZone.getTimeZone("UTC");
            DateFormat df = new SimpleDateFormat(pattern);
            df.setTimeZone(tz);
            builder.failOnEmptyBeans(false)
                    .failOnUnknownProperties(false)
                    .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
                    .dateFormat(df);
        };
    }


    /**
     * @author xiaofu
     * @description LocalDate 类型全局时间格式化
     * @date 2020/8/31 18:22
     */
    @Bean
    public LocalDateTimeSerializer localDateTimeDeserializer() {
        return new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(pattern));
    }


    @Bean
    public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
        return builder -> builder.serializerByType(LocalDateTime.class, localDateTimeDeserializer());
    }
}

看到 DateLocalDate 两种时间类型格式化成功,此种方式有效。

但还有个问题,实际开发中如果我有个字段不想用全局格式化设置的时间样式,想自定义格式怎么办?

那就需要和 @JsonFormat 注解配合使用了。

@Data
public class OrderDTO {


    @JsonFormat(locale = "zh", timezone = "GMT+8", pattern = "yyyy-MM-dd")
    private LocalDateTime createTime;


    @JsonFormat(locale = "zh", timezone = "GMT+8", pattern = "yyyy-MM-dd")
    private Date updateTime;
}

从结果上我们看到 @JsonFormat 注解的优先级比较高,会以 @JsonFormat 注解的时间格式为主。

三、@Configuration 注解

这种全局配置的实现方式与上边的效果是一样的。

注意:在使用此种配置后,字段手动配置@JsonFormat 注解将不再生效。

@Configuration
public class DateFormatConfig2 {


    @Value("${spring.jackson.date-format:yyyy-MM-dd HH:mm:ss}")
    private String pattern;


    public static DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");


    @Bean
    @Primary
    public ObjectMapper serializingObjectMapper() {
        ObjectMapper objectMapper = new ObjectMapper();
        JavaTimeModule javaTimeModule = new JavaTimeModule();
        javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer());
        javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer());
        objectMapper.registerModule(javaTimeModule);
        return objectMapper;
    }


    /**
     * @author xiaofu
     * @description Date 时间类型装换
     * @date 2020/9/1 17:25
     */
    @Component
    public class DateSerializer extends JsonSerializer<Date> {
        @Override
        public void serialize(Date date, JsonGenerator gen, SerializerProvider provider) throws IOException {
            String formattedDate = dateFormat.format(date);
            gen.writeString(formattedDate);
        }
    }


    /**
     * @author xiaofu
     * @description Date 时间类型装换
     * @date 2020/9/1 17:25
     */
    @Component
    public class DateDeserializer extends JsonDeserializer<Date> {


        @Override
        public Date deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
            try {
                return dateFormat.parse(jsonParser.getValueAsString());
            } catch (ParseException e) {
                throw new RuntimeException("Could not parse date", e);
            }
        }
    }


    /**
     * @author xiaofu
     * @description LocalDate 时间类型装换
     * @date 2020/9/1 17:25
     */
    public class LocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {
        @Override
        public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
            gen.writeString(value.format(DateTimeFormatter.ofPattern(pattern)));
        }
    }


    /**
     * @author xiaofu
     * @description LocalDate 时间类型装换
     * @date 2020/9/1 17:25
     */
    public class LocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {
        @Override
        public LocalDateTime deserialize(JsonParser p, DeserializationContext deserializationContext) throws IOException {
            return LocalDateTime.parse(p.getValueAsString(), DateTimeFormatter.ofPattern(pattern));
        }
    }
}

总结

分享了一个简单却又很实用的 Springboot 开发技巧,其实所谓的开发效率,不过是一个又一个开发技巧堆砌而来,聪明的程序员总是能用最少的代码完成任务。

以上就是W3Cschool编程狮关于3种SpringBoot时间格式化的方法,轻松减少你的代码量的相关介绍了,希望对大家有所帮助。

10个简单的技巧让你的 vue.js 代码更优雅

thbcm阅读(175)

文章转载自公众号:前端人

前言

作为深度代码洁癖,我们都希望能写出简单高效的代码,让我们的代码看起来更加优雅,让我们抛弃繁杂的代码,一起开启简单的旅程~~

slots 新语法向 3.0 看齐

使用带有“#”的新命名插槽缩写语法,在Vue 2.6.0+中可用

举个例子:

构建插槽时,最好规划一下布局。这就是我的文章布局。构建插槽与构建组件没有什么不同。本质上,插槽是具有超强功能的组件,让我们细分一下上面的布局,组件的外观如下:

<!-- TidbitPage.vue -->
<template>
  <article-layout>


    <template #articleHeader>
      <h1>I am the header</h1>
    </template>


    <template #articleContent>
      <p>I am the content</p>
    </template>


    <template #articleFooter>
      <footer>I am the footer</footer>
    </template>


    <template #side>
      <aside>I am the side stuff</aside>
    </template>


    <template #banner>
      <div>I am the banner</div>
    </template>


  </article-layout>
<template>

动态指令参数

指令参数现在可以接受动态 JavaScript 表达式 动态参数值应该是字符串,但允许null作为一个明确指示应该删除绑定的特殊值,那将会很方便。任何其他非字符串值都可能出错,并会触发警告。(仅适用于v-bind和v-on

<div v-bind:[attr]="attributeName"></div>
//简写
<div :[attr]="attributeName"></div>

这里的 attributeName 会被作为一个JavaScript表达式进行动态求值,求得的值将会作为最终的参数来使用。例如,如果你的 Vue 实例有一个 data 属性 attributeName,其值为 href,那么这个绑定将等价于 v-bind:href

同样地,你可以使用动态参数为一个动态的事件名绑定处理函数:

<button v-on:[eventName]="handler"></button>
//简写
<button @[eventName]="handler"></button>

eventName 的值为 focus 时,v-on:[eventName] 将等价于v-on:focus

同样可以适用于插槽绑定:

<my-component>
<template v-slot:[slotName]>
Dynamic slot name
</template>
</my-component>
//简写
<foo>
<template #[name]>
Default slot
</template>
</foo>

动态参数预期会求出一个字符串,异常情况下值为 null。这个特殊的 null 值可以被显性地用于移除绑定。任何其它非字符串类型的值都将会触发一个警告。

<!-- 这会触发一个编译警告 且 无效 -->
<a v-bind:['foo' + bar]="value"> ... </a>

变通的办法是使用没有空格或引号的表达式,或用计算属性替代这种复杂表达式。

@hook那些事

处理组件内定时器的步骤。通常我们一般都是这样操作的:

<script>
  export default {
    mounted() {
      this.timer = setInterval(() => { ... }, 1000);
    },
    beforeDestroy() {
      clearInterval(this.timer);
    }
  };
</script>

但是其实更好的做法是:

<script>
  export default {
    mounted() {
      const timer = setInterval(() => { ... }, 1000);
      this.$once('hook:beforeDestroy', () => clearInterval(timer);)
    }
  };
</script>

设想一个场景 如果我们需要在数据渲染到页面的之前让页面 loadingmounted 之后停止 loadingbeforeUpdata 时开始 loadingupdatad 之后停止 loading

最简单的方法就是改写组件的生命周期函数,使其在 mounted/beforeUpdata /updatad 时通知父组件显示或者隐藏 loading。

这样做显示不好,因为侵入了自组件的逻辑,增加的逻辑也和组件本身的功能好不关联。最好的办法就是使用 v-on="hook:xxx" 的方式:

<v-chart
    @hook:mounted="loading = false"
    @hook:beforeUpdated="loading = true"
    @hook:updated="loading = false"
    :data="data"
/>

这样,就实现了对子组件生命周期的监听。对任意的组件都有效果,包括引入的第三方组件。

vue中的$props$attrs$listeners(可用来二次封装组件)

$props:当前组件接收到的 props 对象。Vue 实例代理了对其 props 对象属性的访问。

假如有个 input 输入框。我们有很多的原生属性,比如:name、placeholder、disabled 等等。我们如果都定义在props显示接收,未免太过繁琐。所以官网出现了:v-bind="$props"这样的操作。如果父组件传递很多的原生属性,那么我们在子组件中直接可以:

//good
<input v-bind="$props">

 
//bad
//而不是下面这样,如果很多的属性就特别繁琐
<input :name="name" :placeholder="placeholder" :disabled="disabled">

我们可以利用以下方式$attrs 将原生属性直接传递给子组件,这是 Vue 在2.4.0新增的属性,包含了父作用域中不作为 prop 被识别 (且获取) 的特性绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind=”$attrs” 传入内部组件——在创建高级别的组件时非常有用。

<input
   v-bind="$attrs"
   :value="value"
   @focus=$emit('focus', $event)"
   @input="$emit('input', $event.target.value)"
>

$listeners:包含了父作用域中的 (不含 .native修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件——在创建更高层次的组件时非常有用。

如果子组件不在父组件的根目录下,则可以将所有事件侦听器从父组件传递到子组件,如下所示:

<template>
<div>
  ...
<childComponent v-on="$listeners" />...
</div>
</template>

响应式数据(2.6.0新增)

我们习惯于用 Vuex 去解决状态的共享问题,但是在小项目中使用就会有增大代码体积和将代码复杂化的烦恼,所以在后来的版本中Vue新增

Vue.observable( object ) 让一个对象可响应,Vue 内部会用它来处理 data 函数返回的对象。

返回的对象可以直接用于渲染函数和 计算属性 内,并且会在发生改变时触发相应的更新。也可以作为最小化的跨组件状态存储器,用于简单的场景:

官方示例:

const state = Vue.observable({ count: 0 })


const Demo = {
  render(h) {
    return h('button', {
      on: { click: () => { state.count++ }}
    }, `count is: ${state.count}`)
  }
}

jsx模板组件

以下面的一组状态判断按钮为例,我们很容易就下意识地在模板内写下这种代码

<button v-if="status === 1" class="btn1" :class="status === 1" @click="">未开始</button>
<button v-if="status === 2" class="btn2" :class="status === 2" @click="">进行中</button>
<button v-if="status === 3" class="btn3" :class="status === 3" @click="">可领取</button>
<button v-if="status === 4" class="btn4" :class="status === 4" @click="">已领取</button>

但是如果我们利用渲染函数可以将上面的代码抽取成优雅的使用组件

<!DOCTYPE html>
<html lang="en">


<body>
    <div id="app">
        <child :status="status"></child>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script>
        Vue.component('child', {
            props: {
                status: {
                    type: Number,
                    required: true
                }
            },
            render(createElement) {
                const innerHTML = ['未开始', '进行中', '可领取', '已领取'][this.status]
                return createElement('button', {
                    class: {
                        active: this.status
                    },
                    attrs: {
                        id: 'btn'
                    },
                    domProps: {
                        innerHTML
                    },
                    on: {
                        click: () => console.log(this.status)
                    }
                })
            }
        })
        var app = new Vue({
            el: '#app',
            data: {
                status: 0
            }
        })
    </script>
</body>


</html>

我们将所有的逻辑封装进渲染函数内,外部只需要传递一个状态参数即可改变

<child :status="status"></child>

动态组件

通过 Vue 的 元素加一个特殊的 is attribute 可以实现动态组件的效果

如图,这是一个 v-for 渲染的列表(只是目前这个版块才刚开始做,目前只有一个),圆圈内的就是一个组件,也就是要 v-for 动态组件

实际使用

一开始就是基本的组件引入了

import ColorIn from '@/components/Magic/ColorIn.vue'
import LineIn from "@/components/Magic/LineIn.vue";
import Header from "@/components/Magic/Header.vue";
import Footer from "@/components/Magic/Footer.vue";


export default{
      components:{
        ColorIn,
        LineIn,
        Header,
        Footer
    }
}

接下来就是动态 v-for 动态组件的使用,componentList:['ColorIn','LineIn','Header','Footer']使用下面的代码即可将代码依次循环

<component v-for="(item,index) in componentList" :key="index" :is="item"></component>

编译以后的效果就是

<ColorIn></ColorIn>
<LineIn></LineIn>
<Header></Header>
<Footer></Footer>

Vue.filter

简单介绍一下过滤器,顾名思义,过滤就是一个数据经过了这个过滤之后出来另一样东西,可以是从中取得你想要的,或者给那个数据添加点什么装饰,那么过滤器则是过滤的工具。例如,从[‘abc’,’abd’,’ade’]数组中取得包含‘ab’的值,那么可通过过滤器筛选出来‘abc’和‘abd’;把‘Hello’变成‘Hello World’,那么可用过滤器给值‘Hello’后面添加上‘ World’;或者把时间节点改为时间戳等等都可以使用过滤器。

场景:时间戳转化成年月日这是一个公共方法,所以可以抽离成过滤器使用

// 使用
// 在双花括号中
{{ message | capitalize }}


// 在 `v-bind` 中
<div v-bind:id="rawId | formatId"></div>


// 全局注册
Vue.filter('stampToYYMMDD', (value) =>{
  // 处理逻辑
})


// 局部注册
filters: {
  stampToYYMMDD: (value)=> {
    // 处理逻辑
  }
}


// 多个过滤器全局注册
// /src/common/filters.js
let dateServer = value => value.replace(/(\d{4})(\d{2})(\d{2})/g, '$1-$2-$3')
export { dateServer }
// /src/main.js
import * as custom from './common/filters/custom'
Object.keys(custom).forEach(key => Vue.filter(key, custom[key]))

.sync 语法糖

sync 就是为了实现prop 进行“双向绑定”仅此而已(父对子,子对父,来回传)

当你有需要在子组件修改父组件值的时候这个方法很好用,它的实现机制和v-model是一样的。

利用 object.freeze 提升性能

Object.freeze() 方法可以冻结一个对象。一个被冻结的对象再也不能被修改;冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。此外,冻结一个对象后该对象的原型也不能被修改。 freeze() 返回和传入的参数相同的对象。

比方我们需要渲染一个非常大的数组对象,例如用户列表,对象列表,文章列表等等。

export default {
  data: () => ({
    users: {}
  }),
  async created() {
    const users = await axios.get("/api/users");
    this.users = users;
  }
};

vue 会将 data 对象中的所有的属性加入到 vue 的响应式系统中,当这些属性的值发生改变时,视图将会产生 响应,若对象的体积比较大,会消耗很多浏览器解析时间。

所以我们可以通过减少数据的响应式转换来提供前端的性能。

export default {
  data: () => ({
    users: {}
  }),
  async created() {
    const users = await axios.get("/api/users");
    this.users = Object.freeze(users);
  }
};

以上就是W3Cschool编程狮关于10个简单的技巧让你的 vue.js 代码更优雅的相关介绍了,希望对大家有所帮助。

MySQL的主键为啥不能用uuid

thbcm阅读(173)

原文链接:cnblogs.com/wyq178/p/12548864.html

前言

mysql 中设计表的时候, mysql 官方推荐不要使用 uuid 或者不连续不重复的雪花id (long形且唯一,单机递增),而是推荐连续自增的主键 id,官方的推荐是 auto_increment ,那么为什么不建议采用 uuid ,使用 uuid 究竟有什么坏处?

本篇文章我们就来分析这个问题,探讨一下内部的原因。

一、mysql和程序实例

1.1.要说明这个问题,我们首先来建立三张表

分别是user_auto_key,user_uuid,user_random_key,分别表示自动增长的主键,uuid作为主键,随机key作为主键,其它我们完全保持不变.

根据控制变量法,我们只把每个表的主键使用不同的策略生成,而其他的字段完全一样,然后测试一下表的插入速度和查询速度:

注:这里的随机key其实是指用雪花算法算出来的前后不连续不重复无规律的id:一串18位长度的long值

id自动生成表:

用户uuid表

随机主键表:

1.2.光有理论不行,直接上程序,使用spring的jdbcTemplate来实现增查测试:

技术框架:springboot+jdbcTemplate+junit+hutool,程序的原理就是连接自己的测试数据库,然后在相同的环境下写入同等数量的数据,来分析一下insert插入的时间来进行综合其效率,为了做到最真实的效果,所有的数据采用随机生成,比如名字、邮箱、地址都是随机生成。

package com.wyq.mysqldemo;
import cn.hutool.core.collection.CollectionUtil;
import com.wyq.mysqldemo.databaseobject.UserKeyAuto;
import com.wyq.mysqldemo.databaseobject.UserKeyRandom;
import com.wyq.mysqldemo.databaseobject.UserKeyUUID;
import com.wyq.mysqldemo.diffkeytest.AutoKeyTableService;
import com.wyq.mysqldemo.diffkeytest.RandomKeyTableService;
import com.wyq.mysqldemo.diffkeytest.UUIDKeyTableService;
import com.wyq.mysqldemo.util.JdbcTemplateService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.util.StopWatch;
import java.util.List;
@SpringBootTest
class MysqlDemoApplicationTests {


    @Autowired
    private JdbcTemplateService jdbcTemplateService;


    @Autowired
    private AutoKeyTableService autoKeyTableService;


    @Autowired
    private UUIDKeyTableService uuidKeyTableService;


    @Autowired
    private RandomKeyTableService randomKeyTableService;




    @Test
    void testDBTime() {


        StopWatch stopwatch = new StopWatch("执行sql时间消耗");




        /**
         * auto_increment key任务
         */
        final String insertSql = "INSERT INTO user_key_auto(user_id,user_name,sex,address,city,email,state) VALUES(?,?,?,?,?,?,?)";


        List<UserKeyAuto> insertData = autoKeyTableService.getInsertData();
        stopwatch.start("自动生成key表任务开始");
        long start1 = System.currentTimeMillis();
        if (CollectionUtil.isNotEmpty(insertData)) {
            boolean insertResult = jdbcTemplateService.insert(insertSql, insertData, false);
            System.out.println(insertResult);
        }
        long end1 = System.currentTimeMillis();
        System.out.println("auto key消耗的时间:" + (end1 - start1));


        stopwatch.stop();




        /**
         * uudID的key
         */
        final String insertSql2 = "INSERT INTO user_uuid(id,user_id,user_name,sex,address,city,email,state) VALUES(?,?,?,?,?,?,?,?)";


        List<UserKeyUUID> insertData2 = uuidKeyTableService.getInsertData();
        stopwatch.start("UUID的key表任务开始");
        long begin = System.currentTimeMillis();
        if (CollectionUtil.isNotEmpty(insertData)) {
            boolean insertResult = jdbcTemplateService.insert(insertSql2, insertData2, true);
            System.out.println(insertResult);
        }
        long over = System.currentTimeMillis();
        System.out.println("UUID key消耗的时间:" + (over - begin));


        stopwatch.stop();




        /**
         * 随机的long值key
         */
        final String insertSql3 = "INSERT INTO user_random_key(id,user_id,user_name,sex,address,city,email,state) VALUES(?,?,?,?,?,?,?,?)";
        List<UserKeyRandom> insertData3 = randomKeyTableService.getInsertData();
        stopwatch.start("随机的long值key表任务开始");
        Long start = System.currentTimeMillis();
        if (CollectionUtil.isNotEmpty(insertData)) {
            boolean insertResult = jdbcTemplateService.insert(insertSql3, insertData3, true);
            System.out.println(insertResult);
        }
        Long end = System.currentTimeMillis();
        System.out.println("随机key任务消耗时间:" + (end - start));
        stopwatch.stop();




        String result = stopwatch.prettyPrint();
        System.out.println(result);
    }

1.3.程序写入结果

user_key_auto写入结果:

user_random_key写入结果:

user_uuid表写入结果:

1.4.效率测试结果

在已有数据量为130W的时候:我们再来测试一下插入10w数据,看看会有什么结果:

可以看出在数据量100W左右的时候,uuid的插入效率垫底,并且在后序增加了130W的数据,uudi的时间又直线下降。

时间占用量总体可以打出的效率排名为:auto_key>random_key>uuid,uuid的效率最低,在数据量较大的情况下,效率直线下滑。那么为什么会出现这样的现象呢?带着疑问,我们来探讨一下这个问题:

二、使用uuid和自增id的索引结构对比

2.1.使用自增id的内部结构

自增的主键的值是顺序的,所以Innodb把每一条记录都存储在一条记录的后面。当达到页面的最大填充因子时候(innodb默认的最大填充因子是页大小的15/16,会留出1/16的空间留作以后的 修改):

  1. 下一条记录就会写入新的页中,一旦数据按照这种顺序的方式加载,主键页就会近乎于顺序的记录填满,提升了页面的最大填充率,不会有页的浪费
  2. 新插入的行一定会在原有的最大数据行下一行,mysql定位和寻址很快,不会为计算新行的位置而做出额外的消耗
  3. 减少了页分裂和碎片的产生

2.2.使用uuid的索引内部结构

因为uuid相对顺序的自增 id 来说是毫无规律可言的,新行的值不一定要比之前的主键的值要大,所以innodb无法做到总是把新行插入到索引的最后,而是需要为新行寻找新的合适的位置从而来分配新的空间。

这个过程需要做很多额外的操作,数据的毫无顺序会导致数据分布散乱,将会导致以下的问题:

  1. 写入的目标页很可能已经刷新到磁盘上并且从缓存上移除,或者还没有被加载到缓存中,innodb在插入之前不得不先找到并从磁盘读取目标页到内存中,这将导致大量的随机 IO
  2. 因为写入是乱序的,innodb不得不频繁的做页分裂操作,以便为新的行分配空间,页分裂导致移动大量的数据,一次插入最少需要修改三个页以上
  3. 由于频繁的页分裂,页会变得稀疏并被不规则的填充,最终会导致数据会有碎片

在把随机值(uuid和雪花id)载入到聚簇索引(innodb默认的索引类型)以后,有时候会需要做一次OPTIMEIZE TABLE来重建表并优化页的填充,这将又需要一定的时间消耗。

结论:使用innodb应该尽可能的按主键的自增顺序插入,并且尽可能使用单调的增加的聚簇键的值来插入新行

2.3.使用自增id的缺点

那么使用自增的 id 就完全没有坏处了吗?并不是,自增 id 也会存在以下几点问题:

  1. 别人一旦爬取你的数据库,就可以根据数据库的自增 id 获取到你的业务增长信息,很容易分析出你的经营情况
  2. 对于高并发的负载,innodb在按主键进行插入的时候会造成明显的锁争用,主键的上界会成为争抢的热点,因为所有的插入都发生在这里,并发插入会导致间隙锁竞争
  3. Auto_Increment锁机制会造成自增锁的抢夺,有一定的性能损失

附:Auto_increment的锁争抢问题,如果要改善需要调优innodb_autoinc_lock_mode的配置

三、总结

本篇文章首先从开篇的提出问题,建表到使用jdbcTemplate去测试不同 id 的生成策略在大数据量的数据插入表现,然后分析了 id 的机制不同在 mysql 的索引结构以及优缺点,深入的解释了为何uuid和随机不重复 id 在数据插入中的性能损耗,详细的解释了这个问题。

在实际的开发中还是根据 mysql 的官方推荐最好使用自增 id,mysql 博大精深,内部还有很多值得优化的点需要我们学习。

以上就是W3Cschool编程狮关于MySQL的主键为啥不能用uuid的相关介绍了,希望对大家有所帮助。

联系我们