世界上有10种人,一种人懂二进制,一种人不懂。
都知道程序的世界其实就是二进制的世界,一切的一切都是0和1。但是印象当中的二进制貌似都是黑客门用来耍酷的,我们普通程序员只能使用 高级语言 写写我们的CRUD。
二进制是他们的,我们(CRUD Boy)什么也没有。
我想告诉你们的是,二进制也可以是我们的。Please Follow Me。坐稳了,我要开车了。
1.从场景出发
1.1 问题
如下图,有一个快时尚品牌要做衣服的人工推荐,就给衣服做了个主题系列的分类。这样在衣服的详情页就可以直接推荐该衣服相关主题的其他的衣服了,而且可以直接从主题系列的菜单栏直接查看各种主题以及主题下的所有衣服。(如格子衬衫可以组合为为 一个"程序员主题",异装奇服可以组合为一个"万圣节主题")
但是这里有一个问题,就是主题内的商品如果没有库存,或者没有上架(等等,还有其他条件)是不能展示给用户的。这样才能更有效地推荐给用户,保证用户体验。
传统单体模式下,我想这个问题很容易解决,无非就是加一些过滤条件就好了。但是在微服务模式下,商品的状态不一定在商品中心维护。如商品的库存就会在库存中心维护,商品是否可以售卖可能在营销中心维护。
1.2 方案实现
微服务模式下,那只能再新建一个微服务,把这些转态聚合起来,并且提供查询服务给前台。
于是就有了下面这个简单的架构:
相应地数据库就会像下面这样设计:
然后很自然地,提供给前端的查询服务就这么实现就可以了
SELECT
product_id
FROM
theme_product
WHERE
theme_id=1
AND inventory_state=1 --过滤库存
AND online_state=1 --过滤上下架状态
AND sale_state=1 --过滤是否可售卖
OFFSET 0 LIMIT 20
1.3 方案存在的问题
我们发现这个方案扩展性相对比较弱,且性能不高,如果后面再加入更多的状态,消耗的存储相对也比较高。
那有没有一种比较完美的方案呢?
1.4 比特登场
我们可以将所有商品的状态合并为一个字段:product_state,用每一个bit位来表示商品的每一种状态。如下所示
我们将所有的状态合并为一个tinyint类型的字段product_state,然后使用product_state的低3位分别来表示库存,上下架,是否可售卖的状态。这样商品的各种状态就可以直接转化为下面一个字段了。
对应的SQL查询语句也就变成 了下面这样
SELECT
product_id
FROM
theme_product
WHERE
theme_id=1
AND product_state=7 --商品状态
OFFSET 0 LIMIT 20
当我们这样去实现的时候,我们发现前面提到的3个问题都已经得到解决了。
- 1.之前需要三个字段(且区分度太低)建联合索引,现在一个字段建索引,区分度高。
- 2.只需要一个tinyint字段,就可以存储8种商品状态,扩展性高。
- 3.基本上不需要修改schema。这里我们使用了tinyint类型可以存储8个字段,如果不是那么在意存储的话,可以直接修改为int类型,可以存储32个状态。当然这里使用tinyint字段是在业务迭代和存储之前做的一个权衡点。
2.回归Bit
通过上面的业务场景,我们发现其实我们CRUD boy竟然也能让Bit赋能我们的业务。
但是要想让Bit更好地为我们所用,我们就得看透它才行。所以下面我们就要回归Bit,看Bit本身到底有哪些骚操作。
2.1.程序世界中的数字【补码】
先记住结论:程序世界中的数字都是以补码的形式存在的
我们都知道,数字肯定是二进制表示的,那补码是个什么鬼?
在计算机中,为了区分正数和负数,人为规定最高位为符号位,其他位为真值位:
- 符号位为0:正数
- 符号位为1:负数
比如5(10) = 101(2),-5(10) = 100...00101(2)
那你是不是有一个疑问:那0呢?
记住结论:0在计算机中是正数。也就是说0的符号位为0。
那是不是说程序世界中的数字就是这样简单表示的呢?我们用Java来实验一下
System.out.println(Integer.toBinaryString(5)); // 101
System.out.println(Integer.toBinaryString(-5)); // 11111111111111111111111111111011
你会发现很神奇-5(10)=11111111111111111111111111111011(2)
这貌似跟我们的设想不太一样,按照之前的逻辑-5(2)应该是1000...0101。但是这两者之间貌似差得有点多。
2.2.原码,反码,补码
看到上面这个标题,是不是有点头疼。不慌,问题不大,请调整好坐姿,我们要发车了。
回顾一下之前的问题:为什么我们设想的-5的二进制表示和实际的二进制表示差别这么大
-5(10)= 11111111111111111111111111111011(2)
-5(10) =10000000000000000000000000000101(2)
结论:因为我们设想的-5的二进制表示是原码,而实际上计算机中的二进制表示用的是补码
那为啥需要补码呢?
在解释为什么之前,我们简单介绍一下这3中码之间的运算规则:
如下是-5的原码,反码,补码的3种表示:
那你肯定要问:只要原码不就好了吗,为什么需要反码,补码呢?
结论:
反码:使用加法来代替减法
补码:解决了反码的问题后,还解决了+0和-0的二义性
所以,因为补码只需要加法器,而且还能区分+0和-0,所以计算机中的数字都是用补码表示的。
至于,为什么反码可以代替加法,补码可以解决+0和-0的二义性。请移步:
原码,补码和反码
原码、反码、补码的产生、应用以及优缺点有哪些
如果不想看这么复杂或者严谨的推到的话,我下面给一个例子来直接感受一下就好。
我们来计算十进制的表达式: 1-1=0
先看反码使用加法来代替减法
1.使用原码计算:1 - 1 = 1 + (-1) = [00000001]原 + [10000001]原 = [10000010]原 = -2【结果不正确】
2.使用反码计算:1 - 1 = 1 + (-1) = [0000 0001]原 + [1000 0001]原= [0000 0001]反 + [1111 1110]反 = [1111 1111]反 = [1000 0000]原 = -0 【计算结果的真值部分正确】
再看不满如何解决+0和-0的二义性
1-1 = 1 + (-1) = [0000 0001]原 + [1000 0001]原 = [0000 0001]补 + [1111 1111]补 = [0000 0000]补=[0000 0000]原
这样0用[0000 0000]表示, 而以前出现问题的-0则不存在了.而且可以用[1000 0000]表示-128:
-128 = (-1) + (-127) = [1000 0001]原 + [1111 1111]原 = [1111 1111]补 + [1000 0001]补 = [1000 0000]补
2.3.奇怪的Math.abs方法
下面这段奇怪的代码来自于RocketMQ
有没有感觉很神奇,为什么取了绝对值之后还需要判断是不是负数呢?
没事,我们再看一个神奇的结果:
我们发现:Math.abs(Integer.MIN_VALUE)=Integer.MIN_VALUE
好了,请坐稳,我们又要开车了。
大多数人的解释是:因为int的取值范围为[-231,231-1],-a=231 ,向上溢出了,所以Math.abs(Integer.MIN_VALUE)=Integer.MIN_VALUE
这其实就是解释了个寂寞,向上溢出了为什么就这样呢?还是没有解释清楚其本质。
-a = ~a + 1,一个数的负数等于该数取反 ,然后+1
所以,我们来看一下运算过程:
运算后发现两者的二进制表示是一样的,所以Math.abs(Integer.MIN_VALUE)=Integer.MIN_VALUE
2.3.位运算
2.3.1 逻辑运算
注意:符号位也会参与逻辑运算
2.3.2 位移运算
直接看运算规则,不用记,只需要理解。
下面我们来一个个理解运算的规则
-
左移
左移后,低位就空出来了,因为是低位所以用0来填充
-
右移
右移之后,高位空出,为了保证符号不变,高位空出来的部分与符号位一致
-
无符号右移
无符号右移是相对于右移而言的,差别主要高位填充的策略上。无符号右移统一高位填充0
至于为什么,请大家自己思考,并留言哦!!!
2.4.位移运算的数学意义
在数字没有溢出的前提下,位移操作是有数学含义的
简单看一下ArrayList的扩容,新容量 = 旧容量 * 1.5。这里就用了右移1位表示0.5倍
好了,这次就聊到这里。这次主要聊一下基础性的东西。所谓基础不牢,地动山摇。下面一篇我们继续聊Bit的实际应用场景,相信会为你打开一扇Bit之窗,敬请期待!!!