分类: 未分类

  • MySQL 索引失效跑不出这 8 个场景

    快乐分享,Java干货及时送达👇

    SQL 写不好 加班少不了  日常工作中SQL 是必不可少的一项技术 但是很多人不会过多的去关注SQL问题。

    一是数据量小,二是没有意识到索引的重要性。本文主要是整理 SQL失效场景,如果里面的细节你都知道,那你一定是学习能力比较好的人,膜拜~

    写完这篇文章 我感觉自己之前知道的真的是 “目录” 没有明白其中的内容,如果你能跟着节奏看完文章,一定会有收获,至少我写完感觉思维通透很多,以后百分之九十的 SQl索引问题 和 面试这方面问题都能拿捏两。


    基础数据准备





    准备一个数据表作为 数据演示  这里面一共 创建了三个索引

    • 联合索引  snames_codeaddress

    • 主键索引  id

    • 普通索引  height


    SET NAMES utf8mb4;
    SET FOREIGN_KEY_CHECKS = 0;

    -- ----------------------------
    -- Table structure for student
    -- ----------------------------
    DROP TABLE IF EXISTS `student`;
    CREATE TABLE `student`  (
      `id` int(11NOT NULL AUTO_INCREMENT,
      `sname` varchar(20CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
      `s_code` int(100NULL DEFAULT NULL,
      `address` varchar(100CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
      `height` double NULL DEFAULT NULL,
      `classid` int(11NULL DEFAULT NULL,
      `create_time` datetime(0NOT NULL ON UPDATE CURRENT_TIMESTAMP(0),
      PRIMARY KEY (`id`USING BTREE,
      INDEX `普通索引`(`height`USING BTREE,
      INDEX `联合索引`(`sname``s_code``address`USING BTREE
    ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

    -- ----------------------------
    -- Records of student
    -- ----------------------------
    INSERT INTO `student` VALUES (1'学生1'1'上海'1701'2022-11-02 20:44:14');
    INSERT INTO `student` VALUES (2'学生2'2'北京'1802'2022-11-02 20:44:16');
    INSERT INTO `student` VALUES (3'变成派大星'3'京东'1853'2022-11-02 20:44:19');
    INSERT INTO `student` VALUES (4'学生4'4'联通'1904'2022-11-02 20:44:25');


    正文





    上面的SQL 我们已经创建好基本的数据  在验证之前 先带着几个问题

    我们先从上往下进行验证


    最左匹配原则





    写在前面:我很早之前就听说过数据库的最左匹配原则,当时是通过各大博客论坛了解的,但是这些博客的局限性在于它们对最左匹配原则的描述就像一些数学定义一样,往往都是列出123点,满足这123点就能匹配上索引,否则就不能。

    最左匹配原则就是指在联合索引中,如果你的 SQL 语句中用到了联合索引中的最左边的索引,那么这条 SQL 语句就可以利用这个联合索引去进行匹配,我们上面建立了联合索引 可以用来测试最左匹配原则 snames_codeaddress

    请看下面SQL语句 进行思考 是否会走索引

    -- 联合索引 sname,s_code,address

    1、select create_time from student where sname = "变成派大星"  -- 会走索引吗?

    2select create_time from student where s_code = 1   -- 会走索引吗?

    3select create_time from student where address = "上海"  -- 会走索引吗?

    4select create_time from student where address = "上海" and s_code = 1 -- 会走索引吗?

    5select create_time from student where address = "上海" and sname = "变成派大星"  -- 会走索引吗?

    6select create_time from student where sname = "变成派大星" and address = "上海"  -- 会走索引吗?

    7select create_time from student where sname = "变成派大星" and s_code = 1 and address = "上海"  -- 会走索引吗?

    凭你的经验 哪些会使用到索引呢 ?可以先思考一下 在心中记下数字

    走索引例子

    EXPLAIN  select create_time from student where sname = "变成派大星"  -- 会走索引吗?

    未走索引例子

    EXPLAIN select create_time from student where address = "上海" and s_code = 1 -- 会走索引吗?

    走的全表扫描 rows = 4

    如果你内心的答案没有全部说对就接着往下看

    最左匹配原则顾名思义:最左优先,以最左边的为起点任何连续的索引都能匹配上。同时遇到范围查询(>、。

    例如:s_code = 2 如果建立(snames_code)顺序的索引,是匹配不到(snames_code)索引的;

    但是如果查询条件是sname = “变成派大星” and s_code = 2或者a=1(又或者是s_code = 2 and sname = “变成派大星” )就可以,因为优化器会自动调整snames_code的顺序

    再比如sname = “变成派大星” and s_code > 1 and address = “上海”  address是用不到索引的,因为s_code字段是一个范围查询,它之后的字段会停止匹配。

    不带范围查询 索引使用类型

    带范围使用类型

    根据上一篇文章的讲解 可以明白 ref 和range的含义  级别还是相差很多的

    思考

    为什么左链接一定要遵循最左缀原则呢?

    验证

    看过一个比较好玩的回答:

    你可以认为联合索引是闯关游戏的设计
    例如你这个联合索引是state/city/zipCode
    那么state就是第一关 city是第二关, zipCode就是第三关
    你必须匹配了第一关,才能匹配第二关,匹配了第一关和第二关,才能匹配第三关

    这样描述不算完全准确 但是确实是这种思想

    要想理解联合索引的最左匹配原则,先来理解下索引的底层原理。索引的底层是一颗B+树,那么联合索引的底层也就是一颗B+树,只不过联合索引的B+树节点中存储的是键值。由于构建一棵B+树只能根据一个值来确定索引关系,所以数据库依赖联合索引最左的字段来构建 文字比较抽象 我们看一下

    加入我们建立 A,B 联合索引 他们在底层储存是什么样子呢?

    • 橙色代表字段 A

    • 浅绿色 代表字段B

    图解:

    我们可以看出几个特点

    • A 是有顺序的  1,1,2,2,3,4

    • B 是没有顺序的 1,2,1,4,1,2 这个是散列的

    • 如果A是等值的时候 B是有序的  例如 (1,1),(1,2) 这里的B有序的 (2,1),(2,4) B 也是有序的

    这里应该就能看出 如果没有A的支持 B的索引是散列的 不是连续的

    再细致一点 我们重新创建一个表

    DROP TABLE IF EXISTS `leftaffix`;

    CREATE TABLE `leftaffix`  (

      `a` int(11NOT NULL AUTO_INCREMENT,

      `b` int(11NULL DEFAULT NULL,

      `c` int(11NULL DEFAULT NULL,

      `d` int(11NULL DEFAULT NULL,

      `e` varchar(11CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,

      PRIMARY KEY (`a`USING BTREE,

      INDEX `联合索引`(`b``c``d`USING BTREE

    ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
     
    -- ----------------------------
    -- Records of leftaffix
    -- ----------------------------
    INSERT INTO `leftaffix` VALUES (1111'1');

    INSERT INTO `leftaffix` VALUES (2222'2');

    INSERT INTO `leftaffix` VALUES (3322'3');

    INSERT INTO `leftaffix` VALUES (4311'4');

    INSERT INTO `leftaffix` VALUES (5235'5');

    INSERT INTO `leftaffix` VALUES (6644'6');

    INSERT INTO `leftaffix` VALUES (7888'7');
    SET FOREIGN_KEY_CHECKS = 1;

    在创建索引树的时候会对数据进行排序 根据最左缀原则  会先通过 B 进行排序 也就是 如果出现值相同就 根据 C 排序 如果 C相同就根据D 排序 排好顺序之后就是如下图:

    索引的生成就会根据图二的顺序进行生成  我们看一下 生成后的树状数据是什么样子

    解释一些这个树状图  首先根据图二的排序 我们知道顺序 是 1111a  2222b 所以 在第三层 我们可以看到 1111a 在第一层 2222b在第二层  因为 111

    简化一下就是这个样子

    但是这种顺序是相对的。这是因为MySQL创建联合索引的规则是首先会对联合索引的最左边第一个字段排序,在第一个字段的排序基础上,然后在对第二个字段进行排序。所以B=2这种查询条件没有办法利用索引。

    看到这里还可以明白一个道理 为什么我们建立索引的时候不推荐建立在经常改变的字段 因为这样的话我们的索引结构就要跟着你的改变而改动 所以很消耗性能


    补充

    评论区老哥的提示 最左缀原则可以通过跳跃扫描的方式打破 简单整理一下这方面的知识

    这个是在 8.0 进行的优化

    MySQL8.0版本开始增加了索引跳跃扫描的功能,当第一列索引的唯一值较少时,即使where条件没有第一列索引,查询的时候也可以用到联合索引。

    比如我们使用的联合索引是 bcd  但是b中字段比较少 我们在使用联合索引的时候没有 使用 b 但是依然可以使用联合索引MySQL联合索引有时候遵循最左前缀匹配原则,有时候不遵循。


    小总结

    前提 如果创建 b,c,d 联合索引面

    • 如果 我where 后面的条件是c = 1 and d = 1为什么不能走索引呢 如果没有b的话 你查询的值相当于 *11 我们都知道*是所有的意思也就是我能匹配到所有的数据

    • 如果 我 where 后面是 b = 1 and d =1 为什么会走索引呢?你等于查询的数据是 1*1 我可以通过前面 1 进行索引匹配 所以就可以走索引

    • 最左缀匹配原则的最重要的就是 第一个字段

    我们接着看下一个失效场景


    select *





    思考

    这里是我之前的一个思维误区 select * 不会导致索引失效 之前测试发现失效是因为where 后面的查询范围过大 导致索引失效 并不是Select * 引起的  但是为什么不推荐使用select *

    解释

    • 增加查询分析器解析成本。

    • 增减字段容易与 resultMap 配置不一致。

    • 无用字段增加网络 消耗,尤其是 text 类型的字段。


    在阿里的开发手册中,大面的概括了上面几点。

    在使用Select * 索引使用正常

    虽然走了索引但是 也不推荐这种写法 为什么呢?

    首先我们在上一个验证中创建了联合索引 我们使用B=1 会走索引  但是 与直接查询索引字段不同  使用SELECT*,获取了不需要的数据,则首先通过辅助索引过滤数据,然后再通过聚集索引获取所有的列,这就多了一次b+树查询,速度必然会慢很多,减少使用select * 就是降低回表带来的损耗。

    也就是 Select * 在一些情况下是会走索引的 如果不走索引就是 where 查询范围过大 导致MySQL 最优选择全表扫描了 并不是Select * 的问题

    上图就是索引失效的情况

    范围查找也不是一定会索引失效 下面情况就会索引生效就是 级别低 生效的原因是因为缩小了范围


    小总结

    • select * 会走索引

    • 范围查找有概率索引失效但是在特定的情况下会生效 范围小就会使用 也可以理解为 返回结果集小就会使用索引

    • mysql中连接查询的原理是先对驱动表进行查询操作,然后再用从驱动表得到的数据作为条件,逐条的到被驱动表进行查询。

    • 每次驱动表加载一条数据到内存中,然后被驱动表所有的数据都需要往内存中加载一遍进行比较。效率很低,所以mysql中可以指定一个缓冲池的大小,缓冲池大的话可以同时加载多条驱动表的数据进行比较,放的数据条数越多性能io操作就越少,性能也就越好。所以,如果此时使用select * 放一些无用的列,只会白白的占用缓冲空间。浪费本可以提高性能的机会。

    • 按照评论区老哥的说法 select * 不是造成索引失效的直接原因 大部分原因是 where 后边条件的问题 但是还是尽量少去使用select * 多少还是会有影响的



    使用函数





    使用在Select 后面使用函数可以使用索引 但是下面这种做法就不能

    因为索引保存的是索引字段的原始值,而不是经过函数计算后的值,自然就没办法走索引了。

    不过,从 MySQL 8.0 开始,索引特性增加了函数索引,即可以针对函数计算后的值建立一个索引,也就是说该索引的值是函数计算后的值,所以就可以通过扫描索引来查询数据。

    这种写法我没使用过 感觉情况比较少 也比较容易注意到这种写法


    计算操作





    这个情况和上面一样 之所以会导致索引失效是因为改变了索引原来的值 在树中找不到对应的数据只能全表扫描

    因为索引保存的是索引字段的原始值,而不是 b – 1 表达式计算后的值,所以无法走索引,只能通过把索引字段的取值都取出来,然后依次进行表达式的计算来进行条件判断,因此采用的就是全表扫描的方式。

    下面这种计算方式就会使用索引

    Java比较熟悉的可能会有点疑问,这种对索引进行简单的表达式计算,在代码特殊处理下,应该是可以做到索引扫描的,比方将 b – 1 = 6 变成 b = 6 – 1。是的,是能够实现,但是 MySQL 还是偷了这个懒,没有实现。


    小总结

    总而言之 言而总之 只要是影响到索引列的值 索引就是失效


    Like %





    1.这个真的是难受哦  因为经常使用这个 所以还是要小心点 在看为什么失效之前 我们先看一下 Like % 的解释

    • %百分号通配符: 表示任何字符出现任意次数(可以是0次).

    • _下划线通配符: 表示只能匹配单个字符,不能多也不能少,就是一个字符.

    • like操作符: LIKE作用是指示mysql后面的搜索模式是利用通配符而不是直接相等匹配进行比较.


    注意: 如果在使用like操作符时,后面的没有使用通用匹配符效果是和=一致的,

    SELECT * FROM products WHERE products.prod_name like '1000';

    2.匹配包含”Li”的记录(包括记录”Li”) :

    SELECTFROM products WHERE products.prod_name like '%Li%';

    3.匹配以”Li”结尾的记录(包括记录”Li”,不包括记录”Li “,也就是Li后面有空格的记录,这里需要注意)

    SELECT * FROM products WHERE products.prod_name like '%Li';

    在左不走 在右走

    右:虽然走 但是索引级别比较低主要是模糊查询 范围比较大 所以索引级别就比较低

    左:这个范围非常大 所以没有使用索引的必要了 这个可能不是很好优化 还好不是一直拼接上面的

    小总结

    索引的时候和查询范围关系也很大 范围过大造成索引没有意义从而失效的情况也不少


    使用Or导致索引失效





    这个原因就更简单了

    在 WHERE 子句中,如果在 OR 前的条件列是索引列,而在 OR 后的条件列不是索引列,那么索引会失效 举个例子,比如下面的查询语句,b 是主键,e 是普通列,从执行计划的结果看,是走了全表扫描。

    优化

    这个的优化方式就是 在Or的时候两边都加上索引

    就会使用索引 避免全表扫描


    in使用不当





    首先使用In 不是一定会造成全表扫描的 IN肯定会走索引,但是当IN的取值范围较大时会导致索引失效,走全表扫描

    in 在结果集 大于30%的时候索引失效

    not in 和 In的失效场景相同


    order By





    这一个主要是Mysql 自身优化的问题 我们都知道OrderBy 是排序 那就代表我需要对数据进行排序 如果我走索引 索引是排好序的 但是我需要回表 消耗时间 另一种 我直接全表扫描排序 不用回表 也就是

    • 走索引 + 回表

    • 不走索引 直接全表扫描

    Mysql 认为直接全表扫面的速度比 回表的速度快所以就直接走索引了  在Order By 的情况下 走全表扫描反而是更好的选择

    子查询会走索引吗

    答案是会 但是使用不好就不会


    大总结





    转自:进阶的派大星

    链接:https://juejin.cn/post/7161964571853815822

    
    

  • 发誓将Notepad++拉下马,大佬推出了一款国产开源编辑器…..

    快乐分享,Java干货及时送达👇

    大家知道,Notepad++作者经常大量发表错误言论,且拒不认错,引发了广大程序员的强烈反对,很多人因此而弃用这款软件,纷纷改用其它文本编辑器。而今天推荐的这款开源文本编辑器呢,就是国内大佬写的类似Notepad++的文本编辑器—Notepad–!

    这位大佬也是性情中人,看不惯那小人嘴脸,发誓要自己开发一款超越Notepad++的国内开源文本编辑器,埋头苦干一段时间,终于在Gitee上推出了他的作品。至于名称取为Notepad–,大佬说:

    鉴于Notepad++作者的错误言论,Notepad–的意义在于:减少一点错误言论,减少一点自以为是。严正声明,台湾是中国的一部分!

    下面向各位小伙伴简单介绍一下:

    项目特点

    作为一个来自中国的、支持windows/linux/mac、绿色免费的、开源的文本编辑器,目标是要替换notepad++。该项目是采用了C++进行开发,针对NotePad++中比较受欢迎的诸多功能与特点进行了分析和吸收,逐渐在项目中加以实现。

    功能特点

    • 支持国产uos和苹果os系统

    • 文件夹对比同步

    • 文件对比同步

    • 二进制文件对比

    • 文件编码批量转换

    功能界面

    支持国产uos和苹果os系统
    文件夹对比同步
    支持文件对比及同步,编辑修改,撤销修改等
    支持文本单词高亮
    支持文件夹中批量查找
    支持文件编码批量修改
    支持皮肤切换
    支持二进制文件显示和跳转查看

    以上截图都是截取了一部分内容,如果大家有兴趣了解或者试用更详细的内容,可以去项目地址:

    https://gitee.com/cxasm/notepad–/tree/master

    最后

    Notepad–开源项目也提供全部源代码,方便大家下载研习。大家不仅可以通过使用Notepad–来体验和提升工作效率,尝试替换掉Notepad++,还可以通过对源代码学习和了解如何编写一个主流MDI文本编辑器,也许将来的最好最实用的文本编辑器就是出自你们手中哦!

    作者:cxasm

    来源:zhuanlan.zhihu.com/p/590551947

    
    

  • 为什么 Nginx 比 Apache 更牛叉?

    快乐分享,Java干货及时送达👇

    来源:codebay.cn/post/8557.html

    • 一、Apache三种工作模式
      • 1、prefork的工作原理
      • 2、worker的工作原理
      • 3、event 基于事件机制的特性
    • 二、如何提高Web服务器的并发连接处理能力
    • 三、Nginx优异之处
    • 四、Nginx 工作原理
    • 五、Nginx 的诞生主要解决C10K问题
      • 1、select模型:(apache使用,由于受模块等限制,用的不多);
      • 2、poll:poll是unix沿用select自己重新实现了一遍,唯一解决的问题是poll 没有最大文件描述符数量的限制;
      • 3、epoll模型:(Nginx使用)

    Nginx才短短几年,就拿下了Web服务器大壁江山,众所周知,Nginx在处理大并发静态请求方面,效率明显高于Httpd,甚至能轻松解决C10K问题。

    在高并发连接的情况下,Nginx是Apache服务器不错的替代品。Nginx同时也可以作为7层负载均衡服务器来使用。根据我的测试结果,Nginx + PHP(FastCGI) 可以承受3万以上的并发连接数,相当于同等环境下Apache的10倍。

    一般来说,4GB内存的服务器+Apache(prefork模式)一般只能处理3000个并发连接,因为它们将占用3GB以上的内存,还得为系统预留1GB的内存。我曾经就有两台Apache服务器,因为在配置文件中设置的MaxClients为4000,当Apache并发连接数达到3800时,导致服务器内存和Swap空间用满而崩溃。

    而这台 Nginx + PHP(FastCGI) 服务器在3万并发连接下,开启的10个Nginx进程消耗150M内存(15M*10=150M),开启的64个php-cgi进程消耗1280M内存(20M*64=1280M),加上系统自身消耗的内存,总共消耗不到2GB内存。如果服务器内存较小,完全可以只开启25个php-cgi进程,这样php-cgi消耗的总内存数才500M。

    在3万并发连接下,访问Nginx+ PHP(FastCGI) 服务器的PHP程序,仍然速度飞快。

    为什么Nginx在处理高并发方面要优于httpd,我们先从两种web服务器的工作原理以及工作模式说起。

    一、Apache三种工作模式

    我们都知道Apache有三种工作模块,分别为:prefork、worker、event。

    • prefork: 多进程,每个请求用一个进程响应,这个过程会用到select机制来通知。
    • worker: 多线程,一个进程可以生成多个线程,每个线程响应一个请求,但通知机制还是select不过可以接受更多的请求。
    • event: 基于异步I/O模型,一个进程或线程,每个进程或线程响应多个用户请求,它是基于事件驱动(也就是epoll机制)实现的。

    1、prefork的工作原理

    如果不用“–with-mpm”显式指定某种MPM,prefork就是Unix平台上缺省的MPM。它所采用的预派生子进程方式也是 Apache1.3中采用的模式。prefork本身并没有使用到线程,2.0版使用它是为了与1.3版保持兼容性;另一方面,prefork用单独的子进程来处理不同的请求,进程之间是彼此独立的,这也使其成为最稳定的MPM之一。

    2、worker的工作原理

    相对于prefork,worker是2.0版中全新的支持多线程和多进程混合模型的MPM。由于使用线程来处理,所以可以处理相对海量的请求,而系统资源的开销要小于基于进程的服务器。但是,worker也使用了多进程,每个进程又生成多个线程,以获得基于进程服务器的稳定性,这种MPM的工作方 式将是Apache2.0的发展趋势。

    3、event 基于事件机制的特性

    一个进程响应多个用户请求,利用callback机制,让套接字复用,请求过来后进程并不处理请求,而是直接交由其他机制来处理,通过epoll机制来通知请求是否完成;在这个过程中,进程本身一直处于空闲状态,可以一直接收用户请求。可以实现一个进程程响应多个用户请求。支持持海量并发连接数,消耗更少的资源。

    二、如何提高Web服务器的并发连接处理能力

    有几个基本条件:

    1、基于线程,即一个进程生成多个线程,每个线程响应用户的每个请求。

    2、基于事件的模型,一个进程处理多个请求,并且通过epoll机制来通知用户请求完成。

    3、基于磁盘的AIO(异步I/O)

    4、支持mmap内存映射,mmap传统的web服务器,进行页面输入时,都是将磁盘的页面先输入到内核缓存中,再由内核缓存中复制一份到web服务器上,mmap机制就是让内核缓存与磁盘进行映射,web服务器,直接复制页面内容即可。不需要先把磁盘的上的页面先输入到内核缓存去。

    刚好,Nginx 支持以上所有特性。所以Nginx官网上说,Nginx支持50000并发,是有依据的。

    三、Nginx优异之处

    传统上基于进程或线程模型架构的Web服务通过每进程或每线程处理并发连接请求,这势必会在网络和I/O操作时产生阻塞,其另一个必然结果则是对内存或CPU的利用率低下。

    生成一个新的进程/线程需要事先备好其运行时环境,这包括为其分配堆内存和栈内存,以及为其创建新的执行上下文等。这些操作都需要占用CPU,而且过多的进程/线程还会带来线程抖动或频繁的上下文切换,系统性能也会由此进一步下降。

    另一种高性能web服务器/Web服务器反向代理:Nginx,Nginx的主要着眼点就是其高性能以及对物理计算资源的高密度利用,因此其采用了不同的架构模型。受启发于多种操作系统设计中基于“事件”的高级处理机制,Nginx采用了模块化、事件驱动、异步、单线程及非阻塞的架构,并大量采用了多路复用及事件通知机制。

    在Nginx中,连接请求由为数不多的几个仅包含一个线程的进程Worker以高效的回环(run-loop)机制进行处理,而每个Worker可以并行处理数千个的并发连接及请求。

    四、Nginx 工作原理

    Nginx会按需同时运行多个进程:一个主进程(master)和几个工作进程(worker),配置了缓存时还会有缓存加载器进程(cache loader)和缓存管理器进程(cache manager)等。所有进程均是仅含有一个线程,并主要通过“共享内存”的机制实现进程间通信。主进程以root用户身份运行,而worker、cache loader和cache manager均应以非特权用户身份运行。

    在高连接并发的情况下,Nginx是Apache服务器不错的替代品。

    Nginx 安装非常的简单 , 配置文件非常简洁(还能够支持perl语法),Bugs 非常少的服务器: Nginx 启动特别容易, 并且几乎可以做到7*24不间断运行,即使运行数个月也不需要重新启动. 你还能够 不间断服务的情况下进行软件版本的升级 。

    五、Nginx 的诞生主要解决C10K问题

    最后我们从各自使用的多路复用IO模型来分析:

    1、select模型:(apache使用,由于受模块等限制,用的不多);

    单个进程能够 监视的文件描述符的数量存在最大限制;

    select()所维护的 存储大量文件描述符的数据结构 ,随着文件描述符数量的增长,其在用户态和内核的地址空间的复制所引发的开销也会线性增长;

    由于网络响应时间的延迟使得大量TCP连接处于非活跃状态,但调用select()还是会对 所有的socket进行一次线性扫描 ,会造成一定的开销;

    2、poll:poll是unix沿用select自己重新实现了一遍,唯一解决的问题是poll 没有最大文件描述符数量的限制;

    3、epoll模型:(Nginx使用)

    epoll带来了两个优势,大幅度提升了性能:

    (1)基于事件的就绪通知方式 ,select/poll方式,进程只有在调用一定的方法后,内核才会对所有监视的文件描述符进行扫描,而epoll事件通过epoll_ctl()注册一个文件描述符,一旦某个文件描述符就绪时,内核会采用类似call back的回调机制,迅速激活这个文件描述符,epoll_wait()便会得到通知

    (2)调用一次epoll_wait()获得就绪文件描述符时,返回的并不是实际的描述符,而是一个代表就绪描述符数量的值,拿到这些值去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里使用内存映射(mmap)技术, 避免了复制大量文件描述符带来的开销

    (3)当然epoll也有一定的局限性, epoll只有Linux2.6才有实现 ,而其他平台都没有,这和apache这种优秀的跨平台服务器,显然是有些背道而驰了。

    (4)简单来说epoll是select的升级版,单进程管理的文件描述符没有最大限制。但epoll只有linux平台可使用。作为跨平台的Apache没有使用

    
    

  • “12306” 是如何支撑百万 QPS 的?

    快乐分享,Java干货及时送达👇

    文章来源:https://juejin.cn/post/6844903949632274445


    目录

    • 12306抢票,极限并发带来的思考?
    • 1. 大型高并发系统架构
    • 2.秒杀抢购系统选型
    • 3. 扣库存的艺术
    • 4. 代码演示
    • 5.总结回顾



    12306抢票,极限并发带来的思考?





    每到节假日期间,一二线城市返乡、外出游玩的人们几乎都面临着一个问题:抢火车票!虽然现在大多数情况下都能订到票,但是放票瞬间即无票的场景,相信大家都深有体会。尤其是春节期间,大家不仅使用12306,还会考虑“智行”和其他的抢票软件,全国上下几亿人在这段时间都在抢票。

    “12306服务”承受着这个世界上任何秒杀系统都无法超越的QPS,上百万的并发再正常不过了!笔者专门研究了一下“12306”的服务端架构,学习到了其系统设计上很多亮点,在这里和大家分享一下并模拟一个例子:如何在100万人同时抢1万张火车票时,系统提供正常、稳定的服务。


    github代码地址:https://github.com/GuoZhaoran/spikeSystem




    1. 大型高并发系统架构





    高并发的系统架构都会采用分布式集群部署,服务上层有着层层负载均衡,并提供各种容灾手段(双火机房、节点容错、服务器灾备等)保证系统的高可用,流量也会根据不同的负载能力和配置策略均衡到不同的服务器上。下边是一个简单的示意图:


    1.1 负载均衡简介

    上图中描述了用户请求到服务器经历了三层的负载均衡,下边分别简单介绍一下这三种负载均衡:

    (1)OSPF(开放式最短链路优先)是一个内部网关协议(Interior Gateway Protocol,简称IGP)。OSPF通过路由器之间通告网络接口的状态来建立链路状态数据库,生成最短路径树,OSPF会自动计算路由接口上的Cost值,但也可以通过手工指定该接口的Cost值,手工指定的优先于自动计算的值。OSPF计算的Cost,同样是和接口带宽成反比,带宽越高,Cost值越小。到达目标相同Cost值的路径,可以执行负载均衡,最多6条链路同时执行负载均衡。

    (2)LVS (Linux VirtualServer),它是一种集群(Cluster)技术,采用IP负载均衡技术和基于内容请求分发技术。调度器具有很好的吞吐率,将请求均衡地转移到不同的服务器上执行,且调度器自动屏蔽掉服务器的故障,从而将一组服务器构成一个高性能的、高可用的虚拟服务器。

    (3)Nginx想必大家都很熟悉了,是一款非常高性能的http代理/反向代理服务器,服务开发中也经常使用它来做负载均衡。Nginx实现负载均衡的方式主要有三种:轮询、加权轮询、ip hash轮询,下面我们就针对Nginx的加权轮询做专门的配置和测试

    1.2 Nginx加权轮询的演示

    Nginx实现负载均衡通过upstream模块实现,其中加权轮询的配置是可以给相关的服务加上一个权重值,配置的时候可能根据服务器的性能、负载能力设置相应的负载。下面是一个加权轮询负载的配置,我将在本地的监听3001-3004端口,分别配置1,2,3,4的权重:

    #配置负载均衡
        upstream load_rule {
           server 127.0.0.1:3001 weight=1;
           server 127.0.0.1:3002 weight=2;
           server 127.0.0.1:3003 weight=3;
           server 127.0.0.1:3004 weight=4;
        }
        ...
        server {
        listen       80;
        server_name  load_balance.com www.load_balance.com;
        location / {
           proxy_pass http://load_rule;
        }
    }

    我在本地/etc/hosts目录下配置了 www.load_balance.com的虚拟域名地址,接下来使用Go语言开启四个http端口监听服务,下面是监听在3001端口的Go程序,其他几个只需要修改端口即可:

    package main

    import (
     "net/http"
     "os"
     "strings"
    )


    func main() 
    {
     http.HandleFunc("/buy/ticket", handleReq)
     http.ListenAndServe(":3001", nil)
    }

    //处理请求函数,根据请求将响应结果信息写入日志
    func handleReq(w http.ResponseWriter, r *http.Request) {
     failedMsg :=  "handle in port:"
     writeLog(failedMsg, "./stat.log")
    }

    //写入日志
    func writeLog(msg string, logPath string) {
     fd, _ := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
     defer fd.Close()
     content := strings.Join([]string{msg, "rn"}, "3001")
     buf := []byte(content)
     fd.Write(buf)
    }

    我将请求的端口日志信息写到了./stat.log文件当中,然后使用ab压测工具做压测:

    ab -n 1000 -c 100 http://www.load_balance.com/buy/ticket

    统计日志中的结果,3001-3004端口分别得到了100、200、300、400的请求量,这和我在nginx中配置的权重占比很好的吻合在了一起,并且负载后的流量非常的均匀、随机。



    2.秒杀抢购系统选型





    回到我们最初提到的问题中来:火车票秒杀系统如何在高并发情况下提供正常、稳定的服务呢?


    从上面的介绍我们知道用户秒杀流量通过层层的负载均衡,均匀到了不同的服务器上,即使如此,集群中的单机所承受的QPS也是非常高的。如何将单机性能优化到极致呢?要解决这个问题,我们就要想明白一件事:通常订票系统要处理生成订单、减扣库存、用户支付这三个基本的阶段,我们系统要做的事情是要保证火车票订单不超卖、不少卖 ,每张售卖的车票都必须支付才有效,还要保证系统承受极高的并发。这三个阶段的先后顺序改怎么分配才更加合理呢?我们来分析一下:

    2.1 下单减库存

    当用户并发请求到达服务端时,首先创建订单,然后扣除库存,等待用户支付。这种顺序是我们一般人首先会想到的解决方案,这种情况下也能保证订单不会超卖,因为创建订单之后就会减库存,这是一个原子操作。但是这样也会产生一些问题,第一就是在极限并发情况下,任何一个内存操作的细节都至关影响性能,尤其像创建订单这种逻辑,一般都需要存储到磁盘数据库的,对数据库的压力是可想而知的;第二是如果用户存在恶意下单的情况,只下单不支付这样库存就会变少,会少卖很多订单,虽然服务端可以限制IP和用户的购买订单数量,这也不算是一个好方法。

    2.2 支付减库存

    如果等待用户支付了订单在减库存,第一感觉就是不会少卖。但是这是并发架构的大忌,因为在极限并发情况下,用户可能会创建很多订单,当库存减为零的时候很多用户发现抢到的订单支付不了了,这也就是所谓的“超卖”。也不能避免并发操作数据库磁盘IO

    2.3 预扣库存

    从上边两种方案的考虑,我们可以得出结论:只要创建订单,就要频繁操作数据库IO。那么有没有一种不需要直接操作数据库IO的方案呢,这就是预扣库存。先扣除了库存,保证不超卖,然后异步生成用户订单,这样响应给用户的速度就会快很多;那么怎么保证不少卖呢?用户拿到了订单,不支付怎么办?

    我们都知道现在订单都有有效期,比如说用户五分钟内不支付,订单就失效了,订单一旦失效,就会加入新的库存,这也是现在很多网上零售企业保证商品不少卖采用的方案。订单的生成是异步的,一般都会放到MQ、kafka这样的即时消费队列中处理,订单量比较少的情况下,生成订单非常快,用户几乎不用排队。



    3. 扣库存的艺术





    从上面的分析可知,显然预扣库存的方案最合理。我们进一步分析扣库存的细节,这里还有很大的优化空间,库存存在哪里?怎样保证高并发下,正确的扣库存,还能快速的响应用户请求?

    在单机低并发情况下,我们实现扣库存通常是这样的:

    为了保证扣库存和生成订单的原子性,需要采用事务处理,然后取库存判断、减库存,最后提交事务,整个流程有很多IO,对数据库的操作又是阻塞的。这种方式根本不适合高并发的秒杀系统。

    接下来我们对单机扣库存的方案做优化:本地扣库存 。我们把一定的库存量分配到本地机器,直接在内存中减库存,然后按照之前的逻辑异步创建订单。改进过之后的单机系统是这样的:

    这样就避免了对数据库频繁的IO操作,只在内存中做运算,极大的提高了单机抗并发的能力。但是百万的用户请求量单机是无论如何也抗不住的,虽然nginx处理网络请求使用epoll模型,c10k的问题在业界早已得到了解决。但是linux系统下,一切资源皆文件,网络请求也是这样,大量的文件描述符会使操作系统瞬间失去响应。

    上面我们提到了nginx的加权均衡策略,我们不妨假设将100W的用户请求量平均均衡到100台服务器上,这样单机所承受的并发量就小了很多。然后我们每台机器本地库存100张火车票,100台服务器上的总库存还是1万,这样保证了库存订单不超卖,下面是我们描述的集群架构:

    问题接踵而至,在高并发情况下,现在我们还无法保证系统的高可用,假如这100台服务器上有两三台机器因为扛不住并发的流量或者其他的原因宕机了。那么这些服务器上的订单就卖不出去了,这就造成了订单的少卖。

    要解决这个问题,我们需要对总订单量做统一的管理,这就是接下来的容错方案。服务器不仅要在本地减库存,另外要远程统一减库存 。有了远程统一减库存的操作,我们就可以根据机器负载情况,为每台机器分配一些多余的“buffer库存”用来防止机器中有机器宕机的情况。我们结合下面架构图具体分析一下:

    我们采用Redis存储统一库存,因为Redis的性能非常高,号称单机QPS能抗10W的并发。在本地减库存以后,如果本地有订单,我们再去请求redis远程减库存,本地减库存和远程减库存都成功了,才返回给用户抢票成功的提示,这样也能有效的保证订单不会超卖。

    当机器中有机器宕机时,因为每个机器上有预留的buffer余票,所以宕机机器上的余票依然能够在其他机器上得到弥补,保证了不少卖。buffer余票设置多少合适呢,理论上buffer设置的越多,系统容忍宕机的机器数量就越多,但是buffer设置的太大也会对redis造成一定的影响。

    虽然redis内存数据库抗并发能力非常高,请求依然会走一次网络IO,其实抢票过程中对redis的请求次数是本地库存和buffer库存的总量,因为当本地库存不足时,系统直接返回用户“已售罄”的信息提示,就不会再走统一扣库存的逻辑,这在一定程度上也避免了巨大的网络请求量把redis压跨,所以buffer值设置多少,需要架构师对系统的负载能力做认真的考量。



    4. 代码演示





    Go语言原生为并发设计,我采用go语言给大家演示一下单机抢票的具体流程。

    4.1 初始化工作

    go包中的init函数先于main函数执行,在这个阶段主要做一些准备性工作。我们系统需要做的准备工作有:初始化本地库存、初始化远程redis存储统一库存的hash键值、初始化redis连接池;另外还需要初始化一个大小为1的int类型chan,目的是实现分布式锁的功能,也可以直接使用读写锁或者使用redis等其他的方式避免资源竞争,但使用channel更加高效,这就是go语言的哲学:不要通过共享内存来通信,而要通过通信来共享内存 。redis库使用的是redigo,下面是代码实现:

    ...
    //localSpike包结构体定义
    package localSpike

    type LocalSpike struct {
     LocalInStock     int64
     LocalSalesVolume int64
    }
    ...
    //remoteSpike对hash结构的定义和redis连接池
    package remoteSpike
    //远程订单存储健值
    type RemoteSpikeKeys struct {
     SpikeOrderHashKey string //redis中秒杀订单hash结构key
     TotalInventoryKey string //hash结构中总订单库存key
     QuantityOfOrderKey string //hash结构中已有订单数量key
    }

    //初始化redis连接池
    func NewPool() *redis.Pool {
     return &redis.Pool{
      MaxIdle:   10000,
      MaxActive: 12000// max number of connections
      Dial: func() (redis.Conn, error) {
       c, err := redis.Dial("tcp"":6379")
       if err != nil {
        panic(err.Error())
       }
       return c, err
      },
     }
    }
    ...
    func init() {
     localSpike = localSpike2.LocalSpike{
      LocalInStock:     150,
      LocalSalesVolume: 0,
     }
     remoteSpike = remoteSpike2.RemoteSpikeKeys{
      SpikeOrderHashKey:  "ticket_hash_key",
      TotalInventoryKey:  "ticket_total_nums",
      QuantityOfOrderKey: "ticket_sold_nums",
     }
     redisPool = remoteSpike2.NewPool()
     done = make(chan int1)
     done 1
    }

    4.2 本地扣库存和统一扣库存

    本地扣库存逻辑非常简单,用户请求过来,添加销量,然后对比销量是否大于本地库存,返回bool值:

    package localSpike
    //本地扣库存,返回bool值
    func (spike *LocalSpike) LocalDeductionStock() bool{
     spike.LocalSalesVolume = spike.LocalSalesVolume + 1
     return spike.LocalSalesVolume }

    注意这里对共享数据LocalSalesVolume的操作是要使用锁来实现的,但是因为本地扣库存和统一扣库存是一个原子性操作,所以在最上层使用channel来实现,这块后边会讲。统一扣库存操作redis,因为redis是单线程的,而我们要实现从中取数据,写数据并计算一些列步骤,我们要配合lua脚本打包命令,保证操作的原子性:

    package remoteSpike
    ......
    const LuaScript = `
            local ticket_key = KEYS[1]
            local ticket_total_key = ARGV[1]
            local ticket_sold_key = ARGV[2]
            local ticket_total_nums = tonumber(redis.call('HGET', ticket_key, ticket_total_key))
            local ticket_sold_nums = tonumber(redis.call('HGET', ticket_key, ticket_sold_key))
      -- 查看是否还有余票,增加订单数量,返回结果值
           if(ticket_total_nums >= ticket_sold_nums) then
                return redis.call('HINCRBY', ticket_key, ticket_sold_key, 1)
            end
            return 0
    `
    //远端统一扣库存
    func (RemoteSpikeKeys *RemoteSpikeKeys) RemoteDeductionStock(conn redis.Conn) bool {
     lua := redis.NewScript(1, LuaScript)
     result, err := redis.Int(lua.Do(conn, RemoteSpikeKeys.SpikeOrderHashKey, RemoteSpikeKeys.TotalInventoryKey, RemoteSpikeKeys.QuantityOfOrderKey))
     if err != nil {
      return false
     }
     return result != 0
    }

    我们使用hash结构存储总库存和总销量的信息,用户请求过来时,判断总销量是否大于库存,然后返回相关的bool值。在启动服务之前,我们需要初始化redis的初始库存信息:

     hmset ticket_hash_key "ticket_total_nums" 10000 "ticket_sold_nums" 0

    4.3 响应用户信息

    我们开启一个http服务,监听在一个端口上:

    package main
    ...
    func main() {
     http.HandleFunc("/buy/ticket", handleReq)
     http.ListenAndServe(":3005", nil)
    }

    上面我们做完了所有的初始化工作,接下来handleReq的逻辑非常清晰,判断是否抢票成功,返回给用户信息就可以了。

    package main
    //处理请求函数,根据请求将响应结果信息写入日志
    func handleReq(w http.ResponseWriter, r *http.Request) {
     redisConn := redisPool.Get()
     LogMsg := ""
      //全局读写锁
     if localSpike.LocalDeductionStock() && remoteSpike.RemoteDeductionStock(redisConn) {
      util.RespJson(w, 1,  "抢票成功", nil)
      LogMsg = LogMsg + "result:1,localSales:" + strconv.FormatInt(localSpike.LocalSalesVolume, 10)
     } else {
      util.RespJson(w, -1"已售罄", nil)
      LogMsg = LogMsg + "result:0,localSales:" + strconv.FormatInt(localSpike.LocalSalesVolume, 10)
     }
     done 1

     //将抢票状态写入到log中
     writeLog(LogMsg, "./stat.log")
    }

    func writeLog(msg string, logPath string) {
     fd, _ := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
     defer fd.Close()
     content := strings.Join([]string{msg, "rn"}, "")
     buf := []byte(content)
     fd.Write(buf)
    }

    前边提到我们扣库存时要考虑竞态条件,我们这里是使用channel避免并发的读写,保证了请求的高效顺序执行。我们将接口的返回信息写入到了./stat.log文件方便做压测统计。

    4.4 单机服务压测

    开启服务,我们使用ab压测工具进行测试:

    ab -n 10000 -c 100 http://127.0.0.1:3005/buy/ticket

    下面是我本地低配mac的压测信息

    This is ApacheBench, Version 2.3 1826891 $>
    Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
    Licensed to The Apache Software Foundation, http://www.apache.org/

    Benchmarking 127.0.0.1 (be patient)
    Completed 1000 requests
    Completed 2000 requests
    Completed 3000 requests
    Completed 4000 requests
    Completed 5000 requests
    Completed 6000 requests
    Completed 7000 requests
    Completed 8000 requests
    Completed 9000 requests
    Completed 10000 requests
    Finished 10000 requests


    Server Software:
    Server Hostname:        127.0.0.1
    Server Port:            3005

    Document Path:          /buy/ticket
    Document Length:        29 bytes

    Concurrency Level:      100
    Time taken for tests:   2.339 seconds
    Complete requests:      10000
    Failed requests:        0
    Total transferred:      1370000 bytes
    HTML transferred:       290000 bytes
    Requests per second:    4275.96 [#/sec] (mean)
    Time per request:       23.387 [ms] (mean)
    Time per request:       0.234 [ms] (mean, across all concurrent requests)
    Transfer rate:          572.08 [Kbytes/sec] received

    Connection Times (ms)
                  min  mean[+/-sd] median   max
    Connect:        0    8  14.7      6     223
    Processing:     2   15  17.6     11     232
    Waiting:        1   11  13.5      8     225
    Total:          7   23  22.8     18     239

    Percentage of the requests served within a certain time (ms)
      50%     18
      66%     24
      75%     26
      80%     28
      90%     33
      95%     39
      98%     45
      99%     54
     100%    239 (longest request)

    根据指标显示,我单机每秒就能处理4000+的请求,正常服务器都是多核配置,处理1W+的请求根本没有问题。而且查看日志发现整个服务过程中,请求都很正常,流量均匀,redis也很正常:

    //stat.log
    ...
    result:1,localSales:145
    result:1,localSales:146
    result:1,localSales:147
    result:1,localSales:148
    result:1,localSales:149
    result:1,localSales:150
    result:0,localSales:151
    result:0,localSales:152
    result:0,localSales:153
    result:0,localSales:154
    result:0,localSales:156
    ...


    5.总结回顾





    总体来说,秒杀系统是非常复杂的。我们这里只是简单介绍模拟了一下单机如何优化到高性能,集群如何避免单点故障,保证订单不超卖、不少卖的一些策略,完整的订单系统还有订单进度的查看,每台服务器上都有一个任务,定时的从总库存同步余票和库存信息展示给用户,还有用户在订单有效期内不支付,释放订单,补充到库存等等。

    我们实现了高并发抢票的核心逻辑,可以说系统设计的非常的巧妙,巧妙的避开了对DB数据库IO的操作,对Redis网络IO的高并发请求,几乎所有的计算都是在内存中完成的,而且有效的保证了不超卖、不少卖,还能够容忍部分机器的宕机。我觉得其中有两点特别值得学习总结:

    (1)负载均衡,分而治之。通过负载均衡,将不同的流量划分到不同的机器上,每台机器处理好自己的请求,将自己的性能发挥到极致,这样系统的整体也就能承受极高的并发了,就像工作的的一个团队,每个人都将自己的价值发挥到了极致,团队成长自然是很大的。

    (2)合理的使用并发和异步。自epoll网络架构模型解决了c10k问题以来,异步越来被服务端开发人员所接受,能够用异步来做的工作,就用异步来做,在功能拆解上能达到意想不到的效果,这点在nginx、node.js、redis上都能体现,他们处理网络请求使用的epoll模型,用实践告诉了我们单线程依然可以发挥强大的威力。服务器已经进入了多核时代,go语言这种天生为并发而生的语言,完美的发挥了服务器多核优势,很多可以并发处理的任务都可以使用并发来解决,比如go处理http请求时每个请求都会在一个goroutine中执行,总之:怎样合理的压榨CPU,让其发挥出应有的价值,是我们一直需要探索学习的方向。

  • 太牛了,前后端完全开源,快速搭建简约美观的在线可互动的教室,星标4.5k!

    快乐分享,Java干货及时送达👇

    Agora Flat开源教室是一款高效实用且专业的互动课堂软件,Agora Flat开源教室能够帮助用户快速搭建简约的互动教室,实现网上学习和授课等功能,提高教学效率。

    Agora Flat开源教室客户端支持大小课班和一对一的模式,用户可以多情景灵便转换,进行优质课堂教学。

    特性

    • 实时交互:多功能互动白板,实时音视频(RTC)通讯,即时消息(RTM)聊天
    • 登录方式:微信,GitHub
    • 房间管理:加入、创建、预定房间,支持周期性房间
    • 课堂录制回放:白板信令回放,音视频云录制回放,群聊信令回放
    • 多媒体课件云盘
    • 屏幕共享

    功能

    轻松创建丰富多样的在线教室

    • 大班课

    适用于大型课堂授课,知识传递效率更高。学生可举手发言参与在线互动。

    • 小班课

    适用于 1 名教师和 2~16 名学生进行在线教学,互动性更及时,趣味性更高,提高教学效率的同时保证质量。

    • 一对一

    适用于 1 名老师对 1 名学生,沉浸式的个性化课堂教学,学习效果更加明显。

    支持自定义主题和 UI

    场景插件:无限的组合,无限的可能

    安装运行

    快速上手

    你可以在没有服务端的情况下构建并运行 Flat 客户端。此仓库包含以下项目:

    Flat Electron 客户端

    https://github.com/netless-io/flat/tree/main/desktop

    Flat Web 客户端

    https://github.com/netless-io/flat/tree/main/web

    安装

    如果你还没有安装 pnpm:

    npm i -g pnpm

    Clone 或者 fork 这个项目,在根目录执行:

    pnpm i

    构建并运行 Flat Electron 客户端

    在仓库根目录运行以下命令:

    pnpm start

    你可以运行以下命令将项目打包成可执行文件:

    • 项目根目录执行 pnpm ship 将根据当前系统打包。
    • 或者项目根目录执行 pnpm ship:macpnpm ship:win 可针对指定的系统打包。

    如果你因为网络问题导致无法下载 electron,可在项目目录新建: .npmrc 文件,并写入 ELECTRON_MIRROR=" https://npmmirror.com/mirrors/electron/" 内容。重新执行 pnpm i 即可。

    构建并运行 Flat Web 客户端

    在仓库根目录运行以下任意一个命令:

    pnpm start:web
    cd ./web/flat-web/ && pnpm start

    在 Flat 中 UI 逻辑与业务逻辑分开开发。可以通过 Storybook 快速查看与开发部分 UI。在项目根目录执行 pnpm storybook 在本地运行 Storybook。

    相关项目

    Flat 安卓客户端

    • https://github.com/netless-io/flat-android

    Flat 服务端

    • https://github.com/netless-io/flat-server

    Flat 主页

    • https://flat.whiteboard.agora.io/#download

    传送门

    • 开源地址:https://github.com/netless-io/flat

    整理:骑猪看星星

    来源:开源技术专栏

  • 牛逼哄哄的 BitMap,到底牛逼在哪?

    快乐分享,Java干货及时送达👇

    文章来源:https://www.cnblogs.com/cjsblog/p/11613708.html


    BitMap


    Bit-map的基本思想就是用一个bit位来标记某个元素对应的Value,而Key即是该元素。由于采用了Bit为单位来存储数据,因此在存储空间方面,可以大大节省。(PS:划重点 节省存储空间 )

    假设有这样一个需求:在20亿个随机整数中找出某个数m是否存在其中,并假设32位操作系统,4G内存

    在Java中,int占4字节,1字节=8位(1 byte = 8 bit)

    如果每个数字用int存储,那就是20亿个int,因而占用的空间约为 (2000000000*4/1024/1024/1024)≈7.45 G

    如果按位存储就不一样了,20亿个数就是20亿位,占用空间约为 (2000000000/8/1024/1024/1024)≈0.233 G

    高下立判,无需多言

    那么,问题来了,如何表示一个数呢?

    刚才说了,每一位表示一个数,0表示不存在,1表示存在,这正符合二进制

    这样我们可以很容易表示{1,2,4,6}这几个数:

    计算机内存分配的最小单位是字节,也就是8位,那如果要表示{12,13,15}怎么办呢?

    当然是在另一个8位上表示了:

    图片

    这样的话,好像变成一个二维数组了

    1个int占32位,那么我们只需要申请一个int数组长度为 int tmp[1+N/32] 即可存储,其中N表示要存储的这些数中的最大值,于是乎:

    tmp[0]:可以表示0~31

    tmp[1]:可以表示32~63

    tmp[2]:可以表示64~95

    。。。

    如此一来,给定任意整数M,那么M/32就得到下标,M%32就知道它在此下标的哪个位置


    添加


    这里有个问题,我们怎么把一个数放进去呢?例如,想把5这个数字放进去,怎么做呢?

    首先,5/32=0,5%32=5,也是说它应该在tmp[0]的第5个位置,那我们把1向左移动5位,然后按位或

    换成二进制就是

    这就相当于 86 | 32 = 118

    86 | (1

    b[0] = b[0] | (1

    也就是说,要想插入一个数,将1左移带代表该数字的那一位,然后与原数进行按位或操作

    化简一下,就是 86 + (5/8) | (1

    因此,公式可以概括为:p + (i/8)|(1


    清除


    以上是添加,那如果要清除该怎么做呢?

    还是上面的例子,假设我们要6移除,该怎么做呢?

    从图上看,只需将该数所在的位置为0即可

    1左移6位,就到达6这个数字所代表的位,然后按位取反,最后与原数按位与,这样就把该位置为0了

    b[0] = b[0] & (~(1

    b[0] = b[0] & (~(1


    查找


    前面我们也说了,每一位代表一个数字,1表示有(或者说存在),0表示无(或者说不存在)。通过把该为置为1或者0来达到添加和清除的小伙,那么判断一个数存不存在就是判断该数所在的位是0还是1

    假设,我们想知道3在不在,那么只需判断 b[0] & (1


    Bitmap有什么用


    量数据的快速排序、查找、去重



    快速排序


    假设我们要对0-7内的5个元素(4,7,2,5,3)排序(这里假设这些元素没有重复),我们就可以采用Bit-map的方法来达到排序的目的。

    要表示8个数,我们就只需要8个Bit(1Bytes),首先我们开辟1Byte的空间,将这些空间的所有Bit位都置为0,然后将对应位置为1。

    最后,遍历一遍Bit区域,将该位是一的位的编号输出(2,3,4,5,7),这样就达到了排序的目的,时间复杂度O(n)。

    优点:

    • 运算效率高,不需要进行比较和移位;
    • 占用内存少,比如N=10000000;只需占用内存为N/8=1250000Byte=1.25M

    缺点:

    • 所有的数据不能重复。即不可对重复的数据进行排序和查找。
    • 只有当数据比较密集时才有优势

    快速去重


    20亿个整数中找出不重复的整数的个数,内存不足以容纳这20亿个整数。

    首先,根据“内存空间不足以容纳这05亿个整数”我们可以快速的联想到Bit-map。下边关键的问题就是怎么设计我们的Bit-map来表示这20亿个数字的状态了。其实这个问题很简单,一个数字的状态只有三种,分别为不存在,只有一个,有重复。因此,我们只需要2bits就可以对一个数字的状态进行存储了,假设我们设定一个数字不存在为00,存在一次01,存在两次及其以上为11。那我们大概需要存储空间2G左右。

    接下来的任务就是把这20亿个数字放进去(存储),如果对应的状态位为00,则将其变为01,表示存在一次;如果对应的状态位为01,则将其变为11,表示已经有一个了,即出现多次;如果为11,则对应的状态位保持不变,仍表示出现多次。

    最后,统计状态位为01的个数,就得到了不重复的数字个数,时间复杂度为O(n)。

    快速查找


    这就是我们前面所说的了,int数组中的一个元素是4字节占32位,那么除以32就知道元素的下标,对32求余数(%32)就知道它在哪一位,如果该位是1,则表示存在。



    小结&回顾


    Bitmap主要用于快速检索关键字状态,通常要求关键字是一个连续的序列(或者关键字是一个连续序列中的大部分), 最基本的情况,使用1bit表示一个关键字的状态(可标示两种状态),但根据需要也可以使用2bit(表示4种状态),3bit(表示8种状态)。

    Bitmap的主要应用场合:表示连续(或接近连续,即大部分会出现)的关键字序列的状态(状态数/关键字个数 越小越好)。

    32位机器上,对于一个整型数,比如int a=1 在内存中占32bit位,这是为了方便计算机的运算。但是对于某些应用场景而言,这属于一种巨大的浪费,因为我们可以用对应的32bit位对应存储十进制的0-31个数,而这就是Bit-map的基本思想。Bit-map算法利用这种思想处理大量数据的排序、查询以及去重。

    补充1


    在数字没有溢出的前提下,对于正数和负数,左移一位都相当于乘以2的1次方,左移n位就相当于乘以2的n次方,右移一位相当于除2,右移n位相当于除以2的n次方。

    >> 右移,相当于除以2的n次方,例如:64>>3 相当于64÷8=8

    ^ 异或,相当于求余数,例如:48^32 相当于 48%32=16

    补充2


    不使用第三方变量,交换两个变量的值

    // 方式一
    a = a + b;
    b = a - b;
    a = a - b;

    // 方式二
    a = a ^ b;
    b = a ^ b;
    a = a ^ b;


    BitSet


    BitSet实现了一个位向量,它可以根据需要增长。每一位都有一个布尔值。一个BitSet的位可以被非负整数索引(PS:意思就是每一位都可以表示一个非负整数)。可以查找、设置、清除某一位。通过逻辑运算符可以修改另一个BitSet的内容。默认情况下,所有的位都有一个默认值false。

    图片
    图片
    图片
    图片
    图片

    可以看到,跟我们前面想的差不多

    用一个long数组来存储,初始长度64,set值的时候首先右移6位(相当于除以64)计算在数组的什么位置,然后更改状态位

    别的看不懂不要紧,看懂这两句就够了:

    int wordIndex = wordIndex(bitIndex);
    words[wordIndex] |= (1L 


    Bloom Filters


    图片

    Bloom filter 是一个数据结构,它可以用来判断某个元素是否在集合内,具有运行快速,内存占用小的特点。

    而高效插入和查询的代价就是,Bloom Filter 是一个基于概率的数据结构:它只能告诉我们一个元素绝对不在集合内或可能在集合内。

    Bloom filter 的基础数据结构是一个 比特向量(可理解为数组)。

    主要应用于大规模数据下不需要精确过滤的场景,如检查垃圾邮件地址,爬虫URL地址去重,解决缓存穿透问题等

    如果想判断一个元素是不是在一个集合里,一般想到的是将集合中所有元素保存起来,然后通过比较确定。链表、树、散列表(哈希表)等等数据结构都是这种思路,但是随着集合中元素的增加,需要的存储空间越来越大;同时检索速度也越来越慢,检索时间复杂度分别是O(n)、O(log n)、O(1)。

    布隆过滤器的原理是,当一个元素被加入集合时,通过 K 个散列函数将这个元素映射成一个位数组(Bit array)中的 K 个点,把它们置为 1 。检索时,只要看看这些点是不是都是1就知道元素是否在集合中;如果这些点有任何一个 0,则被检元素一定不在;如果都是1,则被检元素很可能在(之所以说“可能”是误差的存在)。


    BloomFilter 流程

    1、 首先需要 k 个 hash 函数,每个函数可以把 key 散列成为 1 个整数;

    2、初始化时,需要一个长度为 n 比特的数组,每个比特位初始化为 0;

    3、某个 key 加入集合时,用 k 个 hash 函数计算出 k 个散列值,并把数组中对应的比特位置为 1;

    4、判断某个 key 是否在集合时,用 k 个 hash 函数计算出 k 个散列值,并查询数组中对应的比特位,如果所有的比特位都是1,认为在集合中。

    dependency>
        groupId>com.google.guavagroupId>
        artifactId>guavaartifactId>
        version>28.1-jreversion>
    dependency>

  • 还在只用 RedisTemplate 访问 Redis 吗?这几种方式给你安排上!

    快乐分享,Java干货及时送达👇

    • 开始准备
    • RedisTemplate
    • JPA Repository
    • Cache
    • 总结

    开始准备

    开始之前我们需要有Redis安装,我们采用本机Docker运行Redis,主要命令如下

    docker pull redis
    docker run --name my_redis -d -p 6379:6379 redis
    docker exec -it my_redis bash
    redis-cli

    前面两个命令是启动redis docker,后两个是连接到docker,在使用redis-cli 去查看redis里面的内容,主要查看我们存在redis里面的数据。

    RedisTemplate

    我们先从RedisTemplate开始,这个是最好理解的一种方式,我之前在工作中也使用过这种方式,先看代码示例 我们先定义一个POJO类

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public class Book implements Serializable {
        private Long id;
        private String name;
        private String author;
    }

    一个很简单的BOOK类,三个字段:idnameauthor。再来一个RedisTemplate的Bean

    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate template = new RedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

    再定义一个使用这个RedisTemplate的Service类

    public Optional findOneBook(String name) {
        HashOperations hashOperations = redisTemplate.opsForHash();
        if (redisTemplate.hasKey(CACHE) && hashOperations.hasKey(CACHE, name)) {
            log.info("Get book {} from Redis.", name);
            return Optional.of(hashOperations.get(CACHE, name));
        }

        Optional book = bookRepository.getBook(name);
        log.info("Book Found: {}", book);
        if (book.isPresent()) {
            log.info("Put book {} to Redis.", name);
            hashOperations.put(CACHE, name, book.get());
            redisTemplate.expire(CACHE, 10, TimeUnit.MINUTES);
        }
        return book;
    }

    我们使用Hash来存储这个Book信息,在上面的方法中查找书名存不存在Redis中,如果存在就直接返回,如果不存在就去持久化存储中找,找到就再通过Template写入到Redis中, 这是缓存的通用做法。使用起来感觉很方便。

    我们这里为了简单没有使用持久化存储,就硬编码了几条数据,代码如下

    @Repository
    public class BookRepository {
        Map bookMap = new HashMap();
        public BookRepository(){
            bookMap.put("apache kafka", Book.builder()
                    .name("apache kafka").id(1L).author("zhangsan")
                    .build());
            bookMap.put("python", Book.builder()
                    .name("python").id(2L).author("lisi")
                    .build());
        }

        public Optional getBook(String name){
            if(bookMap.containsKey(name)){
                return Optional.of(bookMap.get(name));
            }
            else{
                return Optional.empty();
            }
        }
    }

    我们调用 bookService.findOneBook("python")bookService.findOneBook("apache kafka"); 来把数据写入到换存中

    我们来看下存储在Redis的数据长什么样子。

    127.0.0.1:6379> keys *
    1"xacxedx00x05tx00x04book"
    127.0.0.1:6379> type "xacxedx00x05tx00x04book"
    hash
    127.0.0.1:6379> hgetall "xacxedx00x05tx00x04book"
    1"xacxedx00x05tx00x06python"
    2"xacxedx00x05srx00&com.ken.redistemplatesample.model.Book=x19x96xfbx7fx7fxdaxbex02x00x03Lx00x06authortx00x12Ljava/lang/String;Lx00x02idtx00x10Ljava/lang/Long;Lx00x04nameqx00~x00x01xptx00x04lisisrx00x0ejava.lang.Long;x8bxe4x90xccx8f#xdfx02x00x01Jx00x05valuexrx00x10java.lang.Numberx86xacx95x1dx0bx94xe0x8bx02x00x00xpx00x00x00x00x00x00x00x02tx00x06python"
    3"xacxedx00x05tx00x0capache kafka"
    4"xacxedx00x05srx00&com.ken.redistemplatesample.model.Book=x19x96xfbx7fx7fxdaxbex02x00x03Lx00x06authortx00x12Ljava/lang/String;Lx00x02idtx00x10Ljava/lang/Long;Lx00x04nameqx00~x00x01xptx00bzhangsansrx00x0ejava.lang.Long;x8bxe4x90xccx8f#xdfx02x00x01Jx00x05valuexrx00x10java.lang.Numberx86xacx95x1dx0bx94xe0x8bx02x00x00xpx00x00x00x00x00x00x00x01tx00x0capache kafka"

    我们可以看到数据被存在了key是“xacxedx00x05tx00x04book”的一个Hash表中, Hash里面有两条记录。大家发现一个问题没有?

    就是这个key不是我们想象的用“book”做key,而是多了一串16进制的码, 这是因为RedisTemplate使用了默认的JdkSerializationRedisSerializer 去序列化我们的key和value,如果大家都用Java语言那没有问题, 如果有人用Java语言写,有人用别的语言读,那就有问题,就像我开始的时候用hgetall “book”始终拿不到数据那样。

    RedisTemplate也提供了StringRedisTemplate来方便大家需要使用String来序列化redis里面的数据。简单看下代码

    @Bean
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)
    {
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

    public Optional getBookString(String name){
        HashOperations hashOperations = stringRedisTemplate.opsForHash();
        if (stringRedisTemplate.hasKey(STRINGCACHE) && hashOperations.hasKey(STRINGCACHE, name)) {
            log.info("Get book {} from Redis.", name);
            return Optional.of(hashOperations.get(STRINGCACHE, name));
        }

        Optional book = bookRepository.getBook(name);
        log.info("Book Found: {}", book);
        if (book.isPresent()) {
            log.info("Put book {} to Redis.", name);
            hashOperations.put(STRINGCACHE, name, book.get().getAuthor());
            stringRedisTemplate.expire(STRINGCACHE, 10, TimeUnit.MINUTES);
            return Optional.of(book.get().getAuthor());
        }
        return Optional.empty();
    }

    使用上就没有那么方便,你就得自己写需要存的是哪个字段,读出来是哪个字段。

    127.0.0.1:6379> keys *
    1) "string_book"
    127.0.0.1:6379> hgetall string_book
    1) "python"
    2) "lisi"
    3) "apache kafka"
    4) "zhangsan"

    如上图所示,使用客户端读出来看起来就比较清爽一些。也可以看到占用的Size会小很多,我们这个例子相差7倍,如果是数据量大,这个还是比较大的浪费。

    127.0.0.1:6379> keys *
    1) "xacxedx00x05tx00x04book"
    2) "string_book"
    127.0.0.1:6379> memory usage "string_book"
    (integer) 104
    127.0.0.1:6379> memory usage "xacxedx00x05tx00x04book"
    (integer) 712

    JPA Repository

    我们知道使用JPA Repository来访问DataBase的时候,增删改查那样的操作能够很方便的实现,基本就是定义个接口,代码都不用写,Spring就帮我们完成了大部分的工作,那么访问Redis是不是也可以这样呢?答案是肯定的,我们来看代码 首先我们还是定义一个POJO

    @RedisHash(value = "cache-book", timeToLive = 600)
    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public class CacheBook implements Serializable {

        @Id
        private Long userId;

        @Indexed
        private String name;

        private String author;
    }

    这个类与我们上面template上面的类的区别就是我们加了两个注解, 在类开头加了 @RedisHash(value = "cache-book", timeToLive = 600) 在字段上面加了@Id@Indexed 定义一个Repository的接口

    public interface CacheBookRepository extends CrudRepositoryCacheBookLong> {
        Optional findOneByName(String name);
    }

    再定义一个service和上面那个例子template一样,缓存中有就到缓存中拿,没有就到持久化存储中找,并写入缓存

    @Slf4j
    @Service
    public class BookService {
        private static final String CACHE = "repository-book";
        @Autowired
        private CacheBookRepository cacheRepository;

        @Autowired
        private BookRepository bookRepository;

        public Optional findOneBook(String name) {
            Optional optionalCacheBook = cacheRepository.findOneByName(name);
            if(!optionalCacheBook.isPresent())
            {
                Optional book = bookRepository.getBook(name);
                log.info("Book Found: {}", book);
                if (book.isPresent()) {
                    log.info("Put book {} to Redis.", name);
                    cacheRepository.save(book.get());
                }
                return book;
            }
            return optionalCacheBook;
        }
    }

    代码很简单,简单到不敢相信是真的。还是一样,调用这个方法,我们来看存在Redis里面的数据

    127.0.0.1:6379> keys *
    1) "repository-book:2"
    2) "repository-book:2:idx"
    3) "repository-book"
    4) "repository-book:name:apache kafka"
    5) "repository-book:name:python"
    6) "repository-book:1:idx"
    7) "repository-book:1"

    哇,感觉存的内容有些多, 不用怕我们来看下各自存什么数据 首先看最短的一个

    127.0.0.1:6379> smembers repository-book
    1) "1"
    2) "2"

    它里面存的是我们的id所有的value,可以用来判断id是否存在 再来看

    127.0.0.1:6379> hgetall repository-book:2
    1) "_class"
    2) "com.ken.redisrepositorysample.model.CacheBook"
    3) "author"
    4) "lisi"
    5) "name"
    6) "python"
    7) "userId"
    8) "2"

    这个是我们数据存放的地方

    127.0.0.1:6379> smembers repository-book:1:idx
    1) "repository-book:name:apache kafka"
    127.0.0.1:6379> smembers "repository-book:name:apache kafka"
    1) "1"

    另外两个都是set, 存在在它们里面的数据是索引信息。由此可以看出通过JPA Repository 的方式,代码很少,而且存储的数据也很通用,个人觉得是比较理想的访问方法。

    Cache

    我们已经看了两种方式,在访问的时候遵循这样的模式:缓存中有就从缓存中返回,没有就从持久化存储中找,然后写入缓存,这部分代码我也不想自己写,那么Cache就是你的救星。

    我们先看代码 我们这次使用内存数据库H2作为持久化存储, 放一个schema.sql在resouces下面

    drop table t_book if exists;


    create table t_book (
        id bigint auto_increment,
        create_time timestamp,
        update_time timestamp,
        name varchar(255),
        author varchar(200),
        primary key (id)
    );

    insert into t_book (name, author, create_time, update_time) values ('python''zhangsan'now(), now());
    insert into t_book (name, author, create_time, update_time) values ('hadoop''lisi'now(), now());
    insert into t_book (name, author, create_time, update_time) values ('java''wangwu'now(), now());

    然后定义POJO

    @Entity
    @Table(name = "T_BOOK")
    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public class CacheBook implements Serializable {

        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;

        private String name;

        private String author;

        @Column(updatable = false)
        @CreationTimestamp
        private Date createTime;
        @UpdateTimestamp
        private Date updateTime;
    }

    完全是和数据库绑定的代码,和缓存没有任何关系 一个Repository来访问数据库

    public interface BookRepository extends JpaRepositoryCacheBookLong> {
    }

    定义一个service来调用它

    @Slf4j
    @Service
    @CacheConfig(cacheNames = "cache-book")
    public class BookService {

        @Autowired
        private BookRepository bookRepository;

        @Cacheable
        public List findAllCoffee() {
            return bookRepository.findAll();
        }

        @CacheEvict
        public void reloadCoffee() {
        }
    }

    这里就比较关键了,在类上加上了注解 @CacheConfig(cacheNames = "cache-book") 在方法上面加上了Cacheable和CacheEvict, Cacheable这个方法就是用来实现逻辑,有就从缓存中拿,没有就从数据库拿的,CacheEvict是调用这个方法的时候清除缓存。

    然后再启动入口程序的地方加上注解 @EnableJpaRepositories @EnableCaching(proxyTargetClass = true) 在配置文件application.properties中加上

    spring.jpa.hibernate.ddl-auto=none
    spring.jpa.properties.hibernate.show_sql=true
    spring.jpa.properties.hibernate.format_sql=true

    management.endpoints.web.exposure.include=*

    spring.cache.type=redis
    spring.cache.cache-names=cache-book
    spring.cache.redis.time-to-live=600000
    spring.cache.redis.cache-null-values=false

    spring.redis.host=localhost

    这样就可以了, 感觉就是通过配置下就把缓存给完成了,非常的简单 我们来看Redis中是怎么存的

    127.0.0.1:6379> keys *
    1) "cache-book::SimpleKey []"
    127.0.0.1:6379> get "cache-book::SimpleKey []"
    "xacxedx00x05srx00x13java.util.ArrayListxx81xd2x1dx99xc7ax9dx03x00x01Ix00x04sizexpx00x00x00x03wx04x00x00x00x03srx00(com.ken.rediscachesample.model.CacheBookxecxcbR=xe1Ux9bxf7x02x00x05Lx00x06authortx00x12Ljava/lang/String;Lx00ncreateTimetx00x10Ljava/util/Date;Lx00x02idtx00x10Ljava/lang/Long;Lx00x04nameqx00~x00x03Lx00nupdateTimeqx00~x00x04xptx00bzhangsansrx00x12java.sql.Timestamp&x18xd5xc8x01Sxbfex02x00x01Ix00x05nanosxrx00x0ejava.util.Datehjx81x01KYtx19x03x00x00xpwbx00x00x01x84xff]x85xb0xb-x81x80srx00x0ejava.lang.Long;x8bxe4x90xccx8f#xdfx02x00x01Jx00x05valuexrx00x10java.lang.Numberx86xacx95x1dx0bx94xe0x8bx02x00x00xpx00x00x00x00x00x00x00x01tx00x06pythonsqx00~x00bwbx00x00x01x84xff]x85xb0xb-x81x80sqx00~x00x02tx00x04lisisqx00~x00bwbx00x00x01x84xff]x85xb0xb

    看到没有,就是当成Redis里面的String来存的, 如果数据量比较小,那是非常的方便,如果数据量大,这种方式就有些问题了。

    总结

    我们看了这三种方式,这里仅仅是做了个入门,每个里面都有很多细节的地方需要去研究和使用,整体的感觉是要想使用的简单,那么存储在Redis中的数据就要量少,量大后,就需要自己来定制了,那基本上要用RedisTemplate来做一些工作。

    感谢阅读,希望对你有所帮助 🙂 

    来源:https://juejin.cn/post/7180290294481420348

    
    

  • 快速交付神器:阿里巴巴官方低代码引擎开源了!

    快乐分享,Java干货及时送达👇

    来源:开源技术专栏(ID:GitHubKY)p

    LowCodeEngine是由阿里巴巴钉钉团队开源的低代码引擎, 该引擎全面遵循《阿里巴巴中后端前端基础构建协议规范》和《阿里巴巴中后端前端素材协议规范》。兼容主流浏览器:Chrome >= 80Edge >= 80safari 和 firefox 最近 2 个 版本

    特性

    • 🌈 提炼自企业级低代码平台的面向扩展设计的内核引擎,奉行最小内核,最强生态的设计理念
    • 📦 开箱即用的高质量生态元素,包括 物料体系、设置器、插件 等
    • ⚙️ 完善的工具链,支持 物料体系、设置器、插件 等生态元素的全链路研发周期
    • 🔌 强大的扩展能力,已支撑 100+ 个各种类型低代码平台
    • 🛡 使用 TypeScript 开发,提供完整的类型定义文件

    擎协议

    引擎完整实现了《低代码引擎搭建协议规范》 和《低代码引擎物料协议规范》 ,协议栈是低代码领域的物料能否流通的关键部分。

    使用示例

    npm install @alilc/lowcode-engine --save-dev

    TIPS:仅支持 cdn 方式引入,npm 包用于提供 typings 等代码提示能力

    import { init, skeleton } from '@alilc/lowcode-engine';  
      
    skeleton.add({  
      area'topArea',  
      type'Widget',  
      name'logo',  
      content: YourFantaticLogo,  
      contentProps: {  
        logo:  
          'https://img.alicdn.com/tfs/TB1_SocGkT2gK0jSZFkXXcIQFXa-66-66.png',  
        href'/',  
      },  
      props: {  
        align'left',  
        width100,  
      },  
    });  
      
    init(document.getElementById('lce'));

    工程化配置:

    {  
      "externals": {  
        "@alilc/lowcode-engine""var window.AliLowCodeEngine",  
        "@alilc/lowcode-engine-ext""var window.AliLowCodeEngineExt"  
      }  
    }

    cdn 可选方式:

    方式 1(推荐):alifd cdn

    https://alifd.alicdn.com/npm/@alilc/lowcode-engine@1.0.0/dist/js/engine-core.js  
      
    https://alifd.alicdn.com/npm/@alilc/lowcode-react-simulator-renderer@1.0.0/dist/js/react-simulator-renderer.js

    方式 2:unpkg

    https://unpkg.com/@alilc/lowcode-engine@1.0.0/dist/js/engine-core.js  
      
    https://unpkg.com/@alilc/lowcode-react-simulator-renderer@1.0.0/dist/js/react-simulator-renderer.js

    方式 3:jsdelivr

    https://cdn.jsdelivr.net/npm/@alilc/lowcode-engine@1.0.0/dist/js/engine-core.js  
      
    https://cdn.jsdelivr.net/npm/@alilc/lowcode-react-simulator-renderer@1.0.0/dist/js/react-simulator-renderer.js

    方式 4:使用自有 cdn

    将源码中 packages/engine/dist 和 packages/(react|rax)-simulator-renderer/dist 下的文件传至你的 cdn 提供商

    界面功能

    低代码编辑器中的区块主要包含这些功能点:

    物料面板

    可以查找组件,并在此拖动组件到编辑器画布中:

    大纲面板

    可以调整页面内的组件树结构:

    可以在这里打开或者关闭模态浮层的展现:

    源码面板

    可以编辑页面级别的 JavaScript 代码和 CSS 配置:

    Schema 编辑

    【开发者专属】可以编辑页面的底层 Schema 数据:

    搭配顶部操作区的“保存到本地”和“重置页面”功能,可以实验各种 schema 对低代码页面的改变。

    编辑画布区域

    点击组件在右侧面板中能够显示出对应组件的属性配置选项:

    拖拽修改组件的排列顺序:

    将组件拖拽到容器类型的组件中,注意拖拽时会在右侧提示当前的组件树:

    属性

    组件的基础属性值设置:

    样式

    组件的样式配置,如文字:

    事件

    绑定组件对外暴露的事件:

    高级

    循环、条件渲染与 key 设置:

    案例

    钉钉宜搭是阿里巴巴自研的低代码应用开发平台

    Parts造物是阿里巴巴自研的低代码物料管理、物料集成、物料研发的产品

    传送门

    开源地址:

    • https://github.com/alibaba/lowcode-engine
    
    

  • MySQL 数据同步到 Redis 缓存, so Easy !

    快乐分享,Java干货及时送达👇

    来源:dongshao.blog.csdn.net/article/details/107190925

    本文介绍MySQL与Redis缓存的同步的两种方案


    • 方案1:通过MySQL自动同步刷新Redis,MySQL触发器+UDF函数实现

    • 方案2:解析MySQL的binlog实现,将数据库中的数据同步到Redis

    一、方案1(UDF)


    • 场景分析: 当我们对MySQL数据库进行数据操作时,同时将相应的数据同步到Redis中,同步到Redis之后,查询的操作就从Redis中查找
    • 过程大致如下:
      • 在MySQL中对要操作的数据设置触发器Trigger,监听操作
      • 客户端(NodeServer)向MySQL中写入数据时,触发器会被触发,触发之后调用MySQL的UDF函数
      • UDF函数可以把数据写入到Redis中,从而达到同步的效果

    • 方案分析:
      • 这种方案适合于读多写少,并且不存并发写的场景
      • 因为MySQL触发器本身就会造成效率的降低,如果一个表经常被操作,这种方案显示是不合适的

    演示案例

    • 下面是MySQL的表

    • 下面是UDF的解析代码

    • 定义对应的触发器


    二、方案2(解析binlog)


    • 在介绍方案2之前我们先来介绍一下MySQL复制的原理,如下图所示:
      • 主服务器操作数据,并将数据写入Bin log
      • 从服务器调用I/O线程读取主服务器的Bin log,并且写入到自己的Relay log中,再调用SQL线程从Relay log中解析数据,从而同步到自己的数据库中

    • 方案2就是:
      • 上面MySQL的整个复制流程可以总结为一句话,那就是:从服务器读取主服务器Bin log中的数据,从而同步到自己的数据库中
      • 我们方案2也是如此,就是在概念上把主服务器改为MySQL,把从服务器改为Redis而已(如下图所示),当MySQL中有数据写入时,我们就解析MySQL的Bin log,然后将解析出来的数据写入到Redis中,从而达到同步的效果

    • 例如下面是一个云数据库实例分析:
      • 云数据库与本地数据库是主从关系。云数据库作为主数据库主要提供写,本地数据库作为从数据库从主数据库中读取数据
      • 本地数据库读取到数据之后,解析Bin log,然后将数据写入写入同步到Redis中,然后客户端从Redis读数据

    • 这个技术方案的难点就在于: 如何解析MySQL的Bin Log。但是这需要对binlog文件以及MySQL有非常深入的理解,同时由于binlog存在Statement/Row/Mixedlevel多种形式,分析binlog实现同步的工作量是非常大的

    Canal开源技术

    • canal是阿里巴巴旗下的一款开源项目,纯Java开发。基于数据库增量日志解析,提供增量数据订阅&消费,目前主要支持了MySQL(也支持mariaDB)
    • 开源参考地址有:https://github.com/liukelin/canal_mysql_nosql_sync
    • 工作原理(模仿MySQL复制):
      • canal模拟mysql slave的交互协议,伪装自己为mysql slave,向mysql master发送dump协议
      • mysql master收到dump请求,开始推送binary log给slave(也就是canal)
      • canal解析binary log对象(原始为byte流)

    • 架构:
      • eventParser (数据源接入,模拟slave协议和master进行交互,协议解析)
      • eventSink (Parser和Store链接器,进行数据过滤,加工,分发的工作)
      • eventStore (数据存储)
      • metaManager (增量订阅&消费信息管理器)
      • server代表一个canal运行实例,对应于一个jvm
      • instance对应于一个数据队列 (1个server对应1..n个instance)
      • instance模块:

    • 大致的解析过程如下:
      • parse解析MySQL的Bin log,然后将数据放入到sink中
      • sink对数据进行过滤,加工,分发
      • store从sink中读取解析好的数据存储起来
      • 然后自己用设计代码将store中的数据同步写入Redis中就可以了
      • 其中parse/sink是框架封装好的,我们做的是store的数据读取那一步

    • 更多关于Cancl可以百度搜索
    • 下面是运行拓扑图

    • MySQL表的同步,采用责任链模式,每张表对应一个Filter 。例如zvsync中要用到的类设计如下:

    • 下面是具体化的zvsync中要用到的类 ,每当新增或者删除表时,直接进行增删就可以了


    三、附加


    本文上面所介绍的都是从MySQL中同步到缓存中。

    但是在实际开发中可能有人会用下面的方案:

      • 1、客户端有数据来了之后,先将其保存到Redis中,然后再同步到MySQL中

      • 2、这种方案本身也是不安全/不可靠的,因此如果Redis存在短暂的宕机或失效,那么会丢失数据


  • 美团:为什么 MySQL 不推荐使用 join?

    快乐分享,Java干货及时送达👇

    来源:cnblogs.com/liboware/p/12740901.html

    • 一、应用层关联的优势
    • 二、应用层关联的使用场景
    • 三、不推荐使用join的原因
    • 四、不使用join的解决方案
    • 五、join查询的优势

    1.对于mysql,不推荐使用子查询和join是因为本身join的效率就是硬伤,一旦数据量很大效率就很难保证,强烈推荐分别根据索引单表取数据,然后在程序里面做join,merge数据。

    2.子查询就更别用了,效率太差,执行子查询时,MYSQL需要创建临时表,查询完毕后再删除这些临时表,所以,子查询的速度会受到一定的影响,这里多了一个创建和销毁临时表的过程。

    3.如果是JOIN的话,它是走嵌套查询的。小表驱动大表,且通过索引字段进行关联。如果表记录比较少的话,还是OK的。大的话业务逻辑中可以控制处理。

    4.数据库是最底层的,瓶颈往往是数据库。建议数据库只是作为数据store的工具,而不要添加业务上去。

    一、应用层关联的优势

    让缓存的效率更高。许多应用程序可以方便地缓存单表查询对应的结果对象。如果关联中的某个表发生了变化,那么就无法使用查询缓存了,而拆分后,如果某个表很少改变,那么基于该表的查询就可以重复利用查询缓存结果了。

    • 将查询分解后,执行单个查询可以减少锁的竞争。
    • 在应用层做关联,可以更容易对数据库进行拆分,更容易做到高性能和可扩展。
    • 查询本身效率也可能会有所提升。查询id集的时候,使用IN()代替关联查询,可以让MySQL按照ID顺序进行查询,这可能比随机的关联要更高效。
    • 可以减少冗余记录的查询。在应用层做关联查询,意味着对于某条记录应用只需要查询一次,而在数据库中做关联查询,则可能需
    • 要重复地访问一部分数据。从这点看,这样的重构还可能会减少网络和内存的消艳。
    • 更进一步,这样做相当于在应用中实现了哈希关联,而不是使用MySQL的嵌套循环关联。某些场景哈希关联的效率要高很多。

    二、应用层关联的使用场景

    • 当应用能够方便地缓存单个查询的结果的时候
    • 当可以将数据分布到不同的MySQL服务器上的时候
    • 当能够使用IN()的方式代替关联查询的时候
    • 并发场景多,DB查询频繁,需要分库分表

    三、不推荐使用join的原因

    1.DB承担的业务压力大,能减少负担就减少。当表处于百万级别后,join导致性能下降;

    2.分布式的分库分表。这种时候是不建议跨库join的。目前mysql的分布式中间件,跨库join表现不良。

    3.修改表的schema,单表查询的修改比较容易,join写的sql语句要修改,不容易发现,成本比较大,当系统比较大时,不好维护。

    四、不使用join的解决方案

    在业务层,单表查询出数据后,作为条件给下一个单表查询。也就是子查询。会担心子查询出来的结果集太多。mysql对in的数量没有限制,但是mysql限制整条sql语句的大小。

    通过调整参数max_allowed_packet ,可以修改一条sql的最大值。建议在业务上做好处理,限制一次查询出来的结果集是能接受的。

    五、join查询的优势

    关联查询的好处是可以做分页,可以用副表的字段做查询条件,在查询的时候,将副表匹配到的字段作为结果集,用主表去in它。

    但是问题来了,如果匹配到的数据量太大就不行了,也会导致返回的分页记录跟实际的不一样,解决的方法可以交给前端,一次性查询,让前端分批显示就可以了,这种解决方案的前提是数据量不太,因为sql本身长度有限。