欢迎光临
我们一直在努力

史上最全的BAT大厂Mysql面试题在这里!

mumupudding阅读(1)

1、MySQL的复制原理以及流程

基本原理流程,3个线程以及之间的关联;

  • 主:binlog线程——记录下所有改变了数据库数据的语句,放进master上的binlog中;
  • 从:io线程——在使用start slave 之后,负责从master上拉取 binlog 内容,放进 自己的relay log中;
  • 从:sql执行线程——执行relay log中的语句;

2、MySQL中myisam与innodb的区别,至少5点

(1)、问5点不同;

1>.InnoDB支持事物,而MyISAM不支持事物2>.InnoDB支持行级锁,而MyISAM支持表级锁3>.InnoDB支持MVCC, 而MyISAM不支持4>.InnoDB支持外键,而MyISAM不支持5>.InnoDB不支持全文索引,而MyISAM支持。

(2)、innodb引擎的4大特性

插入缓冲(insert buffer),二次写(double write),自适应哈希索引(ahi),预读(read ahead)

(3)、2者selectcount(*)哪个更快,为什么

myisam更快,因为myisam内部维护了一个计数器,可以直接调取。

3、MySQL中varchar与char的区别以及varchar(50)中的50代表的涵义

(1)、varchar与char的区别char是一种固定长度的类型,varchar则是一种可变长度的类型 (2)、varchar(50)中50的涵义最多存放50个字符,varchar(50)和(200)存储hello所占空间一样,但后者在排序时会消耗更多内存,因为order by col采用fixed_length计算col长度(memory引擎也一样) (3)、int(20)中20的涵义是指显示字符的长度但要加参数的,最大为255,比如它是记录行数的id,插入10笔资料,它就显示00000000001 ~~~00000000010,当字符的位数超过11,它也只显示11位,如果你没有加那个让它未满11位就前面加0的参数,它不会在前面加020表示最大显示宽度为20,但仍占4字节存储,存储范围不变; (4)、mysql为什么这么设计对大多数应用没有意义,只是规定一些工具用来显示字符的个数;int(1)和int(20)存储和计算均一样;

4、问了innodb的事务与日志的实现方式

(1)、有多少种日志;

错误日志:记录出错信息,也记录一些警告信息或者正确的信息。查询日志:记录所有对数据库请求的信息,不论这些请求是否得到了正确的执行。慢查询日志:设置一个阈值,将运行时间超过该值的所有SQL语句都记录到慢查询的日志文件中。二进制日志:记录对数据库执行更改的所有操作。中继日志:事务日志:

(2)、事物的4种隔离级别

隔离级别读未提交(RU)读已提交(RC)可重复读(RR)串行

(3)、事务是如何通过日志来实现的,说得越深入越好。

事务日志是通过redo和innodb的存储引擎日志缓冲(Innodb log buffer)来实现的,当开始一个事务的时候,会记录该事务的lsn(log sequence number)号; 当事务执行时,会往InnoDB存储引擎的日志的日志缓存里面插入事务日志;当事务提交时,必须将存储引擎的日志缓冲写入磁盘(通过innodb_flush_log_at_trx_commit来控制),也就是写数据前,需要先写日志。这种方式称为“预写日志方式”

5、MySQL binlog的几种日志录入格式以及区别

Statement:每一条会修改数据的sql都会记录在binlog中。

优点:不需要记录每一行的变化,减少了binlog日志量,节约了IO,提高性能。(相比row能节约多少性能 与日志量,这个取决于应用的SQL情况,正常同一条记录修改或者插入row格式所产生的日志量还小于Statement产生的日志量,但是考虑到如果带条 件的update操作,以及整表删除,alter表等操作,ROW格式会产生大量日志,因此在考虑是否使用ROW格式日志时应该跟据应用的实际情况,其所 产生的日志量会增加多少,以及带来的IO性能问题。)缺点:由于记录的只是执行语句,为了这些语句能在slave上正确运行,因此还必须记录每条语句在执行的时候的 一些相关信息,以保证所有语句能在slave得到和在master端执行时候相同 的结果。另外mysql 的复制,像一些特定函数功能,slave可与master上要保持一致会有很多相关问题(如sleep()函数, last_insert_id(),以及user-defined functions(udf)会出现问题).使用以下函数的语句也无法被复制:

  • LOAD_FILE()
  • UUID()
  • USER()
  • FOUND_ROWS()
  • SYSDATE() (除非启动时启用了 –sysdate-is-now 选项)
  • 同时在INSERT …SELECT 会产生比 RBR 更多的行级锁
  • 2.Row:不记录sql语句上下文相关信息,仅保存哪条记录被修改。
  • 优点: binlog中可以不记录执行的sql语句的上下文相关的信息,仅需要记录那一条记录被修改成什么了。所以rowlevel的日志内容会非常清楚的记录下 每一行数据修改的细节。而且不会出现某些特定情况下的存储过程,或function,以及trigger的调用和触发无法被正确复制的问题
  • 缺点:所有的执行的语句当记录到日志中的时候,都将以每行记录的修改来记录,这样可能会产生大量的日志内容,比 如一条update语句,修改多条记录,则binlog中每一条修改都会有记录,这样造成binlog日志量会很大,特别是当执行alter table之类的语句的时候,由于表结构修改,每条记录都发生改变,那么该表每一条记录都会记录到日志中。
  • 3.Mixedlevel: 是以上两种level的混合使用,一般的语句修改使用statment格式保存binlog,如一些函数,statement无法完成主从复制的操作,则 采用row格式保存binlog,MySQL会根据执行的每一条具体的sql语句来区分对待记录的日志形式,也就是在Statement和Row之间选择 一种.新版本的MySQL中队row level模式也被做了优化,并不是所有的修改都会以row level来记录,像遇到表结构变更的时候就会以statement模式来记录。至于update或者delete等修改数据的语句,还是会记录所有行的 变更。

6、MySQL数据库cpu飙升到500%的话他怎么处理?

1、列出所有进程 show processlist,观察所有进程 ,多秒没有状态变化的(干掉)2、查看超时日志或者错误日志 (做了几年开发,一般会是查询以及大批量的插入会导致cpu与i/o上涨,当然不排除网络状态突然断了,,导致一个请求服务器只接受到一半,比如where子句或分页子句没有发送,,当然的一次被坑经历)

7、sql优化各种方法

(1)、explain出来的各种item的意义;select_type表示查询中每个select子句的类型type表示MySQL在表中找到所需行的方式,又称“访问类型”possible_keys指出MySQL能使用哪个索引在表中找到行,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询使用key显示MySQL在查询中实际使用的索引,若没有使用索引,显示为NULLkey_len表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度ref表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值Extra包含不适合在其他列中显示但十分重要的额外信息(2)、profile的意义以及使用场景;查询到 SQL 会执行多少时间, 并看出 CPU/Memory 使用量, 执行过程中 Systemlock, Table lock 花多少时间等等

8、备份计划,mysqldump以及xtranbackup的实现原理

(1)、备份计划;这里每个公司都不一样,您别说那种1小时1全备什么的就行(2)、备份恢复时间;这里跟机器,尤其是硬盘的速率有关系,以下列举几个仅供参考20G的2分钟(mysqldump)80G的30分钟(mysqldump)111G的30分钟(mysqldump)288G的3小时(xtra)3T的4小时(xtra)逻辑导入时间一般是备份时间的5倍以上(3)、xtrabackup实现原理在InnoDB内部会维护一个redo日志文件,我们也可以叫做事务日志文件。事务日志会存储每一个InnoDB表数据的记录修改。当InnoDB启动时,InnoDB会检查数据文件和事务日志,并执行两个步骤:它应用(前滚)已经提交的事务日志到数据文件,并将修改过但没有提交的数据进行回滚操作。

9、mysqldump中备份出来的sql,如果我想sql文件中,一行只有一个insert….value()的话,怎么办?如果备份需要带上master的复制点信息怎么办?

–skip-extended-insert

[root@helei-zhuanshu ~]# mysqldump -uroot -p helei --skip-extended-insert
Enter password:
 KEY `idx_c1` (`c1`),
 KEY `idx_c2` (`c2`)
) ENGINE=InnoDB AUTO_INCREMENT=51 DEFAULT CHARSET=latin1;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `helei`
--
LOCK TABLES `helei` WRITE;
/*!40000 ALTER TABLE `helei` DISABLE KEYS */;
INSERT INTO `helei` VALUES (1,32,37,38,'2016-10-18 06:19:24','susususususususususususu');
INSERT INTO `helei` VALUES (2,37,46,21,'2016-10-18 06:19:24','susususususu');
INSERT INTO `helei` VALUES (3,21,5,14,'2016-10-18 06:19:24','susu');

10、500台db,在最快时间之内重启

puppet,dsh

11、innodb的读写参数优化

(1)、读取参数global buffer pool以及 local buffer;(2)、写入参数;innodb_flush_log_at_trx_commitinnodb_buffer_pool_size(3)、与IO相关的参数;innodb_write_io_threads = 8innodb_read_io_threads = 8innodb_thread_concurrency = 0(4)、缓存参数以及缓存的适用场景。query cache/query_cache_type并不是所有表都适合使用query cache。造成query cache失效的原因主要是相应的table发生了变更 第一个:读操作多的话看看比例,简单来说,如果是用户清单表,或者说是数据比例比较固定,比如说商品列表,是可以打开的,前提是这些库比较集中,数据库中的实务比较小。 第二个:我们“行骗”的时候,比如说我们竞标的时候压测,把query cache打开,还是能收到qps激增的效果,当然前提示前端的连接池什么的都配置一样。大部分情况下如果写入的居多,访问量并不多,那么就不要打开,例如社交网站的,10%的人产生内容,其余的90%都在消费,打开还是效果很好的,但是你如果是qq消息,或者聊天,那就很要命。 第三个:小网站或者没有高并发的无所谓,高并发下,会看到 很多 qcache 锁 等待,所以一般高并发下,不建议打开query cache

12、你是如何监控你们的数据库的?你们的慢日志都是怎么查询的?

监控的工具有很多,例如zabbix,lepus,我这里用的是lepus

13、你是否做过主从一致性校验,如果有,怎么做的,如果没有,你打算怎么做?

主从一致性校验有多种工具 例如checksum、mysqldiff、pt-table-checksum等

14、你们数据库是否支持emoji表情,如果不支持,如何操作?

如果是utf8字符集的话,需要升级至utf8_mb4方可支持

15、你是如何维护数据库的数据字典的?

这个大家维护的方法都不同,我一般是直接在生产库进行注释,利用工具导出成excel方便流通。

16、表中有大字段X(例如:text类型),且字段X不会经常更新,以读为为主,请问

拆带来的问题:连接消耗 + 存储拆分空间;不拆可能带来的问题:查询性能;1、如果能容忍拆分带来的空间问题,拆的话最好和经常要查询的表的主键在物理结构上放置在一起(分区) 顺序IO,减少连接消耗,最后这是一个文本列再加上一个全文索引来尽量抵消连接消耗2、如果能容忍不拆分带来的查询性能损失的话:上面的方案在某个极致条件下肯定会出现问题,那么不拆就是最好的选择

17、MySQL中InnoDB引擎的行锁是通过加在什么上完成(或称实现)的?为什么是这样子的?

InnoDB是基于索引来完成行锁例: select * from tab_with_index where id = 1 for update;for update 可以根据条件来完成行锁锁定,并且 id 是有索引键的列,如果 id 不是索引键那么InnoDB将完成表锁,,并发将无从谈起

18、开放性问题:据说是腾讯的

一个6亿的表a,一个3亿的表b,通过外间tid关联,你如何最快的查询出满足条件的第50000到第50200中的这200条数据记录。

1、如果A表TID是自增长,并且是连续的,B表的ID为索引

select * from a,b where a.tid = b.id and a.tid>500000 limit 200;

2、如果A表的TID不是连续的,那么就需要使用覆盖索引.TID要么是主键,要么是辅助索引,B表ID也需要有索引。

select * from b , (select tid from a limit 50000,200) a where b.id = a .tid;

19、什么是存储过程?有哪些优缺点?

存储过程是一些预编译的SQL语句。

1、更加直白的理解:存储过程可以说是一个记录集,它是由一些T-SQL语句组成的代码块,这些T-SQL语句代码像一个方法一样实现一些功能(对单表或多表的增删改查),然后再给这个代码块取一个名字,在用到这个功能的时候调用他就行了。2、存储过程是一个预编译的代码块,执行效率比较高,一个存储过程替代大量T_SQL语句 ,可以降低网络通信量,提高通信速率,可以一定程度上确保数据安全

20、索引是什么?有什么作用以及优缺点?

1、索引是对数据库表中一或多个列的值进行排序的结构,是帮助MySQL高效获取数据的数据结构2、索引就是加快检索表中数据的方法。数据库的索引类似于书籍的索引。在书籍中,索引允许用户不必翻阅完整个书就能迅速地找到所需要的信息。在数据库中,索引也允许数据库程序迅速地找到表中的数据,而不必扫描整个数据库。

MySQL数据库几个基本的索引类型:普通索引、唯一索引、主键索引、全文索引

1、索引加快数据库的检索速度2、索引降低了插入、删除、修改等维护任务的速度3、唯一索引可以确保每一行数据的唯一性4、通过使用索引,可以在查询的过程中使用优化隐藏器,提高系统的性能5、索引需要占物理和数据空间

21、什么是事务?

事务(Transaction)是并发控制的基本单位。所谓的事务,它是一个操作序列,这些操作要么都执行,要么都不执行,它是一个不可分割的工作单位。事务是数据库维护数据一致性的单位,在每个事务结束时,都能保持数据一致性。

24、数据库的乐观锁和悲观锁是什么?

数据库管理系统(DBMS)中的并发控制的任务是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性。乐观并发控制(乐观锁)和悲观并发控制(悲观锁)是并发控制主要采用的技术手段。

悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。

22、使用索引查询一定能提高查询的性能吗?为什么

通常,通过索引查询数据比全表扫描要快.但是我们也必须注意到它的代价.

1、索引需要空间来存储,也需要定期维护, 每当有记录在表中增减或索引列被修改时,索引本身也会被修改. 这意味着每条记录的INSERT,DELETE,UPDATE将为此多付出4,5 次的磁盘I/O. 因为索引需要额外的存储空间和处理,那些不必要的索引反而会使查询反应时间变慢.使用索引查询不一定能提高查询性能,索引范围查询(INDEX RANGE SCAN)适用于两种情况:2、基于一个范围的检索,一般查询返回结果集小于表中记录数的30%3、基于非唯一性索引的检索

23、简单说一说drop、delete与truncate的区

SQL中的drop、delete、truncate都表示删除,但是三者有一些差别

1、delete和truncate只删除表的数据不删除表的结构2、速度,一般来说: drop> truncate >delete3、delete语句是dml,这个操作会放到rollback segement中,事务提交之后才生效;4、如果有相应的trigger,执行的时候将被触发. truncate,drop是ddl, 操作立即生效,原数据不放到rollback segment中,不能回滚. 操作不触发trigger.

24、drop、delete与truncate分别在什么场景之下使用?

1、不再需要一张表的时候,用drop2、想删除部分数据行时候,用delete,并且带上where子句3、保留表而删除所有数据的时候用truncate

25、超键、候选键、主键、外键分别是什么?

1、超键:在关系中能唯一标识元组的属性集称为关系模式的超键。一个属性可以为作为一个超键,多个属性组合在一起也可以作为一个超键。超键包含候选键和主键。2、候选键:是最小超键,即没有冗余元素的超键。3、主键:数据库表中对储存数据对象予以唯一和完整标识的数据列或属性的组合。一个数据列只能有一个主键,且主键的取值不能缺失,即不能为空值(Null)。4、外键:在一个表中存在的另一个表的主键称此表的外键。

26、什么是视图?以及视图的使用场景有哪些?

1、视图是一种虚拟的表,具有和物理表相同的功能。可以对视图进行增,改,查,操作,试图通常是有一个表或者多个表的行或列的子集。对视图的修改不影响基本表。它使得我们获取数据更容易,相比多表查询。2、只暴露部分字段给访问者,所以就建一个虚表,就是视图。3、查询的数据来源于不同的表,而查询者希望以统一的方式查询,这样也可以建立一个视图,把多个表查询结果联合起来,查询者只需要直接从视图中获取数据,不必考虑数据来源于不同表所带来的差异

27、说一说三个范式。

第一范式(1NF):数据库表中的字段都是单一属性的,不可再分。这个单一属性由基本类型构成,包括整型、实数、字符型、逻辑型、日期型等。 第二范式(2NF):数据库表中不存在非关键字段对任一候选关键字段的部分函数依赖(部分函数依赖指的是存在组合关键字中的某些字段决定非关键字段的情况),也即所有非关键字段都完全依赖于任意一组候选关键字。 第三范式(3NF):在第二范式的基础上,数据表中如果不存在非关键字段对任一候选关键字段的传递函数依赖则符合第三范式。所谓传递函数依赖,指的是如 果存在”A → B → C”的决定关系,则C传递函数依赖于A。因此,满足第三范式的数据库表应该不存在如下依赖关系: 关键字段 → 非关键字段 x → 非关键字段y

欢迎工作一到五年的Java工程师朋友们加入Java高级架构:705127209

群内提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,

MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)

合理利用自己每一分每一秒的时间来学习提升自己,不要再用”没有时间“来掩饰自己思想上的懒惰!趁年轻,使劲拼,给未来的自己一个交代!

【Nginx】Nginx多级代理,获取客户端真实请求IP以及每级代理IP

mumupudding阅读(15)

Nginx多级代理,获取客户端真实请求IP以及每级代理IP

如图所示,每一级nginx里的location配置里需要加上对应的配置,最后一级nginx是直接到应用,测试时为了方便,直接用echo模块去测试,打印IP地址。

原理分析:

只有客户端直接请求到的那个nginx能够拿到客户端的真实IP,所以第一级nginx配置了

proxy_set_header X-Real-IP $remote_addr;

这个配置就会将客户端IP放到http的header里,这样到最后的应用里可以通过request.getHeader去拿到客户端真实IP了

public String getRemoteIp() {
        String ip = request.getHeader(“X-Forwarded-For”);
        if (isEffective(ip) && ip.indexOf(“,”) > -1) {
            String[] array = ip.split(“,”);
            for (String str : array) {
                if (isEffective(str)) {
                    ip = str;
                    break;
                }
            }
        }
        if (!isEffective(ip)) {
            ip = request.getHeader(“Proxy-Client-IP”);
        }
        if (!isEffective(ip)) {
            ip = request.getHeader(“WL-Proxy-Client-IP”);
        }
        if (!isEffective(ip)) {
            ip = request.getHeader(“HTTP_CLIENT_IP”);
        }
        if (!isEffective(ip)) {
            ip = request.getHeader(“HTTP_X_FORWARDED_FOR”);
        }
        if (!isEffective(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
 
    private boolean isEffective(String remoteAddr) {
        if ((null != remoteAddr) && (!””.equals(remoteAddr.trim()))
                && (!”unknown”.equalsIgnoreCase(remoteAddr.trim()))) {
            return true;
        }
        return false;
    }
如何拿到用户从请求源头到应用所经过的各个代理IP呢?
 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
通过如上配置,将每级代理IP与$remote_addr用逗号分隔开

所以从上述情况来看,整体的流程很清晰了:

客户端请求(172.23.11.251) –>172.23.26.130–>172.23.26.132–>172.23.26.133–>172.23.30.100(具体应用)

但是我们发现上面IP貌似打印少了一个133的地址,我们在100上修改下echo测试

这里打印出了133的地址

nginx的安装包里有一个http_realip_module模块,在编译的时候可以编译此模块

./configure –prefix=/home/nginx/install –with-pcre=../pcre-8.36 –with-zlib=../zlib-1.2.8 –with-openssl=../openssl-1.0.2 –add-module=/root/nginx/echo-nginx-module-0.60 –with-http_realip_module && make && make install

新的配置如下:

set_real_ip_from   172.23.0.0/16;     IP段,指定接收来自哪个前端发送的 IP head 可以是单个IP或者IP段
set_real_ip_from   192.168.1.1;     单个IP
real_ip_header     X-Real-IP;         IP head  的对应参数,默认即可,含义是客户端真实IP从哪个头取?

real_ip_recursive on;  是否递归解析real_ip_header
 

深入理解 React 的 Virtual DOM

mumupudding阅读(3)

React在前端界一直很流行,而且学起来也不是很难,只需要学会JSX、理解StateProps,然后就可以愉快的玩耍了,但想要成为React的专家你还需要对React有一些更深入的理解,希望本文对你有用。

这是Choerodon的一个前端页面

在复杂的前端项目中一个页面可能包含上百个状态,对React框架理解得更精细一些对前端优化很重要。曾经这个页面点击一条记录展示详情会卡顿数秒,而这仅仅是前端渲染造成的。

为了能够解决这些问题,开发者需要了解React组件从定义到在页面上呈现(然后更新)的整个过程。

React在编写组件时使用混合HTMLJavaScript的一种语法(称为JSX)。 但是,浏览器对JSX及其语法一无所知,浏览器只能理解纯JavaScript,因此必须将JSX转换为HTML。 这是一个div的JSX代码,它有一个类和一些内容:

<div className='cn'>  文本</div>

在React中将这段jsx变成普通的js之后它就是一个带有许多参数的函数调用:

React.createElement(  'div',  { className: 'cn' },  '文本');

它的第一个参数是一个字符串,对应html中的标签名,第二个参数是它的所有属性所构成的对象,当然,它也有可能是个空对象,剩下的参数都是这个元素下的子元素,这里的文本也会被当作一个子元素,所以第三个参数是 “文本”

到这里你应该就能想象这个元素下有更多children的时候会发生什么。

<div className='cn'>  文本1  <br />  文本2</div>
React.createElement(  'div',  { className: 'cn' },  '文本1',              // 1st child  React.createElement('br'), // 2nd child  '文本1'               // 3rd child)

目前的函数有五个参数:元素的类型,全部属性的对象和三个子元素。 由于一个child也是React已知的HTML标签,因此它也将被解释成函数调用。

到目前为止,本文已经介绍了两种类型的child参数,一种是string纯文本,一种是调用其他的React.createElement函数。其实,其他值也可以作为参数,比如:

  • 基本类型 false,null,undefined和 true
  • 数组
  • React组件

使用数组是因为可以将子组件分组并作为一个参数传递:

React.createElement(  'div',  { className: 'cn' },  ['Content 1!', React.createElement('br'), 'Content 2!'])

当然,React的强大功能不是来自HTML规范中描述的标签,而是来自用户创建的组件,例如:

function Table({ rows }) {  return (    <table>      {rows.map(row => (        <tr key={row.id}>          <td>{row.title}</td>        </tr>      ))}    </table>  );}

组件允许开发者将模板分解为可重用的块。在上面的“纯函数”组件的示例中,组件接受一个包含表行数据的对象数组,并返回React.createElement对<table>元素及其行作为子元素的单个调用 。

每当开发者将组件放入JSX布局中时它看上去是这样的:

<Table rows={rows} />

但从浏览器角度,它看到的是这样的:

React.createElement(Table, { rows: rows });

请注意,这次的第一个参数不是以string描述的HTML元素,而是组件的引用(即函数名)。第二个参数是传入该组件的props对象。

将组件放在页面上

现在,浏览器已经将所有JSX组件转换为纯JavaScript,现在浏览器获得了一堆函数调用,其参数是其他函数调用,还有其他函数调用……如何将它们转换为构成网页的DOM元素?

为此,开发者需要使用ReactDOM库及其render方法:

function Table({ rows }) { /* ... */ } // 组件定义// 渲染一个组件ReactDOM.render(  React.createElement(Table, { rows: rows }), // "创建" 一个 component  document.getElementById('#root') // 将它放入DOM中);

ReactDOM.render被调用时,React.createElement最终也会被调用,它返回以下对象:

// 这个对象里还有很多其他的字段,但现在对开发者来说重要的是这些。{  type: Table,  props: {    rows: rows  },  // ...}

这些对象构成了React意义上的Virtual DOM

它们将在所有进一步渲染中相互比较,并最终转换为真正的DOM(与Virtual DOM对比)。

这是另一个例子:这次有一个div具有class属性和几个子节点:

React.createElement(  'div',  { className: 'cn' },  'Content 1!',  'Content 2!',);

变成:

{  type: 'div',  props: {    className: 'cn',    children: [      'Content 1!',      'Content 2!'    ]  }}

所有的传入的展开函数,也就是React.createElement除了第一第二个参数剩下的参数都会在props对象中的children属性中,不管传入的是什么函数,他们最终都会作为children传入props中。

而且,开发者可以直接在JSX代码中添加children属性,将子项直接放在children中,结果仍然是相同的:

<div className='cn' children={['Content 1!', 'Content 2!']} />

在Virtual DOM对象被建立出来之后ReactDOM.render会尝试按以下规则把它翻译成浏览器能够看得懂的DOM节点:

  • 如果Virtual DOM对象中的type属性是一个string类型的tag名称,创建一个tag,包含props里的全部属性。
  • 如果Virtual DOM对象中的type属性是一个函数或者class,调用它,它返回的可能还是一个Virtual DOM然后将结果继续递归调用此过程。
  • 如果props中有children属性,对children中的每个元素进行以上过程,并将返回的结果放到父DOM节点中。

最后,浏览器获得了以下HTML(对于上述table的例子):

<table>  <tr>    <td>Title</td>  </tr>  ...</table>

重建DOM

接下浏览器要“重建”一个DOM节点,如果浏览器要更新一个页面,显然,开发者并不希望替换页面中的全部元素,这就是React真正的魔法了。如何才能实现它?先从最简单的方法开始,重新调用这个节点的ReactDOM.render方法。

// 第二次调用ReactDOM.render(  React.createElement(Table, { rows: rows }),  document.getElementById('#root'));

这一次,上面的代码执行逻辑将与看到的代码不同。React不是从头开始创建所有DOM节点并将它们放在页面上,React将使用“diff”算法,以确定节点树的哪些部分必须更新,哪些部分可以保持不变。

那么它是怎样工作的?只有少数几个简单的情况,理解它们将对React程序的优化有很大帮助。请记住,接下来看到的对象是用作表示React Virtual DOM中节点的对象。

▌Case 1:type是一个字符串,type在调用之间保持不变,props也没有改变。

// before update{ type: 'div', props: { className: 'cn' } }// after update{ type: 'div', props: { className: 'cn' } }

这是最简单的情况:DOM保持不变。

▌Case 2:type仍然是相同的字符串,props是不同的。

// before update:{ type: 'div', props: { className: 'cn' } }// after update:{ type: 'div', props: { className: 'cnn' } }

由于type仍然代表一个HTML元素,React知道如何通过标准的DOM API调用更改其属性,而无需从DOM树中删除节点。

▌Case 3:type已更改为不同的组件String或从String组件更改为组件。

// before update:{ type: 'div', props: { className: 'cn' } }// after update:{ type: 'span', props: { className: 'cn' } }

由于React现在看到类型不同,它甚至不会尝试更新DOM节点:旧元素将与其所有子节点一起被删除(unmount)。因此,在DOM树上替换完全不同的元素的代价会非常之高。幸运的是,这在实际情况中很少发生。

重要的是要记住React使用===(三等)来比较type值,因此它们必须是同一个类或相同函数的相同实例。

下一个场景更有趣,因为这是开发者最常使用React的方式。

▌Case 4:type是一个组件。

// before update:{ type: Table, props: { rows: rows } }// after update:{ type: Table, props: { rows: rows } }

你可能会说,“这好像没有任何变化”,但这是不对的。

如果type是对函数或类的引用(即常规React组件),并且启动了树diff比较过程,那么React将始终尝试查看组件内部的所有child以确保render的返回值没有更改。即在树下比较每个组件 – 是的,复杂的渲染也可能变得昂贵!

组件中的children

除了上面描述的四种常见场景之外,当元素有多个子元素时,开发者还需要考虑React的行为。假设有这样一个元素:

// ...props: {  children: [      { type: 'div' },      { type: 'span' },      { type: 'br' }  ]},// ...

开发者开发者想将它重新渲染成这样(spandiv交换了位置):

// ...props: {  children: [    { type: 'span' },    { type: 'div' },    { type: 'br' }  ]},// ...

那么会发生什么?

当React看到里面的任何数组类型的props.children,它会开始将它中的元素与之前看到的数组中的元素按顺序进行比较:index 0将与index 0,index 1与index 1进行比较,对于每对子元素,React将应用上述规则集进行比较更新。在以上的例子中,它看到div变成一个span这是一个情景3中的情况。但这有一个问题:假设开发者想要从1000行表中删除第一行。React必须“更新”剩余的999个孩子,因为如果与先前的逐个索引表示相比,他们的内容现在将不相等。

幸运的是,React有一种内置的方法来解决这个问题。如果元素具有key属性,则元素将通过key而不是索引进行比较。只要key是唯一的,React就会移动元素而不将它们从DOM树中移除,然后将它们放回(React中称为挂载/卸载的过程)。

// ...props: {  children: [ // 现在react就是根据key,而不是索引来比较了    { type: 'div', key: 'div' },    { type: 'span', key: 'span' },    { type: 'br', key: 'bt' }  ]},// ...

当状态改变时

到目前为止,本文只触及了props,React哲学的一部分,但忽略了state。这是一个简单的“有状态”组件:

class App extends Component {  state = { counter: 0 }  increment = () => this.setState({    counter: this.state.counter + 1,  })  render = () => (<button onClick={this.increment}>    {'Counter: ' + this.state.counter}  </button>)}

现在,上述例子中的state对象有一个counter属性。单击按钮会增加其值并更改按钮文本。但是当用户点击时,DOM会发生什么?它的哪一部分将被重新计算和更新?

调用this.setState也会导致重新渲染,但不会导致整个页面重渲染,而只会导致组件本身及其子项。父母和兄弟姐妹都可以幸免于难。

修复问题

本文准备了一个DEMO,这是修复问题前的样子。你可以在这里查看其源代码。不过在此之前,你还需要安装React Developer Tools

打开demo要看的第一件事是哪些元素以及何时导致Virtual DOM更新。导航到浏览器的Dev Tools中的React面板,点击设置然后选择“Highlight Updates”复选框:

现在尝试在表中添加一行。如你所见,页面上的每个元素周围都会出现边框。这意味着每次添加行时,React都会计算并比较整个Virtual DOM树。现在尝试按一行内的计数器按钮。你将看到Virtual DOM如何更新 (state仅相关元素及其子元素更新)。

React DevTools暗示了问题可能出现的地方,但没有告诉开发者任何细节:特别是有问题的更新是指元素“diff”之后有不同,还是组件被unmount/mount了。要了解更多信息,开发者需要使用React的内置分析器(请注意,它不能在生产模式下工作)。

转到Chrome DevTools中的“Performance”标签。点击record按钮,然后点击表格。添加一些行,更改一些计数器,然后点击“Stop”按钮。稍等一会儿之后开发者会看到:

在结果输出中,开发者需要关注“Timing”。缩放时间轴,直到看到“React Tree Reconciliation”组及其子项。这些都是组件的名称,旁边有[update]或[mount]。可以看到有一个TableRow被mount了,其他所有的TableRow都在update,这并不是开发者想要的。

大多数性能问题都由[update]或[mount]引起

一个组件(以及组件下的所有东西)由于某种原因在每次更新时重新挂载,开发者不想让它发生(重新挂载很慢),或者在大型分支上执行代价过大的重绘,即使组件似乎没有发生任何改变。

修复mount/unmount

现在,当开发者了解React如何决定更新Virtual DOM并知道幕后发生的事情时,终于准备好解决问题了!修复性能问题首先要解决 mount/unmount。

如果开发者将任何元素/组件的多个子元素在内部表示为数组,那么程序可以获得非常明显的速度提升。

考虑一下:

<div>  <Message />  <Table />  <Footer /></div>

在虚拟DOM中,将表示为:

// ...props: {  children: [    { type: Message },    { type: Table },    { type: Footer }  ]}// ...

一个简单的Message组件(是一个div带有一些文本,像是猪齿鱼的顶部通知)和一个很长的Table,比方说1000多行。它们都是div元素的child,因此它们被放置在父节点的props.children之下,并且它们没有key。React甚至不会通过控制台警告来提醒开发者分配key,因为子节点React.createElement作为参数列表而不是数组传递给父节点。

现在,用户已经关闭了顶部通知,所以Message从树中删除。TableFooter是剩下的child。

// ...props: {  children: [    { type: Table },    { type: Footer }  ]}// ...

React如何看待它?它将它视为一系列改变了type的child:children[0]的type本来是Message,但现在他是Table。因为它们都是对函数(和不同函数)的引用,它会卸载整个Table并再次安装它,渲染它的所有子代:1000多行!

因此,你可以添加唯一键(但在这种特殊情况下使用key不是最佳选择)或者采用更智能的trick:使用 && 的布尔短路运算,这是JavaScript和许多其他现代语言的一个特性。像这样:

<div>  {isShowMessage && <Message />}  <Table />  <Footer /></div>

即使Message被关闭了(不再显示),props.children父母div仍将拥有三个元素,children[0]具有一个值false(布尔类型)。还记得true/false, null甚至undefined都是Virtual DOM对象type属性的允许值吗?浏览器最终得到类似这样的东西:

// ...props: {  children: [    false, //  isShowMessage && <Message /> 短路成了false    { type: Table },    { type: Footer }  ]}// ...

所以,不管Message是否被显示,索引都不会改变,Table仍然会和Table比较,但仅仅比较Virtual DOM通常比删除DOM节点并从中创建它们要快得多。

现在来看看更高级的东西。开发者喜欢HOC。高阶组件是一个函数,它将一个组件作为一个参数,添加一些行为,并返回一个不同的组件(函数):

function withName(SomeComponent) {  return function(props) {    return <SomeComponent {...props} name={name} />;  }}

开发者在父render方法中创建了一个HOC 。当React需要重新渲染树时,React 的Virtual DOM将如下所示:

// On first render:{  type: ComponentWithName,  props: {},}// On second render:{  type: ComponentWithName, // Same name, but different instance  props: {},}

现在,React只会在ComponentWithName上运行一个diff算法,但是这次同名引用了一个不同的实例,三等于比较失败,必须进行完全重新挂载。注意它也会导致状态丢失,幸运的是,它很容易修复:只要返回的实例都是同一个就好了:

// 单例const ComponentWithName = withName(Component);class App extends React.Component() {  render() {    return <ComponentWithName />;  }}

修复update

现在浏览器已经确保不会重新装载东西了,除非必要。但是,对位于DOM树根目录附近的组件所做的任何更改都将导致其所有子项的进行对比重绘。结构复杂,价格昂贵且经常可以避免。

如果有办法告诉React不要查看某个分支,那将是很好的,因为它没有任何变化。

这种方式存在,它涉及一个叫shouldComponentUpdate的组件生命周期函数。React会在每次调用组件之前调用此方法,并接收propsstate的新值。然后开发者可以自由地比较新值和旧值之间的区别,并决定是否应该更新组件(返回truefalse)。如果函数返回false,React将不会重新渲染有问题的组件,也不会查看其子组件。

通常比较两组propsstate一个简单的浅层比较就足够了:如果顶层属性的值相同,浏览器就不必更新了。浅比较不是JavaScript的一个特性,但开发者很多方法来自己实现它,为了不重复造轮子,也可以使用别人写好的方法

在引入浅层比较的npm包后,开发者可以编写如下代码:

class TableRow extends React.Component {  shouldComponentUpdate(nextProps, nextState) {    const { props, state } = this;    return !shallowequal(props, nextProps)           && !shallowequal(state, nextState);  }  render() { /* ... */ }}

但是你甚至不必自己编写代码,因为React在一个名为React.PureComponent的类中内置了这个功能,它类似于React.Component,只是shouldComponentUpdate已经为你实现了浅层props/state比较。

或许你会有这样的想法,能替换ComponentPureComponent就去替换。但开发者如果错误地使用PureComponent同样会有重新渲染的问题存在,需要考虑下面三种情况:

<Table    // map每次都会返回一个新的数组实例,所以每次比较都是不同的    rows={rows.map(/* ... */)}    // 每一次传入的对象都是新的对象,引用是不同的。    style={ { color: 'red' } }    // 箭头函数也一样,每次都是不同的引用。    onUpdate={() => { /* ... */ }}/>

上面的代码片段演示了三种最常见的反模式,请尽量避免它们!

正确地使用PureComponent,你可以在这里看到所有的TableRow都被“纯化”后渲染的效果。

但是,如果你迫不及待想要全部使用纯函数组件,这样是不对的。比较两组propsstate不是免费的,对于大多数基本组件来说甚至都不值得:运行shallowCompare比diff算法需要更多时间。

可以使用此经验法则:纯组件适用于复杂的表单和表格,但它们通常会使按钮或图标等简单元素变慢。

现在,你已经熟悉了React的渲染模式,接下来就开始前端优化之旅吧。

关于Choerodon猪齿鱼

Choerodon猪齿鱼是一个开源企业服务平台,基于Kubernetes的容器编排和管理能力,整合DevOps工具链、微服务和移动应用框架,来帮助企业实现敏捷化的应用交付和自动化的运营管理的开源平台,同时提供IoT、支付、数据、智能洞察、企业应用市场等业务组件,致力帮助企业聚焦于业务,加速数字化转型。

大家也可以通过以下社区途径了解猪齿鱼的最新动态、产品特性,以及参与社区贡献:

现代IM系统中的消息系统架构 – 架构篇

mumupudding阅读(2)

前言

IM全称是『Instant Messaging』,中文名是即时通讯。在这个高度信息化的移动互联网时代,生活中IM类产品已经成为必备品,比较有名的如钉钉、微信、QQ等以IM为核心功能的产品。当然目前微信已经成长为一个生态型产品,但其核心功能还是IM。还有一些非以IM系统为核心的应用,最典型的如一些在线游戏、社交应用,IM也是其重要的功能模块。可以说,IM系统已经是任何一个带有社交属性的应用需要具备的基础功能,网络上对于这类系统的设计与实现的讨论也越来越多。

IM系统在互联网初期即存在,其基础技术架构在这十几年的发展中更新迭代多次,从早期的CS、P2P架构,到现在后台已经演变为一个复杂的分布式系统,涉及移动端、网络通信、协议、安全、存储和搜索等技术的方方面面。IM系统中最核心的部分是消息系统,消息系统中最核心的功能是消息的同步、存储和检索:

  • 消息的同步:将消息完整的、快速的从发送方传递到接收方,就是消息的同步。消息同步系统最重要的衡量指标就是消息传递的实时性、完整性以及能支撑的消息规模。从功能上来说,一般至少要支持在线和离线推送,高级的IM系统还支持『多端同步』。
  • 消息的存储:消息存储即消息的持久化保存,传统消息系统通常只能支持消息在接收端的本地存储,数据基本不具备可靠性。现代消息系统能支持消息在服务端的在线存储,功能上对应的就是『消息漫游』,消息漫游的好处是可以实现账号在任意端登陆查看所有历史消息。
  • 消息的检索:消息一般是文本,所以支持全文检索也是必备的能力之一。传统消息系统通常来说也是只能支持消息的本地检索,基于本地存储的消息数据来构建。而现在消息系统在能支持消息的在线存储后,也具备了消息的『在线检索』能力。

本篇文章内容主要涉及IM系统中的消息系统架构,会介绍一种基于阿里云表格存储Tablestore的Timeline模型构建的消息系统。基于Tablestore Timeline构建的现代消息系统,能够同时支持消息系统的众多高级特性,包括『多端同步』、『消息漫游』和『在线检索』。在性能和规模上,能够做到全量消息云端存储和索引,百万TPS写入以及毫秒级延迟的消息同步和检索能力。

之后我们会继续发表两篇文章,来更详细介绍Tablestore Timeline模型概念及使用:

  • 模型篇:详细介绍Tablestore Timeline模型的基本概念和基础数据结构,并结合IM系统进行基本的建模。
  • 实现篇:会基于Tablestore Timeline实现一个具备『多端同步』、『消息漫游』和『在线检索』这些高级功能的简易IM系统,并共享我们的源代码。

传统架构 vs 现代架构

传统架构下,消息是先同步后存储。对于在线的用户,消息会直接实时同步到在线的接收方,消息同步成功后,并不会在服务端持久化。而对于离线的用户或者消息无法实时同步成功时,消息会持久化到离线库,当接收方重新连接后,会从离线库拉取所有未读消息。当离线库中的消息成功同步到接收方后,消息会从离线库中删除。传统的消息系统,服务端的主要工作是维护发送方和接收方的连接状态,并提供在线消息同步和离线消息缓存的能力,保证消息一定能够从发送方传递到接收方。服务端不会对消息进行持久化,所以也无法支持消息漫游。消息的持久化存储及索引同样只能在接收端本地实现,数据可靠性极低。

现代架构下,消息是先存储后同步。先存储后同步的好处是,如果接收方确认接收到了消息,那这条消息一定是已经在云端保存了。并且消息会有两个库来保存,一个是消息存储库,用于全量保存所有会话的消息,主要用于支持消息漫游。另一个是消息同步库,主要用于接收方的多端同步。消息从发送方发出后,经过服务端转发,服务端会先将消息保存到消息存储库,后保存到消息同步库。完成消息的持久化保存后,对于在线的接收方,会直接选择在线推送。但在线推送并不是一个必须路径,只是一个更优的消息传递路径。对于在线推送失败或者离线的接收方,会有另外一个统一的消息同步方式。接收方会主动的向服务端拉取所有未同步消息,但接收方何时来同步以及会在哪些端来同步消息对服务端来说是未知的,所以要求服务端必须保存所有需要同步到接收方的消息,这是消息同步库的主要作用。对于新的同步设备,会有消息漫游的需求,这是消息存储库的主要作用,在消息存储库中,可以拉取任意会话的全量历史消息。消息检索的实现依赖于对消息存储库内消息的索引,通常是一个近实时(NRT,near real time)的索引构建过程,这个索引同样是在线的。

以上就是传统架构和现代架构的一个简单的对比,现代架构上整个消息的同步、存储和索引流程,并没有变复杂太多。现代架构的实现本质上是把传统架构内本地存储和索引都搬到云上,最大挑战是需要集中管理全量消息的存储和索引,带来的好处是能实现多端同步消息漫游以及在线检索。可以看到现代架构中最核心的就是两个消息库『消息同步库』和『消息存储库』,以及对『消息存储库』的『消息索引』的实现,接下来我们逐步拆解这几个核心的设计和实现。

基础模型

在深入讲解消息系统的设计和实现之前,需要对消息系统内的几个基本概念和基础模型有一个理解。网上分析的很多的不同类型的消息系统实现,实现差异上主要在消息同步和存储的方案上,在消息的数据模型上其实有很大的共性。围绕数据同步模型的讨论主要在『读扩散』、『写扩散』和『混合模式』这三种方案,目前还没有更多的选择。而对于数据模型的抽象,还没有一个标准的定义。

本章节会介绍下表格存储Tablestore提出的Timeline模型,这是一个对消息系统内消息模型的一个抽象,能简化和更好的让开发者理解消息系统内的消息同步和存储模型,基于此模型我们会再深入探讨消息的同步和存储的选择和实现。

Timeline模型

Timeline是一个对消息抽象的逻辑模型,该模型会帮助我们简化对消息同步和存储模型的理解,而消息同步库和存储库的设计和实现也是围绕Timeline的特性和需求来展开。

如图是Timeline模型的一个抽象表述,Timeline可以简单理解为是一个消息队列,但这个消息队列有如下特性:

  • 每条消息对应一个顺序ID:每个消息拥有一个唯一的顺序ID(SequenceId),队列消息按SequenceId排序。
  • 新消息写入能自动分配递增的顺序ID,保证永远插入队尾:Timeline中是根据同步位点也就是顺序ID来同步消息,所以需要保证新写入的消息数据的顺序ID绝对不能比已同步的消息的顺序ID还小,否则会导致数据漏同步,所以需要支持对新写入的数据自动分配比当前已存储的所有消息的顺序ID更大的顺序ID。
  • 新消息写入也能自定义顺序ID,满足自定义排序需求:上面提到的自动分配顺序ID,主要是为了满足消息同步的需求,消息同步要求消息是根据『已同步』或是『已写入』的顺序来排序。而消息的存储,通常要求消息能根据会话顺序来排序,会话顺序通常由端的会话来决定,而不是服务端的同步顺序来定,这是两种顺序要求。
  • 支持根据顺序ID的随机定位:可根据SequenceId随机定位到Timeline中的某个位置,从这个位置开始正序或逆序的读取消息,也可支持读取指定顺序ID的某条消息。
  • 支持对消息的自定义索引:消息体内数据根据业务不同会包含不同的字段,Timeline需要支持对不同字段的自定义索引,来支持对消息内容的全文索引,或者是任意字段的灵活条件组合查询。

消息同步可以基于Timeline很简单的实现,图中的例子中,消息发送方是A,消息接收方是B,同时B存在多个接收端,分别是B1、B2和B3。A向B发送消息,消息需要同步到B的多个端,待同步的消息通过一个Timeline来进行交换。A向B发送的所有消息,都会保存在这个Timeline中,B的每个接收端都是独立的从这个Timeline中拉取消息。每个接收端同步完毕后,都会在本地记录下最新同步到的消息的SequenceId,即最新的一个位点,作为下次消息同步的起始位点。服务端不会保存各个端的同步状态,各个端均可以在任意时间从任意点开始拉取消息。

消息存储也是基于Timeline实现,和消息同步唯一的区别是,消息存储要求服务端能够对Timeline内的所有数据进行持久化,并且消息采用会话顺序来保存,需要自定义顺序ID。

消息检索基于Timeline提供的消息索引来实现,能支持比较灵活的多字段索引,根据业务的不同可有自由度较高的定制。

消息存储模型

如图是基于Timeline的消息存储模型,消息存储要求每个会话都对应一个独立的Timeline。如图例子所示,A与B/C/D/E/F均发生了会话,每个会话对应一个独立的Timeline,每个Timeline内存有这个会话中的所有消息,消息根据会话顺序排序,服务端会对每个Timeline进行持久化存储,也就拥有了消息漫游的能力。

消息同步模型

消息同步模型会比消息存储模型稍复杂一些,消息的同步一般有读扩散(也叫拉模式)和写扩散(也叫推模式)两种不同的方式,分别对应不同的Timeline物理模型。

如图是读扩散和写扩散两种不同同步模式下对应的不同的Timeline模型,按图中的示例,A作为消息接收者,其与B/C/D/E/F发生了会话,每个会话中的新的消息都需要同步到A的某个端,看下读扩散和写扩散两种模式下消息如何做同步。

  • 读扩散:消息存储模型中,每个会话的Timeline中保存了这个会话的全量消息。读扩散的消息同步模式下,每个会话中产生的新的消息,只需要写一次到其用于存储的Timeline中,接收端从这个Timeline中拉取新的消息。优点是消息只需要写一次,相比写扩散的模式,能够大大降低消息写入次数,特别是在群消息这种场景下。但其缺点也比较明显,接收端去同步消息的逻辑会相对复杂和低效。接收端需要对每个会话都拉取一次才能获取全部消息,读被大大的放大,并且会产生很多无效的读,因为并不是每个会话都会有新消息产生。
  • 写扩散:写扩散的消息同步模式,需要有一个额外的Timeline来专门用于消息同步,通常是每个接收端都会拥有一个独立的同步Timeline(或者叫收件箱),用于存放需要向这个接收端同步的所有消息。每个会话中的消息,会产生多次写,除了写入用于消息存储的会话Timeline,还需要写入需要同步到的接收端的同步Timeline。在个人与个人的会话中,消息会被额外写两次,除了写入这个会话的存储Timeline,还需要写入参与这个会话的两个接收者的同步Timeline。而在群这个场景下,写入会被更加的放大,如果这个群拥有N个参与者,那每条消息都需要额外的写N次。写扩散同步模式的优点是,在接收端消息同步逻辑会非常简单,只需要从其同步Timeline中读取一次即可,大大降低了消息同步所需的读的压力。其缺点就是消息写入会被放大,特别是针对群这种场景。
    Timeline模型不会对选择读扩散还是写扩散做约束,而是能同时支持两种模式,因为本质上两种模式的逻辑数据模型并无差别,只是消息数据是用一个Timeline来支持多端读还是复制到多个Timeline来支持多端读的问题。

针对IM这种应用场景,消息系统通常会选择写扩散这种消息同步模式。IM场景下,一条消息只会产生一次,但是会被读取多次,是典型的读多写少的场景,消息的读写比例大概是10:1。若使用读扩散同步模式,整个系统的读写比例会被放大到100:1。一个优化的好的系统,必须从设计上去平衡这种读写压力,避免读或写任意一维触碰到天花板。所以IM系统这类场景下,通常会应用写扩散这种同步模式,来平衡读和写,将100:1的读写比例平衡到30:30。当然写扩散这种同步模式,还需要处理一些极端场景,例如万人大群。针对这种极端写扩散的场景,会退化到使用读扩散。一个简单的IM系统,通常会在产品层面限制这种大群的存在,而对于一个高级的IM系统,会采用读写扩散混合的同步模式,来满足这类产品的需求。采用混合模式,会根据数据的不同类型和不同的读写负载,来决定用写扩散还是读扩散。

典型架构设计

如图是一个典型的消息系统架构,架构中包含几个重要组件:

  • :作为消息的发送和接收端,通过连接消息服务器来发送和接收消息。
  • 消息服务器:一组无状态的服务器,可水平扩展,处理消息的发送和接收请求,连接后端消息系统。
  • 消息队列:新写入消息的缓冲队列,消息系统的前置消息存储,用于削峰填谷以及异步消费。
  • 消息处理:一组无状态的消费处理服务器,用于异步消费消息队列中的消息数据,处理消息的持久化和写扩散同步。
  • 消息存储和索引库:持久化存储消息,每个会话对应一个Timeline进行消息存储,存储的消息建立索引来实现消息检索。
  • 消息同步库:写扩散形式同步消息,每个用户的收件箱对应一个Timeline,同步库内消息不需要永久保存,通常对消息设定一个生命周期。
    新消息会由端发出,通常消息体中会携带消息ID(用于去重)、逻辑时间戳(用于排序)、消息类型(控制消息、图片消息或者文本消息等)、消息体等内容。消息会先写入消息队列,作为底层存储的一个临时缓冲区。消息队列中的消息会由消息处理服务器消费,可以允许乱序消费。消息处理服务器对消息先存储后同步,先写入发件箱Timeline(存储库),后写扩散至各个接收端的收件箱(同步库)。消息数据写入存储库后,会被近实时的构建索引,索引包括文本消息的全文索引以及多字段索引(发送方、消息类型等)。

对于在线的设备,可以由消息服务器主动推送至在线设备端。对于离线设备,登录后会主动向服务端同步消息。每个设备会在本地保留有最新一条消息的顺序ID,向服务端同步该顺序ID后的所有消息。

总结

本篇文章主要介绍了现代IM系统中消息系统所需要具备的能力,对比了传统架构和现代架构。为方便接下来的深入探讨,介绍了表格存储Tablestore推出的Timeline模型,以及在IM系统中消息存储和消息同步模型的基本概念和策略,最后介绍了一个典型的架构设计。

作者:木洛

原文链接

本文为云栖社区原创内容,未经允许不得转载。

Redis 概念以及底层数据结构

mumupudding阅读(3)


Redis 简介

REmote DIctionary Server(Redis) 是一个由SalvatoreSanfilippo写的key-value存储系统。

Redis是一个开源的使用ANSI C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。

它通常被称为数据结构服务器,因为值(value)可以是字符串(String), 哈希(Map), 列表(list), 集合(sets) 和有序集合(sorted sets)等类型。

Redis特点

Redis 是完全开源免费的,遵守BSD协议,是一个高性能的key-value数据库。

Redis 与其他 key – value 缓存产品有以下三个特点:

  • Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用。

  • Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。

  • Redis支持数据的备份,即master-slave模式的数据备份。

Redis 优势

性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。

丰富的数据类型 – Redis支持 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。

原子 – Redis的所有操作都是原子性的,同时Redis还支持对几个操作全并后的原子性执行。

丰富的特性 – Redis 还支持 publish/subscribe, 队列,key 过期等等特性。

Redis对象类型简介

  • Redis是一种key/value型数据库,其中,每个key和value都是使用对象表示的。比如,我们执行以下代码:
redis> SET message "hello redis"  

其中的key是message,是一个包含了字符串"message"的对象。而value是一个包含了"hello redis"的对象。Redis共有五种对象的类型,分别是:

类型常量 对象的名称
REDIS_STRING 字符串对象
REDIS_LIST 列表对象
REDIS_HASH 哈希对象
REDIS_SET 集合对象
REDIS_ZSET 有序集合对象

Redis中的一个对象的结构体表示如下:

typedef struct redisObject {        // 类型      unsigned type:4;              // 编码方式      unsigned encoding: 4;        // 引用计数      int refcount;        // 指向对象的值      void *ptr;    } robj;  

type表示了该对象的对象类型,即上面五个中的一个。但为了提高存储效率与程序执行效率,每种对象的底层数据结构实现都可能不止一种。encoding就表示了对象底层所使用的编码。

  • Redis对象底层数据结构
编码常量 编码所对应的底层数据结构
REDIS_ENCODING_INT long 类型的整数
REDIS_ENCODING_EMBSTR embstr 编码的简单动态字符串
REDIS_ENCODING_RAW 简单动态字符串
REDIS_ENCODING_HT 字典
REDIS_ENCODING_LINKEDLIST 双端链表
REDIS_ENCODING_ZIPLIST 压缩列表
REDIS_ENCODING_INTSET 整数集合
REDIS_ENCODING_SKIPLIST 跳跃表和字典
  • 字符串对象

字符串对象的编码可以是int、raw或者embstr如果一个字符串的内容可以转换为long,那么该字符串就会被转换成为long类型,对象的ptr就会指向该long,并且对象类型也用int类型表示。普通的字符串有两种,embstr和raw。embstr应该是Redis 3.0新增的数据结构,在2.8中是没有的。如果字符串对象的长度小于39字节,就用embstr对象。否则用传统的raw对象。

#define REDIS_ENCODING_EMBSTR_SIZE_LIMIT 44  robj *createStringObject(char *ptr, size_t len) {      if (len <= REDIS_ENCODING_EMBSTR_SIZE_LIMIT)          return createEmbeddedStringObject(ptr,len);      else          return createRawStringObject(ptr,len);  }  

embstr的好处有如下几点:

  1. embstr的创建只需分配一次内存,而raw为两次(一次为sds分配对象,另一次为objet分配对象,embstr省去了第一次)。
  2. 相对地,释放内存的次数也由两次变为一次。
  3. embstr的objet和sds放在一起,更好地利用缓存带来的优势。

raw和embstr的区别可以用下面两幅图所示:

图-1.png

图-2.png

  • 列表对象列表对象的编码可以是ziplist或者linkedlist
  1. ziplist是一种压缩链表,它的好处是更能节省内存空间,因为它所存储的内容都是在连续的内存区域当中的。当列表对象元素不大,每个元素也不大的时候,就采用ziplist存储但当数据量过大时就ziplist就不是那么好用了。因为为了保证他存储内容在内存中的连续性,插入的复杂度是O(N),即每次插入都会重新进行realloc。如下图所示,对象结构中ptr所指向的就是一个ziplist整个ziplist只需要malloc一次,它们在内存中是一块连续的区域。

图-3.png

  1. linkedlist是一种双向链表。它的结构比较简单,节点中存放pre和next两个指针,还有节点相关的信息。当每增加一个node的时候,就需要重新malloc一块内存。

图-4.png

  • 哈希对象哈希对象的底层实现可以是ziplist或者hashtable。ziplist中的哈希对象是按照key1,value1,key2,value2这样的顺序存放来存储的。当对象数目不多且内容不大时,这种方式效率是很高的。

hashtable的是由dict这个结构来实现的, dict是一个字典,其中的指针dicht ht[2] 指向了两个哈希表

typedef struct dict {      dictType *type;      void *privdata;      dictht ht[2];      long rehashidx; /* rehashing not in progress if rehashidx == -1 */      int iterators; /* number of iterators currently running */  } dict;  typedef struct dictht {      dictEntry **table;      unsigned long size;      unsigned long sizemask;      unsigned long used;  } dictht;  

dicht[0] 是用于真正存放数据,dicht[1]一般在哈希表元素过多进行rehash的时候用于中转数据。dictht中的table用语真正存放元素了,每个key/value对用一个dictEntry表示,放在dictEntry数组中。

图-5.png

  • 集合对象集合对象的编码可以是intset或者hashtableintset是一个整数集合,里面存的为某种同一类型的整数,支持如下三种长度的整数:
#define INTSET_ENC_INT16 (sizeof(int16_t))  #define INTSET_ENC_INT32 (sizeof(int32_t))  #define INTSET_ENC_INT64 (sizeof(int64_t))  

intset是一个有序集合,查找元素的复杂度为O(logN),但插入时不一定为O(logN),因为有可能涉及到升级操作。比如当集合里全是int16_t型的整数,这时要插入一个int32_t,那么为了维持集合中数据类型的一致,那么所有的数据都会被转换成int32_t类型,涉及到内存的重新分配,这时插入的复杂度就为O(N)了。intset不支持降级操作。

  • 有序集合对象有序集合的编码可能两种,一种是ziplist,另一种是skiplist与dict的结合。ziplist作为集合和作为哈希对象是一样的,member和score顺序存放。按照score从小到大顺序排列skiplist是一种跳跃表,它实现了有序集合中的快速查找,在大多数情况下它的速度都可以和平衡树差不多。但它的实现比较简单,可以作为平衡树的替代品。它的结构比较特殊。下面分别是跳跃表skiplist和它内部的节点skiplistNode的结构体:
/*  * 跳跃表  */  typedef struct zskiplist {      // 头节点,尾节点      struct zskiplistNode *header, *tail;      // 节点数量      unsigned long length;      // 目前表内节点的最大层数      int level;  } zskiplist;  /* ZSETs use a specialized version of Skiplists */  /*  * 跳跃表节点  */  typedef struct zskiplistNode {      // member 对象      robj *obj;      // 分值      double score;      // 后退指针      struct zskiplistNode *backward;      // 层      struct zskiplistLevel {          // 前进指针          struct zskiplistNode *forward;          // 这个层跨越的节点数量          unsigned int span;      } level[];  } zskiplistNode;  

head和tail分别指向头节点和尾节点,然后每个skiplistNode里面的结构又是分层的(即level数组)用图表示,大概是下面这个样子:

图-6.png

总结

以上简单介绍了Redis的简介,特性以及五种对象类型和五种对象类型的底层实现。事实上,Redis的高效性和灵活性正是得益于同一个对象类型采用不同的底层结构,并且在必要的时候对二者进行转换,还有就是各种底层结构对内存的合理利用。

本文作者:Worktile高级工程师 龚林杰

文章来源:Worktile技术博客

欢迎访问交流更多关于技术及协作的问题。

文章转载请注明出处。

Spring Cloud OAuth 实现微服务内部Token传递的源码解析

mumupudding阅读(3)


背景分析

本文主要来探讨第三部 A –> B ,token 自定维护的源码实现

如何实现token 传递

配置OAuth2FeignRequestInterceptor 即可

  • 此类是Feign 的拦截器实现

@Bean@ConditionalOnProperty("security.oauth2.client.client-id")public RequestInterceptor oauth2FeignRequestInterceptor(OAuth2ClientContext oAuth2ClientContext,              OAuth2ProtectedResourceDetails resource,) { return new OAuth2FeignRequestInterceptor(oAuth2ClientContext, resource);}

源码解析

  • 获取上下文中的token ,组装到请求头
public class OAuth2FeignRequestInterceptor implements RequestInterceptor { // 给请求增加 token @Override public void apply(RequestTemplate template) {  template.header(header, extract(tokenType)); }  protected String extract(String tokenType) {  OAuth2AccessToken accessToken = getToken();  return String.format("%s %s", tokenType, accessToken.getValue()); } // 从spring security 上下文中获取token public OAuth2AccessToken getToken() {  OAuth2AccessToken accessToken = oAuth2ClientContext.getAccessToken();  if (accessToken == null || accessToken.isExpired()) {   try {    accessToken = acquireAccessToken();   }  }  return accessToken; }}
  • 再来看AccessTokenContextRelay, 上下文token 中转器.非常简单从上下文获取认证信息得到把 token 放到上下文
public class AccessTokenContextRelay { private OAuth2ClientContext context; public AccessTokenContextRelay(OAuth2ClientContext context) {  this.context = context; }     public boolean copyToken() {  if (context.getAccessToken() == null) {   Authentication authentication = SecurityContextHolder.getContext()     .getAuthentication();   if (authentication != null) {    Object details = authentication.getDetails();    if (details instanceof OAuth2AuthenticationDetails) {     OAuth2AuthenticationDetails holder = (OAuth2AuthenticationDetails) details;     String token = holder.getTokenValue();     DefaultOAuth2AccessToken accessToken = new DefaultOAuth2AccessToken(       token);     String tokenType = holder.getTokenType();     if (tokenType != null) {      accessToken.setTokenType(tokenType);     }     context.setAccessToken(accessToken);     return true;    }   }  }  return false; }}
  • 什么时候执行中转,oauth2 资源服务器非常简单暴力,加了个拦截器给转发。

源码非常简单

谈谈spring security oauth 实现的问题

  1. 当请求上下文没有Token,如果调用feign 会直接,这个OAuth2FeignRequestInterceptor 肯定会报错,因为上下文copy 失败
  2. 如果设置线程隔离,这里也会报错。导致安全上下问题传递不到子线程中。
  3. 强制使用拦截器去处理 token 转发到这里上下文,使用的业务场景只有这里,影响性能高

这三个问题,大家在使用的过程中一定会遇到

自定义OAuth2FeignRequestInterceptor

  • 通过外部条件是否执行token中转
public void apply(RequestTemplate template) { Collection<String> fromHeader = template.headers().get(SecurityConstants.FROM); if (CollUtil.isNotEmpty(fromHeader) && fromHeader.contains(SecurityConstants.FROM_IN)) {  return; } accessTokenContextRelay.copyToken(); if (oAuth2ClientContext != null  && oAuth2ClientContext.getAccessToken() != null) {  super.apply(template); }}
  • 手动调用accessTokenContextRelay的copy,当然需要覆盖原生oauth 客户端的配置

总结

欢迎关注我们获得更多的好玩JavaEE 实践

Spring如何使用4行代码优雅的实现模糊查询,精确查询,分页查询功能。

mumupudding阅读(3)

最近开始使用Spring开发新项目了,开发新项目必定少不了折腾增删查改。 其中模糊查询,精确查询,分页查询也算是不好对付的功能,需要手写大量重复的代码来实现相关的功能,如何优雅的实现查询功能呢? 

首先上两张截图。

 

第一张截图是分页列出活动,可以根据活动名称和活动内容进行模糊查询。 举办机构,举办年份,活动分类为可选项。如指定了年份就会检索相应年份的数据。

第二张截图是列出分类。 可以根据分类名称进行模糊查询。 

可以发现,两个listData 方法均只有4行代码。 

第一行指定模糊查询的列名s,第二行获取一个 Specification 对象,并且传递需精确匹配的参数。 第三行查询。第四行return 。

以下是获取Specification对象的实现 。   这里使用了静态泛型方法,关键在于熟练的使用 CriteriaBuilder 来构建or  and  like  eq 等操作符号。 

 

最终的实现效果如下: 

 

 

这里需要根据fid 隔离不同用户的数据,所以每条sql查询都带有一个fid=? 条件查询,当然这个条件是通过 addCommon 方法来实现的。 如果有其他共同的查询条件,在此方法中实现即可。

 

PageRequestUtil 类的实现,简单的实现了参数的校验。 比如页码不能为负数,每页最多100条数据。 如果 numPerPage大于100,很有可能就是有人在采集你网站的数据了,虽然这种限制也根本阻止不了别人的采集行为。但至少可以使得程序更加安全一点,不会一次性查询大量数据导致mysql崩溃。

 

 

 

如何应对新的业务, 如果只有根据某些字段进行模糊查询, 只需写4行代码足以实现分页功能。 

如何应对业务的变更, 今天产品说根据活动名称检索足以,但第二天又说需要跟据活动内容进行检索,或者新增检索字段。 那么还是只需要修改第一行代码即可。 

 

 

 

 

 

 

 

 

 

 

开源|ns4_frame分布式服务框架开发指南

mumupudding阅读(2)

导语:宜信于2019年3月29日正式开源nextsystem4(以下简称“NS4”)系列模块。此次开源的NS4系列模块是围绕当前支付系统笨重、代码耦合度高、维护成本高而产生的分布式业务系统解决方案。NS4系列框架允许创建复杂的流程/业务流,对于业务服务节点的实现可串联,可分布式。其精简、轻量,实现了“脱容器”(不依赖tomcat、jetty等容器)独立运行。NS4系列框架的设计理念是将业务和逻辑进行分离,开发人员只需通过简单的配置和业务实现就可以实现逻辑复杂、性能高效、功能稳定的业务系统。【点击查看框架整体介绍】

NS4系列包括4个开源模块,分别是:ns4_frame分布式服务框架、ns4_gear_idgen ID 生成器组件(NS4框架Demo示例)、ns4_gear_watchdog 监控系统组件(服务守护、应用性能监控、数据采集、自动化报警系统)和ns4_chatbot通讯组件。本文将详细介绍ns4_frame分布式服务框架开发指南。

项目开源地址:https://github.com/newsettle/ns4_frame

一、框架介绍

ns4_frame本质上是两个应用加三套开发框架组合起来的分布式业务框架。

1.1 使用范围

ns4_frame分布式框架主要适用于业务类型为消息流或者业务核心模型为流式业务的业务系统。它支持消息分发、传递、追踪,支持分步骤、分批次的消息处理,对于信息流、数据流等消息驱动型的业务尤为契合。

1.2 项目结构

ns4_frame框架是一套MAVEN父子项目,由五个项目组成:

  • NS_MQ :负责和底层消息队列进行通信,提供了对消息队列进行操作的API。
  • NS_TRANSPORTER:通过调用NS_MQ提供的API,对业务消息进行收取、处理、转发。
  • NS_CHAIN :一个可选开发框架,负责对同一个jvm中的业务处理步骤进行链条式的整合,组成当前业务模块的业务处理流程。
  • NS_CONTROLLER:一个业务消息转发应用,负责将接收到的消息转给对应的业务模块进行处理,同时负责将业务模块根据整体业务进行关联。NS_CONTROLLER本质是一个独立的应用系统,构建于 NS_TRANPORTOR和NS_CHAIN之上。
  • NS_DISPATCHER :ns4_frame架构规定的消息入口,通过提供的http服务接受业务系统边界外的http请求,并将请求转化成业务系统内部通信使用的消息协议格式。

二、基础入⻔

2.1 开发环境配置

开发语言:JAVA

JDK版本:JDK1.7

MAVEN版本:3.3以上

REDIS版本:3.0以上

以上是开发环境必备的组件和配置,其中java为开发语言,maven为项目编译打包部署必备, redis作为消息中间件使用。

2.2 运行

ns4_frame运行至少需要启动三个jvm项目才能完整运行。启动整个项目分为如下三步:

  • 第一步: ns4_frame消息入口是一个http接口,http服务是由NS_DISPATCHER项目提供的,所以我们第一件事情就是要把NS_DISPATCHER运行起来。要运行NS_DISPATCHER,直接运行Bootstrap类的main方法即可。默认的NS_DISPATCHER会在本机(127.0.0.1)的8027端口上监听http请求,如果收到http请求,默认会将http请求转换成内部通信消息,并存储到本机(127.0.0.1)的 redis中,默认访问的redis端口号是6379。

  • 第二步: NS_CONTROLLER负责接收NS_DISPATCHER传入的消息,并根据配置进行消息分发。 所以我们随后需要运行NS_CONTROLLER项目(为了方便,以下简称“CONTROLLER”) 。在CONTROLLER项目中我们不能直接运行,需要配置一些东⻄。CONTROLLER要运行至少需要指定一个配置文件位置。这个配置文件需要通过java命令参数来指定。假设我现在指定java运行参数-Dconfigfile=nscontroller.xml这个参数本质上是给CONTROLLER底层的NS_TRANSPORTER使用的,它指明了 NS_TRANSPORTER必须得配置文件位置,使得CONTROLLER能顺利利用 NS_TRANSPORTER进行消息收发。默认情况下,CONTROLLER还会到classpath下去找关于NS_CHAIN需要的配置文件, 默认路径是classpath下的nschainconfig目录,在这个目录下所有的xml文件会被认作是NS_CHAIN需要的配置文件集合。当配置文件配置好后,可以通过调用 com.creditease.ns.transporter.context.XmlAppTransporterContext的 main方法来启动NS_CONTROLLER 。

  • 第三步: 在这个步骤中我们需要启动自己的业务项目,在这个业务项目中,必须有以下三个前置条件:

    • 业务项目需要建立在NS_TRANSPORTER框架之上。
    • 业务项目的消息队列名称必须和CONTROLLER项目中配置的队列名一致。
    • 业务启动必须通过 com.creditease.ns.transporter.context.XmlAppTransporterContext的 main方法来启动。

完成以上的三个步骤,一个基本的ns4_frame系统就搭建好并运行起来了。

三、项目架构

3.1 层次划分

上图展示了ns4_frame每个系统的层次结构。

  • 底层是以redis作为消息中间件,对消息中间件的操作被封装入了NS_MQ项目,它向上层提供了对消息队列的操作API接口。
  • 在NS_MQ的上层是NS_TRANSPORTER,它本质是一套消息收发处理框架,它负责接收消息后反向回调业务代码,并将消息交给业务层处理。当业务层处理完毕后,它负责将处理后的消息返回到redis中。
  • NS_CHAINS是一套开发辅助框架,它负责将一个模块的业务处理步骤解耦成一个个零散的任务,并可以随意以任何顺序做关联。
  • NS_CONTROLLER是一个项目,它本质上是一个独立的应用,它负责将整体业务分解成一个个节点,并通过配置将他们以一定的顺序关联起来,并通过消息机制,将这些节点结合起来 形成一套业务系统。
  • NS_DISPATCHER也是一个项目,它是以NETTY框架作为基础,开发出的一个能提供基本的 http服务的独立应用。同时它也是业务系统和外部通讯的唯一边界。

3.2 运行流程

ns4_frame整套系统本质上其实就是一套消息中间件服务加开发框架,整体的结构图如下:

上图显示了一个ns4_frame整体分布式项目的运行流程,一个消息的运转流程按如下顺序:

  • NS_DISPATCHER收到http请求,并将http请求转化为内部消息协议放入指定的消息队列中(根据配置文件)。
  • NS_CONTORLLER从步骤1指定的队列接收到消息,并根据配置的服务编排开始按照顺序将消息发送到每个服务步骤对应的消息队列中。
  • 业务系统收到步骤2中NS_CONTROLLER指定的消息队列接收到消息并开始处理,处理完毕后,将结果返回。
  • NS_CONTROLLER收到业务系统的响应,开始根据配置好的服务,将返回的消息结果发送到下一个服务对应的消息队列中。

四、NS_MQ框架介绍

4.1 核心类和接口

  • RedisMQTemplate类:封装了所有和消息队列的操作相关的API
  • MQConfig:存储了所有和底层消息中间件相关的配置。

4.2 配置方案

默认的,在没有做任何配置的情况下,NS_MQ会自动访问本机(127.0.0.1)的6379端口的redis,如果没有,则会报异常。通常,NS_MQ会去找classpath下一个名为ns_mq.properties的配置文件,这个配置文件中存储着所有和底层消息中间件相关的属性。

列举一些关键的配置元素:

  • redis.type 1 代表redis单机 2 代表redis集群 默认为1
  • redis.single/cluster.host redis单机或者集群的主机地址(包含端口)
  • redis.single/cluster.maxTotal redis单机或者集群的最大连接数
  • redis.single/cluster.miniIdle redis单机或者集群的最小闲置连接数
  • redis.single/cluster.maxIdle redis单机或者集群的最大闲置连接数
  • redis.single/cluster.connectionTimeout redis单机或者集群的尝试连接的超时时间(尚未连接到服务需要等待的时间)
  • redis.single/cluster.socketTimeout redis单机或者集群连接后socket闲置的超时时间

五、NS_TRANSPORTER框架介绍

5.1 框架架构

上图展示了整个NS_TRANSPORTER的整体架构,整套框架收发处理消息分为如下三个步骤:

  • 首先由接收消息的线程(Fetcher线程)通过NS_MQ从底层消息中间件获取消息并放入到本地消息缓存。
  • 消息处理线程(Handler线程)从本地消息缓存中取出消息,并调用业务层的方法实现对消息进行处理,处理完毕后,将处理后的消息放到本地发送缓存。
  • 发送线程(Sender线程)从本地发送缓存取出消息后,将消息通过NS_MQ将消息放入底层消息中间件。

5.2 核心类和接口

  • ServiceMessage:对各个模块之间传递的消息的java封装,包含了模块间通信需要知道的任何信息;

  • Worker:业务层需要实现此接口的doWork方法,实现此接口的对象会被NS_TRANSPORTER的Handler线程回调用来对ServiceMessage中的信息进行处理。

  • ActionWorker:已经部分封装好的抽象类,实现了Worker接口,业务层可以直接继承这个抽象类,简化开发。

5.3 配置方案

默认的,NS_TRANSPORTER会去找名为configfile的系统变量,这个系统变量的值就是NS_TRANSPOTER需要的配置文件所在的路径,NS_TRANSPORTER会找到这个xml配置文件,并在解析相关的配置后启动。

NS_TRANSPORTER相关的配置文件模板如下:

<queues>     <prefix></prefix>     <launchers>           <launcher><class name="类的全名" method="method方法名" property="" /><class name="类的全名" static-method="method方法名" /> </launcher>     </launchers>       <inqueues>           <queue>               <name></name><fetchernum></fetchernum> <buffersize></buffersize> <handlersize></handlersize> <serviceClass></serviceClass> <sendernum></sendernum>           </queue>     </inqueues></queues>

以上xml模板中有如下几个关键元素需要注意:

  • Launcher:用来定义在整个框架完全运行之前需要执行的方法。
  • queue:在这个元素下 name元素表示需要监听的底层消息中间件的队列名。
  • Fetchernum:表示监听消息队列并获取消息的线程数,默认是1。
  • buffersize:表示本地接收/发送消息队列的大小默认是100。
  • handlersize:表示处理消息的线程数,默认是10。
  • Serviceclass:表示具体的处理消息的业务类,这个类必须实现了Worker接口。
  • Sendernum:表示从本地发送消息队列中获取消息后发送到底层消息中间件的线程数。

六、NS_CHAIN框架介绍

6.1 框架架构

由于NS_CHAIN本质是一个纯开发框架,故暂时忽略此框架的框架架构。

6.2 核心类和接口

暂略

6.3 配置方案

本节将详细介绍NS_CHAINS的配置。

NS_CHAINS启动时会去找系统变量chainconfig,这个变量的值就是NS_CHAINS配置文件所在的路径。NS_CHAINS支持配置目录(目录下的所有xml格式文件都被视作NS_CHAINS框架的配置文件)和配置文件。

对于NS_CHAINS的配置格式我们大致列举出关键要素如下:

  • catalog:这个相当于一个完整的服务或者一个命名空间,是NS_CHAINS对外服务的基本单位,NS_CHAINS外部系统只能看到catalog。
  • Command:这是NS_CHAINS任务执行的最小单位,所有执行任务都可以以command的形式被调用执行。
  • Chain:这是一个command的容器,可以将多个command的任务组合成一个执行链路。
  • Group:这个一个command的组合,它可以将多个command组合成一个整体,并按照配置顺序执行。
  • 同时NS_CHAINS的配置具有完整的逻辑语法,支持if条件判断,while循环结构和顺序结构。

七、NS_DISPATCHER应用介绍

7.1 框架架构

NS_DISPATCHER本质是一个独立的建立在Netty框架上的一个能提供http服务的独立应用,所以框架结构此处从略。

7.2 核心类和接口

NS_DISPATCHER是以NETTY框架为基础的,所以其核心类就是如下的几个协议处理器:

  • HttpDispatcherServerHandler:主要负责解析传入的http请求,并封装成对应的java对象交给 HttpRPCHandler做进一步处理。
  • HttpRPCHandler:主要接收上一步封装好的java对象,并取出对应的请求参数、请求内容等,封装成系统内部传输用的协议对象,并可以以同步请求响应模式/异步发送模式将协议对象放入底层消息中间件。

7.3 配置方案

NS_DISPATCHER启动会去找ns_dispatcher.properties文件,下面介绍配置的关键元素:

  • http.port:指定了http服务的监听端口。
  • dispatcher.pool.num:指定了dispatcher的并发线程数,dispatcher的性能和这个参数有非常大的关系。
  • dispatcher.queuename:封装好的内部协议消息要放入的队列的名字,NS_DISPATCHER也支持https,所以,如果在ns_dispatcher.properties文件中有如下几个选项,那么NS_DISPATCHER也会启动对应的https服务。
  • ca.path:指明了可信任证书的路径。
  • key.path:指明了公钥的路径。
  • https.port:指明了https服务监听的端口。

八、NS_CONTROLLER应用介绍

8.1 框架架构

NS_CONTROLLER本质是建立在NS_TRANSPORTER和NS_CHAINS上的独立应用,核心就是 NS_TRANSPORTER的架构加NS_CHAINS的辅助,故不再重复列举其架构。

8.2 核心类和接口

DefaultPublishCommand:这是NS_CONTROLLER对于NS_CHAINS的一个扩展,它支持同步发送消息,并等待消息的响应,并可以设置等待响应的超时时间。同时,还支持异步发送消息,不需要等待消息的响应。

8.3 配置方案

遵循NS_TRANSPORTER和NS_CHAINS的配置规则,所以不再赘述。注意:在NS_CONTROLLER中对于NS_CHAINS的配置做了一些功能扩展,主要是添加了publish的配置元素,这个随后可以提供配置模板。

九、项目部署

9.1 部署方案

如果要部署整个ns4_frame项目,请按照以下步骤进行:

  • 部署NS_DISPATCHER项目:NS_DISPATCHER项目是一个Maven项目,首先需要通过mvn:package deploy将整个项目打成一个zip包上传到服务器,然后解压成一个目录。在这个目录中,有如下几个子目录:bin、config、lib、logs。其中,bin目录中包含了DISPATCHER的启动脚本;config目录存放了NS_DISPATCHER必须的配置文件;lib目录存放了NS_DISPATCHER所需要的所有jar包;logs目录存放了所有NS_DISPATCHER打印的日志。
  • 部署NS_CONTROLLER项目:NS_CONTROLLER项目也是一个Maven项目,需要通过mvn:package deploy将整个项目打成一个zip包。目录结构同NS_DISPATCHER项目,此处不再赘述。
  • 部署业务代码:业务代码请自行按照各个团队的规则部署。

十、运行日志

10.1 日志分类

ns4_frame项目将日志大致分成了四类:

  • *fram.log:系统日志,属于整个ns4_frame底层系统内部的日志,包括系统的启动,线程的启动关闭等信息。
  • *biz.log:业务日志,所有业务相关的日志统统会被导向到这里。
  • *flow.log:消息流日志,这里记录了系统所有消息的流转信息。
  • *mq.log:这里记录所有对底层消息中间件进行操作的信息。

10.2 如何查看日志

  • 业务报错:如果业务报错,基本所有的报错信息都会在*biz.log中查到。
  • 消息流转: 如果是消息发送响应的问题,基本上在*flow.log中可以查到或者推断出相关的信息。
  • 底层消息中间件交互: 如果消息流转无法推断出问题,或者无法查到对应的消息,就需要转到*mq.log中进行查询。

十一、其他

11.1 常⻅问题

ns4_frame系统本质是一个以消息为通信机制的分布式系统,经常出现的问题分成以下两部分:

  • 业务异常

由于业务本身是由底层NS_TRANSPORTER回调来执行的,当业务出现异常的时候,很可能由于没有合适的被catch到,从而被底层的NS_TRANSPOTER框架捕获。 对于没有在*biz.log和stdoout.log中查找到的问题,可以去查看下*flow.log的日志,看是否出现了异常被底层NS_TRANSPOTER捕获了。

  • 底层异常

有些情况,业务本身并没有出现问题,但是由于消息通信出现了问题,会导致业务没有执行,对于 这种情况我们需要首先从消息入口处即NS_DISPATCHER的*flow.log中查找到对应的 messageId,然后根据消息流转路径,一步步去对应的部署机器上查询。

内容来源:宜信技术学院

Java并发编程之CountDownLatch源码解析

mumupudding阅读(1)


一、导语

最近在学习并发编程原理,所以准备整理一下自己学到的知识,先写一篇CountDownLatch的源码分析,之后希望可以慢慢写完整个并发编程。

二、什么是CountDownLatch

CountDownLatch是java的JUC并发包里的一个工具类,可以理解为一个倒计时器,主要是用来控制多个线程之间的通信。
比如有一个主线程A,它要等待其他4个子线程执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。

三、简单使用

public static void main(String[] args){ System.out.println("主线程和他的两个小兄弟约好去吃火锅"); System.out.println("主线程进入了饭店"); System.out.println("主线程想要开始动筷子吃饭"); //new一个计数器,初始值为2,当计数器为0时,主线程开始执行 CountDownLatch latch = new CountDownLatch(2);   new Thread(){             public void run() {                 try {                    System.out.println("子线程1——小兄弟A 正在到饭店的路上");                    Thread.sleep(3000);                    System.out.println("子线程1——小兄弟A 到饭店了");      //一个小兄弟到了,计数器-1                    latch.countDown();                } catch (InterruptedException e) {                    e.printStackTrace();                }             };         }.start();     new Thread(){             public void run() {                 try {                    System.out.println("子线程2——小兄弟B 正在到饭店的路上");                    Thread.sleep(3000);                    System.out.println("子线程2——小兄弟B 到饭店了");      //另一个小兄弟到了,计数器-1                    latch.countDown();                } catch (InterruptedException e) {                    e.printStackTrace();                }             };         }.start();  //主线程等待,直到其他两个小兄弟也进入饭店(计数器==0),主线程才能吃饭  latch.await();  System.out.println("主线程终于可以开始吃饭了~");}

四、源码分析

核心代码:

CountDownLatch latch = new CountDownLatch(1);        latch.await();        latch.countDown();

其中构造函数的参数是计数器的值;
await()方法是用来阻塞线程,直到计数器的值为0
countDown()方法是执行计数器-1操作

1、首先来看构造函数的代码

public CountDownLatch(int count) {        if (count < 0) throw new IllegalArgumentException("count < 0");        this.sync = new Sync(count);    }

这段代码很简单,首先if判断传入的count是否<0,如果小于0直接抛异常。
然后new一个类Sync,这个Sync是什么呢?我们一起来看下

private static final class Sync extends AbstractQueuedSynchronizer {        private static final long serialVersionUID = 4982264981922014374L;        Sync(int count) {            setState(count);        }        int getCount() {            return getState();        } //尝试获取共享锁        protected int tryAcquireShared(int acquires) {            return (getState() == 0) ? 1 : -1;        } //尝试释放共享锁        protected boolean tryReleaseShared(int releases) {            // Decrement count; signal when transition to zero            for (;;) {                int c = getState();                if (c == 0)                    return false;                int nextc = c-1;                if (compareAndSetState(c, nextc))                    return nextc == 0;            }        }    }

可以看到Sync是一个内部类,继承了AQS,AQS是一个同步器,之后我们会详细讲。
其中有几个核心点:

  1. 变量 state是父类AQS里面的变量,在这里的语义是计数器的值
  2. getState()方法也是父类AQS里的方法,很简单,就是获取state的值
  3. tryAcquireShared和tryReleaseShared也是父类AQS里面的方法,在这里CountDownLatch对他们进行了重写,先有个印象,之后详讲。

2、了解了CountDownLatch的构造函数之后,我们再来看它的核心代码,首先是await()。

public void await() throws InterruptedException {        sync.acquireSharedInterruptibly(1);    }

可以看到,其实是通过内部类Sync调用了父类AQS的acquireSharedInterruptibly()方法。

public final void acquireSharedInterruptibly(int arg)            throws InterruptedException { //判断线程是否是中断状态        if (Thread.interrupted())            throw new InterruptedException(); //尝试获取state的值        if (tryAcquireShared(arg) < 0)//step1            doAcquireSharedInterruptibly(arg);//step2    }

tryAcquireShared(arg)这个方法就是我们刚才在Sync内看到的重写父类AQS的方法,意思就是判断是否getState() == 0,如果state为0,返回1,则step1处不进入if体内acquireSharedInterruptibly(int arg)方法执行完毕。若state!=0,则返回-1,进入if体内step2处。

下面我们来看acquireSharedInterruptibly(int arg)方法:

private void doAcquireSharedInterruptibly(int arg)        throws InterruptedException { //step1、把当前线程封装为共享类型的Node,加入队列尾部        final Node node = addWaiter(Node.SHARED);        boolean failed = true;        try {            for (;;) {  //step2、获取当前node的前一个元素                final Node p = node.predecessor();  //step3、如果前一个元素是队首                if (p == head) {      //step4、再次调用tryAcquireShared()方法,判断state的值是否为0                    int r = tryAcquireShared(arg);      //step5、如果state的值==0                    if (r >= 0) {   //step6、设置当前node为队首,并尝试释放共享锁                        setHeadAndPropagate(node, r);                        p.next = null; // help GC                        failed = false;                        return;                    }                }  //step7、是否可以安心挂起当前线程,是就挂起;并且判断当前线程是否中断                if (shouldParkAfterFailedAcquire(p, node) &&                    parkAndCheckInterrupt())                    throw new InterruptedException();            }        } finally { //step8、如果出现异常,failed没有更新为false,则把当前node从队列中取消            if (failed)                cancelAcquire(node);        }    }

按照代码中的注释,我们可以大概了解该方法的内容,下面我们来仔细看下其中调用的一些方法是干什么的。
1、首先看addWaiter()

//step1private Node addWaiter(Node mode) { //把当前线程封装为node        Node node = new Node(Thread.currentThread(), mode);        // Try the fast path of enq; backup to full enq on failure //获取当前队列的队尾tail,并赋值给pred        Node pred = tail; //如果pred!=null,即当前队尾不为null        if (pred != null) { //把当前队尾tail,变成当前node的前继节点            node.prev = pred;     //cas更新当前node为新的队尾            if (compareAndSetTail(pred, node)) {                pred.next = node;                return node;            }        } //如果队尾为空,走enq方法        enq(node);//step1.1        return node;    }-----------------------------------------------------------------//step1.1private Node enq(final Node node) {        for (;;) {            Node t = tail;     //如果队尾tail为null,初始化队列            if (t == null) { // Must initialize  //cas设置一个新的空node为队首                if (compareAndSetHead(new Node()))                    tail = head;            } else {  //cas把当前node设置为新队尾,把前队尾设置成当前node的前继节点                node.prev = t;                if (compareAndSetTail(t, node)) {                    t.next = node;                    return t;                }            }        }    }

2、接下来我们在来看setHeadAndPropagate()方法,看其内部实现

//step6private void setHeadAndPropagate(Node node, int propagate) { //获取队首head        Node h = head; // Record old head for check below //设置当前node为队首,并取消node所关联的线程        setHead(node); //        if (propagate > 0 || h == null || h.waitStatus < 0 ||            (h = head) == null || h.waitStatus < 0) {            Node s = node.next;     //如果当前node的后继节点为null或者是shared类型的            if (s == null || s.isShared())  //释放锁,唤醒下一个线程                doReleaseShared();//step6.1        }    }--------------------------------------------------------------------//step6.1private void doReleaseShared() {        for (;;) {     //找到头节点            Node h = head;            if (h != null && h != tail) {  //获取头节点状态                int ws = h.waitStatus;                if (ws == Node.SIGNAL) {                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))                        continue;            // loop to recheck cases      //唤醒head节点的next节点                    unparkSuccessor(h);                }                else if (ws == 0 &&                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))                    continue;                // loop on failed CAS            }            if (h == head)                   // loop if head changed                break;        }    }

3、接下来我们来看countDown()方法。

public void countDown() {        sync.releaseShared(1);    }

可以看到调用的是父类AQS的releaseShared 方法

public final boolean releaseShared(int arg) { //state-1        if (tryReleaseShared(arg)) {//step1     //唤醒等待线程,内部调用的是LockSupport.unpark方法            doReleaseShared();//step2            return true;        }        return false;    }------------------------------------------------------------------//step1protected boolean tryReleaseShared(int releases) {            // Decrement count; signal when transition to zero            for (;;) {  //获取当前state的值                int c = getState();                if (c == 0)                    return false;                int nextc = c-1;  //cas操作来进行原子减1                if (compareAndSetState(c, nextc))                    return nextc == 0;            }        }

五、总结

CountDownLatch主要是通过计数器state来控制是否可以执行其他操作,如果不能就通过LockSupport.park()方法挂起线程,直到其他线程执行完毕后唤醒它。下面我们通过一个简单的图来帮助我们理解一下:juc
PS:本人也是还在学习的路上,理解的也不是特别透彻,如有错误,愿倾听教诲。^_^

ES7之Async/await的使用详解

mumupudding阅读(25)

在 js 异步请求数据时,通常,我们多采用回调函数的方式解决,但是,如果有多个回调函数嵌套时,代码显得很不优雅,维护成本也相应较高。 ES6 提供的 Promise 方法和 ES7 提供的 Async/Await 语法糖可以更好解决多层回调问题。

Promise 对象用于表示一个异步操作的最终状态(完成或失败),以及其返回的值。await 操作符用于等待一个Promise 对象吗。。它只能在异步函数 async function 中使用。await 表达式会暂停当前 async function 的执行,等待 Promise 处理完成。若 Promise 正常处理(fulfilled),其回调的resolve函数参数作为 await 表达式的值,继续执行 async function。一个ajax请求时

通常 使用 ajax 请求数据时,会

$.ajax({ url: 'data1.json', type: 'GET', success: function (res) {  console.log(res) // 请求成功,则得到结果res }, error: function(err) {  console.log(err) }})//在此我向大家推荐一个前端全栈开发交流圈:619586920 突破技术瓶颈,提升思维能力

上面可以得到我们想要的结果 res —> { "url": "data2.json" }

多个ajax请求时

但是 当得到的数据 res 需要用于另一个 ajax 请求时,则需要如下写法:

$.ajax({ url: 'data1.json', type: 'GET', success: function (res) {  $.ajax({   url: res.url, // 将 第一个ajax请求成功得到的res 用于第二个ajax请求   type: 'GET',   success: function (res) {    $.ajax({     url: res.url, // 将第二个ajax请求成功得到的res 用于第三个ajax请求     type: 'GET',     success: function (res) {      console.log(res) // {url: "this is data3.json"}     },     error: function(err) {      console.log(err)     }    })   },   error: function(err) {    console.log(err)   }  }) },//在此我向大家推荐一个前端全栈开发交流圈:619586920 突破技术瓶颈,提升思维能力 error: function(err) {  console.log(err) }})

上面出现多个回调函数的嵌套,可读性较差(虽然这种嵌套在平常的开发中少见,但是在node服务端开发时,还是很常见的)

优化方法

使用 promise 链式操作

如下,使用 Promise,进行链式操作,可以使上面的异步代码看起来如同步般易读,从回调地狱中解脱出来。

function ajaxGet (url) { return new Promise(function (resolve, reject) {  $.ajax({   url: url,   type: 'GET',   success: function (res) {    resolve(res);   },   error: function(err) {    reject('请求失败');   }  }) })}; ajaxGet('data1.json').then((d) => { console.log(d);  // {url: "data2.json"} return ajaxGet(d.url);}).then((d) => { console.log(d);  // {url: "data3.json"} return ajaxGet(d.url);}).then((d) => { console.log(d);  // {url: "this is data3.json"}})//在此我向大家推荐一个前端全栈开发交流圈:619586920 突破技术瓶颈,提升思维能力

Async/await 方法

  • async 表示这是一个async函数,即异步函数,await只能用在这个函数里面。
  • await 表示在这里等待promise返回结果了,再继续执行。
  • await 后面跟着的应该是一个promise对象(当然,其他返回值也没关系,只是会立即执行,不过那样就没有意义了…)
  • await 操作符用于等待一个Promise 对象。它只能在异步函数 async function 中使用。
  • await 等待的虽然是promise对象,但不必写.then(..),直接可以得到返回值。

执行一个ajax请求,可以通过如下方法:

function ajaxGet (url) { return new Promise(function (resolve, reject) {  $.ajax({   url: url,   type: 'GET',   success: function (res) {    resolve(res)   },   error: function(err) {    reject('请求失败')   }  }) })}; async function getDate() { console.log('开始') let result1 = await ajaxGet('data1.json'); console.log('result1 ---> ', result1); // result1 ---> {url: "data2.json"}};getDate();  // 需要执行异步函数

执行多个ajax请求时:

function ajaxGet (url) { return new Promise(function (resolve, reject) {  $.ajax({   url: url,   type: 'GET',   success: function (res) {    resolve(res)   },   error: function(err) {    reject('请求失败')   }  }) })}; async function getDate() { console.log('开始') let result1 = await ajaxGet('data1.json'); let result2 = await ajaxGet(result1.url); let result3 = await ajaxGet(result2.url); console.log('result1 ---> ', result1); // result1 ---> {url: "data2.json"} console.log('result2 ---> ', result2); // result2 ---> {url: "data3.json"} console.log('result3 ---> ', result3); // result3 ---> {url: "this is data3.json"}}; getDate(); // 需要执行异步函数

async await捕捉错误:async await中.then(..)不用写了,那么.catch(..)也不用写,可以直接用标准的try catch语法捕捉错误。例如,如果下面的 url 写错了

function ajaxGet (url) { return new Promise(function (resolve, reject) {  $.ajax({   url: url111, // 此处为错误的 url   type: 'GET',   success: function (res) {    resolve(res)   },   error: function(err) {    reject('请求失败')   }  })//在此我向大家推荐一个前端全栈开发交流圈:619586920 突破技术瓶颈,提升思维能力 })};  async function getDate() { console.log('开始') try {  let result1 = await ajaxGet('data1.json'); // 执行到这里报错,直接跳至下面 catch() 语句  let result2 = await ajaxGet(result1.url);  let result3 = await ajaxGet(result2.url);  console.log('result1 ---> ', result1);  console.log('result2 ---> ', result2);  console.log('result3 ---> ', result3);  } catch(err) {  console.log(err) // ReferenceError: url111 is not defined }}; getDate(); // 需要执行异步函数

打开看板娘