微服务
微服务拆分
-
服务拆分策略
- 创建微服务架构的策略之一就是采用业务能力进行服务拆分
- 根据子域进行拆分
-
拆分的指导原则
1、单一职责、高内聚低耦合:简单来说一张表划分为一个服务
2、服务粒度适中:服务不要太细(有的团队甚至一个接口一个服务)
3、 以业务模型切入:比如产品,用户,订单为一个模型来切入
4.、演进式拆分:刚开始不要划分太细,可以随着迭代过程来逐步优化
5.、避免环形依赖与双向依赖:尽量不要做服务之间的循环依赖改,需要调整的类应该都在这个包之内。
补充几点实践的:
1、服务分层,将系统的功能进行分层,服务归属于前后中台,层间单向调用,有效控制调用深度
2、数据耦合,对于需要跨机事务的拆分,要重点分析,是否可以通过服务功能的调整避免(如将两段式提交变成数据副本的最终一致性),如果确实不行,对拆分的必要性进行确认。
网关Gateway和Zuul的区别
-
一方面因为Zuul1.0已经进入了维护阶段, 而且Gateway是SpringCloud团队研发的,是亲儿子产品,值得信赖。而且很多功能Zuul都没有用起来也非常的简单便捷。
-
Gateway是基于异步非阻塞模型上进行开发的,性能方面不需要担心。虽然Netlix早就发布了最新的Zuul 2.x,但Spring Cloud貌似没有整合计划。而且Netflix相关组件都布进入维护期;不知前景如何?
-
Zuul 1.x基于Servlet 2. 5使用阻塞架构它不支持任何长连接(如WebSocket) Zuul的设计模式和Nginx较像,每次I0操作都是从工作线程中选择一个执行, 请求线程被阻塞到工作线程完成,但是差别是Nginx用C++实现,Zuul 用Java实现,而JVM本身会有第一次加载较慢的情况,使得Zuul 的性能相对较差。
-
Zuul 2.x理念更先进,想基于Netty非阻塞和支持长连接,但SpringCloud目前还没有整合。Zuul 2.x的性能较Zuul 1.x有较大提升。在性能方面,根据官方提供的基准测试, Spring Cloud Gateway的RPS (每秒请求数)是Zuul的1.6倍。
-
Spring Cloud Gateway建立在Spring Framework
-
Project Reactor和Spring Boot2之上,使用非阻塞API。
-
Spring Cloud Gateway还支持WebSocket,并且 与Spring紧密集成拥有更好的开发体验
多方面综合考虑Gateway是很理想的网关选择。
分布式事务
什么是事务
事务是应用程序中一系列严密的操作,所有操作必须成功完成,否则在每个操作中所作的所有更改都会被撤消。也就是事务具有原子性,一个事务中的一系列的操作要么全部成功,要么一个都不做。
事务拥有以下四个特性,习惯上被称为ACID特性:
-
原子性(Atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。
-
一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态是指数据库中的数据应满足完整性约束。除此之外,一致性还有另外一层语义,就是事务的中间状态不能被观察到(这层语义也有说应该属于原子性)。
-
隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行,如同只有这一个操作在被数据库所执行一样。
-
持久性(Durability):已被提交的事务对数据库的修改应该永久保存在数据库中。在事务结束时,此操作将不可逆转。
本地事务
起初,事务仅限于对单一数据库资源的访问控制,架构服务化以后,事务的概念延伸到了服务中。倘若将一个单一的服务操作作为一个事务,那么整个服务操作只能涉及一个单一的数据库资源,这类基于单个服务单一数据库资源访问的事务,被称为本地事务(Local Transaction)。
分布式事务
分布式事务指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上,且属于不同的应用,分布式事务需要保证这些操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。
解决方案
Seata 2PC
seata中有两种分布式事务实现方案,AT及TCC
- AT模式主要关注多 DB 访问的数据一致性,当然也包括多服务下的多 DB 数据访问一致性问题
- TCC 模式主要关注业务拆分,在按照业务横向扩展资源时,解决微服务间调用的一致性问题
AT模式
Seata AT模式是基于XA事务演进而来的一个分布式事务中间件
解释:
Transaction Coordinator (TC): 事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。
Transaction Manager(TM): 控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。
Resource Manager (RM): 控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。
- 第一阶段
Seata 的 JDBC 数据源代理通过对业务 SQL 的解析,把业务数据在更新前后的数据镜像组织成回滚日志,利用 本地事务 的 ACID 特性,将业务数据的更新和回滚日志的写入在同一个 本地事务 中提交。
这样,可以保证:任何提交的业务数据的更新一定有相应的回滚日志存在
基于这样的机制,分支的本地事务便可以在全局事务的第一阶段提交,并马上释放本地事务锁定的资源
这也是Seata和XA事务的不同之处,两阶段提交往往对资源的锁定需要持续到第二阶段实际的提交或者回滚操作,而有了回滚日志之后,可以在第一阶段释放对资源的锁定,降低了锁范围,提高效率,即使第二阶段发生异常需要回滚,只需找对undolog中对应数据并反解析成sql来达到回滚目的
同时Seata通过代理数据源将业务sql的执行解析成undolog来与业务数据的更新同时入库,达到了对业务无侵入的效果。
- 第二阶段
如果决议是全局提交,此时分支事务此时已经完成提交,不需要同步协调处理(只需要异步清理回滚日志),Phase2 可以非常快速地完成.
如果决议是全局回滚,RM 收到协调器发来的回滚请求,通过 XID 和 Branch ID 找到相应的回滚日志记录,通过回滚记录生成反向的更新 SQL 并执行,以完成分支的回滚
TCC模式
seata也针对TCC做了适配兼容,支持TCC事务方案,原理前面已经介绍过,基本思路就是使用侵入业务上的补偿及事务管理器的协调来达到全局事务的一起提交及回滚。
实战
-
undolog表结构导入
核心在于对业务sql进行解析,转换成undolog,所以只要支持Fescar分布式事务的微服务数据都需要导入该表结构,我们在每个微服务的数据库中都导入下面表结构:
CREATE TABLE `undo_log` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `branch_id` bigint(20) NOT NULL, `xid` varchar(100) NOT NULL, `rollback_info` longblob NOT NULL, `log_status` int(11) NOT NULL, `log_created` datetime NOT NULL, `log_modified` datetime NOT NULL, `ext` varchar(100) DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_unionkey` (`xid`,`branch_id`) ) ENGINE=InnoDB AUTO_INCREMENT=200 DEFAULT CHARSET=utf8;
-
Fescar工程搭建
在所有微服务工程中,不一定所有工程都需要使用分布式事务,我们可以创建一个独立的分布式事务工程,指定微服务需要支持分布式事务的时候,直接依赖独立的分布式工程即可。
搭建一个changgou-transaction-fescar提供fescar分布式事务支持。
-
TM和ProxyDataSource
核心在于对业务sql进行解析,转换成undolog,并同时入库,此时需要创建一个代理数据源,用代理数据源来实现。
要想实现全局事务管理器,需要添加一个
@GlobalTransactional注解
,该注解需要创建一个解析器,GlobalTransactionScanner
,它是一个全局事务扫描器,用来解析带有@GlobalTransactional注解的方法,然后采用AOP的机制控制事务。每次微服务和微服务之间相互调用,要想控制全局事务,每次TM都会请求TC生成一个XID,每次执行下一个事务,也就是调用其他微服务的时候都需要将该XID传递过去,所以我们可以每次请求的时候,都获取头中的XID,并将XID传递到下一个微服务。
1.创建FescarAutoConfiguration类
a 创建代理数据库
b 全局事务扫描器
用来解析带有@GlobalTransactional注解的方法,然后采用AOP的机制控制事务
c 每次微服务和微服务之间相互调用
要想控制全局事务,每次TM都会请求TC生成一个XID,每次执行下一个事务,也就是调用其他微服务的时候都需要将该XID传递过去 所以我们可以每次请求的时候,都获取头中的XID,并将XID传递到下一个微服务
2.创建FescarRMRequestFilter,给每个线程绑定一个XID
3.创建FescarRestInterceptor过滤器,每次请求其他微服务的时候,都将XID携带过去
-
微服务添加依赖
因为所有微服务都有可能使用分布式事务,所以我们可以在每个微服务工程中添加fescar的依赖,当然,搜索工程排除,因为它不需要依赖数据库
-
在业务微服务上添加
@GlobalTransactional(name = "add")
(业务方法也叫add)注解即可
补充知识
分布式事务解决方案
1.基于XA协议的两阶段提交
2.TCC三段提交(2段,高效率[不推荐(补偿代码)])
3.本地消息(MQ+Table)
4.事务消息(RocketMQ[alibaba])
5.Seata(alibaba)
基于XA协议的两阶段提交(2PC)
两阶段提交协议(Two Phase Commitment Protocol)中,涉及到两种角色
一个事务协调者(coordinator):负责协调多个参与者进行事务投票及提交(回滚)
多个事务参与者(participants):即本地事务执行者
总共处理步骤有两个
(1)投票阶段(voting phase):协调者将通知事务参与者准备提交或取消事务,然后进入表决过程。参与者将告知协调者自己的决策:同意(事务参与者本地事务执行成功,但未提交)或取消(本地事务执行故障);
(2)提交阶段(commit phase):收到参与者的通知后,协调者再向参与者发出通知,根据反馈情况决定各参与者是否要提交还是回滚;
优点: 尽量保证了数据的强一致,适合对数据强一致要求很高的关键领域。
缺点: 牺牲了可用性,对性能影响较大,不适合高并发高性能场景,如果分布式系统跨接口调用,目前 .NET 界还没有实现方案。
补偿事务(TCC)
TCC 将事务提交分为 Try(method1) - Confirm(method2) - Cancel(method3) 3个操作。其和两阶段提交有点类似,Try为第一阶段,Confirm - Cancel为第二阶段,是一种应用层面侵入业务的两阶段提交。
操作方法 | 含义 |
---|---|
Try | 预留业务资源/数据效验 |
Confirm | 确认执行业务操作,实际提交数据,不做任何业务检查,try成功,confirm必定成功,需保证幂等 |
Cancel | 取消执行业务操作,实际回滚数据,需保证幂等 |
其核心在于将业务分为两个操作步骤完成。不依赖 RM 对分布式事务的支持,而是通过对业务逻辑的分解来实现分布式事务
A要向 B 转账,思路大概是:
我们有一个本地方法,里面依次调用
1、首先在 Try 阶段,要先调用远程接口把 B和 A的钱给冻结起来。
2、在 Confirm 阶段,执行远程调用的转账的操作,转账成功进行解冻。
3、如果第2步执行成功,那么转账成功,如果第二步执行失败,则调用远程冻结接口对应的解冻方法 (Cancel)。
1、当所有try()方法均执行成功时,对全局事物进行提交,即由事物管理器调用每个微服务的confirm()方法
2、 当任意一个方法try()失败(预留资源不足,抑或网络异常,代码异常等任何异常),由事物管理器调用每个微服务的cancle()方法对全局事务进行回滚
优点: 跟2PC比起来,实现以及流程相对简单了一些,但数据的一致性比2PC也要差一些
缺点: 缺点还是比较明显的,在2,3步中都有可能失败。TCC属于应用层的一种补偿方式,所以需要程序员在实现的时候多写很多补偿的代码,在一些场景中,一些业务流程可能用TCC不太好定义及处理。
本地消息表(异步确保)
本地消息表这种实现方式应该是业界使用最多的,其核心思想是将分布式事务拆分成本地事务进行处理,这种思路是来源于ebay。我们可以从下面的流程图中看出其中的一些细节:
基本思路就是:
消息生产方,需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面。然后消息会经过MQ发送到消息的消费方。如果消息发送失败,会进行重试发送。
消息消费方,需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,如果处理失败,那么就会重试执行。如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。
生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。
这种方案遵循BASE理论,采用的是最终一致性,笔者认为是这几种方案里面比较适合实际业务场景的,即不会出现像2PC那样复杂的实现(当调用链很长的时候,2PC的可用性是非常低的),也不会像TCC那样可能出现确认或者回滚不了的情况。
优点: 一种非常经典的实现,避免了分布式事务,实现了最终一致性。在 .NET中 有现成的解决方案。
缺点: 消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理。
MQ 事务消息
有一些第三方的MQ是支持事务消息的,比如RocketMQ,他们支持事务消息的方式也是类似于采用的二阶段提交,但是市面上一些主流的MQ都是不支持事务消息的,比如 RabbitMQ 和 Kafka 都不支持。
以阿里的 RocketMQ 中间件为例,其思路大致为:
第一阶段Prepared消息,会拿到消息的地址。
第二阶段执行本地事务,第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。
也就是说在业务方法内要想消息队列提交两次请求,一次发送消息和一次确认消息。如果确认消息发送失败了RocketMQ会定期扫描消息集群中的事务消息,这时候发现了Prepared消息,它会向消息发送者确认,所以生产方需要实现一个check接口,RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。
优点: 实现了最终一致性,不需要依赖本地数据库事务。
缺点: 目前主流MQ中只有RocketMQ支持事务消息。
分库分表mycat
订单根据用户id进行分库分表
mycat了解
MyCat原理介绍
MyCat原理中最重要的一个动词就是 “拦截”, 它拦截了用户发送过来的SQL语句, 首先对SQL语句做一些特定的分析,如分片分析、路由分析、读写分离分析、缓存分析等,然后将此SQL语句发往后端的真实数据库,并将返回的结果做适当处理,最终再返回给用户
MyCAT相关配置
server.xml中定义用户以及系统相关变量,如端口等。
schema.xml中定义逻辑库,表、分片节点等内容;
rule.xml中定义分片规则;
常用分片规则
-
取模(订单)
对分片字段取模运算.是水平分表常用的规则
-
分片枚举
通过在配置文件中配置可枚举的 id,自己配置分片, 本规则适用于特定的场景, 比如某些业务需要按照省份区县来做保存,二全国的省份区县是固定的,这类业务使用本条规则
-
范围约定
该分片适用于,提前规划好某个范围属于哪个分片
-
按日期 ( 天 ) 分片
-
全局序列
在实现分库分表的情况下, 数据库自增主键已经无法保证自增主键的全局唯一,为此 MyCat 提供了全局 sequence,并且提供了包产本地配置和数据库配置等多种实现方式
-
数据库方式 【常用】
利用数据库中的一个表来进行计数累加,但不是每次生成序列都读取数据库,这样效率太低,MyCat 会预加载一部份号段到 MyCat 内存中,这样大部分读写都是在内存中完成的。如果内存中的段号用完了 MyCat 会从数据库中再次索要。
如果 MyCat 宕机,内存中的段号丢失,MyCat 重新启动后会向数据库申请新的段号,原有段号会弃用。这样 MyCat 宕机后只是损失当前未用完的号码,而不会出现重复的主键
-
时间戳方式
优点: 配置简单
缺点: 18 位 有点长
-
自主生成
在 java 项目中自己生产全局序列:
- 根据业务逻辑组合
- 利用 redis 单线程原子性 incr 来生成序列
MyCat跨库Join
-
全局表
每个企业级的系统中, 都会存在一些系统的基础信息表, 类似于字典表、省份、城市、区域、语言表等, 这些表与业务表之间存在关系, 但不是业务主从关系,而是一种属性关系。当我们对业务表进行分片处理时, 可以将这些基础信息表设置为全局表, 也就是在每个节点中都存在该表。
全局表的特性如下:
A. 全局表的insert、update、delete操作会实时地在所有节点同步执行, 保持各个节点数据的一致性
B. 全局表的查询操作会从任意节点执行,因为所有节点的数据都一致
C. 全局表可以和任意表进行join操作
<table name="dict_order_type" dataNode="dn1,dn2" type="global"></table>
-
ER表
MyCat提出了基于ER关系的数据分片策略 , 子表的记录与其所关联的父表的记录存放在同一个数据分片中, 通过表分组(Table Group)保证数据关联查询不会跨库操作。
<table name="orders" dataNode="dn1,dn2" rule="mod_rule"> <childTable name="orders_detail" primaryKey="id" joinKey="orde_id" parentKe y="id" /> </table>
-
Catlet
ShareJoin 是Catlet的实现, 是一个简单的跨分片Join, 目前支持两个表的Join,原理就是解析SQL语句, 拆分成单表的语句执行, 单后把各个节点的数据进行汇集。
要想使用Catlet完成join, 还需要借助于MyCat中的注解, 在执行SQL语句时,使用catlet注解:
/*!mycat:catlet=demo.catlets.ShareJoin */ select a.id as aid , a.id , b.id as bid , b.name as name from customer a, company b where a.company_id=b.id and a.id = 1;
写在前面的关于数据分库分表的一些建议
- 达到一定数量级才拆分(800万);
- 不到800万但跟大表(超800万的表)有关联查询的表也要拆分,在此称为大表关联表;
- 大表关联表如何拆:小于100万的使用全局表;大于100万小于800万跟大表使用同样的拆分策略(er表);无法跟大表使用相同规则的,可以考虑从java代码上分步骤查询,不用关联查询,或者破例使用全局表;
- 【真实案例】破例的全局表:如item_sku表250万,跟大表关联了,又无法跟大表使用相同拆分策略,也做成了全局表。破例的全局表必须满足的条件:没有太激烈的并发update,如多线程同时update同一条id=1的记录。虽有多线程update,但不是操作同一行记录的不在此列。多线程update全局表的同一行记录会死锁。批量insert没问题;
- 拆分字段是不可修改的;
- 拆分字段只能是一个字段,如果想按照两个字段拆分,必须新建一个冗余字段,冗余字段的值使用两个字段的值拼接而成(如大区+年月拼成zone_yyyymm字段);
- 拆分算法的选择和合理性评判:按照选定的算法拆分后每个库中单表不得超过800万;
- 能不拆的就尽量不拆。如果某个表不跟其他表关联查询,数据量又少,直接不拆分,使用单库即可。
分库分表总结
垂直分表:可以把一个宽表的字段按访问频次、是否是大字段的原则拆分为多个表,这样既能使业务清晰,还能提升部分性能。拆分后,尽量从业务角度避免联查,否则性能方面将得不偿失。
垂直分库:可以把多个表按业务耦合松紧归类,分别存放在不同的库,这些库可以分布在不同服务器,从而使访问压力被多服务器负载,大大提升性能,同时能提高整体架构的业务清晰度,不同的业务库可根据自身情况定制优化方案。但是它需要解决跨库带来的所有复杂问题。
水平分库:可以把一个表的数据(按数据行)分到多个不同的库,每个库只有这个表的部分数据,这些库可以分布在不同服务器,从而使访问压力被多服务器负载,大大提升性能。它不仅需要解决跨库带来的所有复杂问题,还要解决数据路由的问题(数据路由问题后边介绍)。
水平分表:可以把一个表的数据(按数据行)分到多个同一个数据库的多张表中,每个表只有这个表的部分数据,这样做能小幅提升性能,它仅仅作为水平分库的一个补充优化。
垂直切分
优点:解决业务系统层面的耦合,业务清晰;与微服务治理类似,可以对不同业务的数据进行分级管理、维护、监控、扩展等;高并发场景下垂直切分一定程度提升IO、数据库连接、单机硬件资源、网络资源等。
缺点:分库后无法进行join,只能通过接口聚合的方式解决,提升了开发的复杂度;分库后分布式事务变得更加复杂,需要同时操作多个数据库保持事务原子性。依然存在单表数据量过大的问题(需要进行水平切分)
水平切分
优点:不存在单表数据量过大、高并发的性能瓶颈,提升系统稳定性和负载能力应用端改造较小,不需要进行业务模块拆分
缺点:夸分片的事务一致性难以保证;夸库的join关联查询性能差;数据多次扩展难度和维护难度大
一般来说,在系统设计阶段就应该根据业务耦合松紧来确定垂直分库,垂直分表方案,在数据量及访问压力不是特别大的情况,首先考虑缓存、读写分离、索引技术等方案。若数据量极大,且持续增长,再考虑水平分库水平分表方案。
分库分表带来的问题
分布式事务问题
可以通过分布式事务中间件解决,具体要看业务需求,使用强一致性分布事务还是最终一致性事务。
跨节点关联查询join问题
全局表:就是系统中所有模块都依赖的一些表,改动比较小,为了避免跨库join查询,可以在每个数据库中保存一份
字段冗余:用一定空间换时间
数据组装:在系统层面分两次查询,先从第一次的结果集里面找出关联的数据id,再根据关联id查询关联数据,最后将获得的数据进行字段拼装。
全局主键避重问题
主键自增不合理,而且多库的情况下可能出现主键冲突问题。UUID能保证唯一性,但UUID是无序的,无法做到根据主键排序,可以考虑用雪花算法。
数据迁移问题
采用双写的方式,修改代码,所有涉及到分库分表的表的增、删、改的代码,都要对新库进行增删改。同时,再有一个数据抽取服务,不断地从老库抽数据,往新库写,
边写边按时间比较数据是不是最新的。
跨节点、排序、函数问题
跨节点多库查询时,会出现limit分页、order by 排序等问题,当排序字段是分片时可以根据分片规则快速定位到指定的分片。当排序字段为分片字段时,就变得复杂很多。需要先查询不同分片数据进行排序返回,然后将不同分片的数据汇总再次排序,最终返回数据。
分库分表之后,id 主键如何处理?
1.数据库自增长ID
这个就是说你的系统里每次得到一个 id,都是往一个库的一个表里插入一条没什么业务含义的数据,然后获取一个数据库自增的一个 id。拿到这个 id 之后再往对应的分库分表里去写入。
优点:非常简单,有序递增,方便分页和排序。
缺点:分库分表后,同一数据表的自增ID容易重复,无法直接使用(可以设置步长,但局限性很明显);性能吞吐量整个较低,如果设计一个单独的数据库来实现 分布式应用的数据唯一性,即使使用预生成方案,也会因为事务锁的问题,高并发场景容易出现单点瓶颈。
适用场景:单数据库实例的表ID(包含主从同步场景),部分按天计数的流水号等;分库分表场景、全系统唯一性ID场景不适用。
2.redis生成ID
通过Redis的INCR/INCRBY自增原子操作命令,能保证生成的ID肯定是唯一有序的,本质上实现方式与数据库一致。
优点:整体吞吐量比数据库要高。
缺点:Redis实例或集群宕机后,找回最新的ID值比较麻烦。
适用场景:比较适合计数场景,如用户访问量,订单流水号(日期+流水号)等
3.UUID
优点:性能非常高,本地生成,没有网络消耗;
缺点:UUID 太长了、占用空间大,作为主键性能太差了;
由于UUID 不具有有序性,会导致 B+ 树索引在写的时候有过多的随机写操作
适合的场景:如果你是要随机生成个什么文件名、编号之类的,你可以用 UUID,但是作为主键不建议用 UUID 的。
UUID.randomUUID().toString().replace("-", "") -> sfsdf23423rr234sfdaf
4.获取系统当前时间
这个就是获取当前时间即可,但是问题是,并发很高的时候,比如一秒并发几千,会有重复的情况,这个是肯定不合适的。基本就不用考虑了。
适合的场景:一般如果用这个方案,是将当前时间跟很多其他的业务字段拼接起来,作为一个 id,如果业务上你觉得可以接受,那么也是可以的。你可以将别的业务字段值跟当前时间拼接起来,组成一个全局唯一的编号。
5.snowflake 算法(雪花算法)
solr
介绍
Request-Handler(qt):
q: 查询字符串(必须的)。:表示查询所有;keyword:吕布 表示按关键字“吕布”查询
fq: filter query 过滤查询。
sort: 排序。格式如下:字段名 排序方式;如advertiserId desc 表示按id字段降序排列查询结果。
start,rows:表示查回结果从第几条数据开始显示,共显示多少条。
fl: field list。指定查询结果返回哪些字段。多个时以空格“ ”或逗号“,”分隔。不指定时,默认全返回。
df: default field默认的查询字段,一般默认指定。
Raw Query Parameters: wt: write type。指定查询输出结果格式,我们常用的有json格式与xml格式。在solrconfig.xml中定义了查询输出格式:xml、json、Python、ruby、PHP、phps、custom。
//SOLR_URL---声明一个连接solr的地址 ip:port/solr/库名
一个连接solr的对象
HttpSolrClient httpSolrClient = new HttpSolrClient.Builder(SOLR_URL).withConnectionTimeout(10000).withSocketTimeout(60000).build();
//新增
//1. 一个一个添加
SolrInputDocument doc=new SolrInputDocument();
doc.addField("id",1);
doc.addField("goods_name","尚品宅配");
httpSolrClient.add(doc);
httpSolrClient.commit();
httpSolrClient.close();
//2. 一个集合一个集合的添加
List<SolrInputDocument> docs=new ArrayList<>();
for (int i = 1; i <=5; i++) {
SolrInputDocument dox=new SolrInputDocument();
dox.addField("id",i);
dox.addField("goods_name","锤子手机"+i);
docs.add(dox);
}
httpSolrClient.add(docs);
httpSolrClient.commit();
httpSolrClient.close();
//指定库添加数据
//httpSolrClient.add("db1-core","docs");
//删除
// 根据ID删除
httpSolrClient.deleteById("1");
// 根据IDS删除
httpSolrClient.deleteById(Arrays.asList("1","2","3"));
// 根据ID删除指定库的数据
httpSolrClient.deleteById("db1-core","1");
// 全部删除
httpSolrClient.deleteByQuery("*:*");
httpSolrClient.commit();
httpSolrClient.close();
//查询
SolrQuery query = new SolrQuery();
//按条件单个查询:查询name为“陈龙”的user
query.set("q","name:陈龙" );
//按条件模糊查询:查询name包含“玉”的user
query.set("q","name:*玉*" );
//多条件查询:查询id为“1”,并且name为“陈龙”,或者age为“28”岁,或者爱好包含“钓鱼”的user
query.set("q","id:1 AND name:陈龙 OR age:28 OR hobby:钓鱼" );
//过滤查询,age为1到30岁的user
query.set("fq", "age:[1 TO 30]");
//以age降序
query.addSort("age", SolrQuery.ORDER.desc);
//设置分页:
//开始位置
query.setStart(0);
//每页3条
query.setRows(3);
QueryResponse response = httpSolrClient.query(CORE_NAME,query);
//查询结果
SolrDocumentList results = response.getResults();
mysql与solr数据一致性
一般我们通过事件来通知进行同步,保证数据库与solr的一致性。
比如 库存变化数据库同步问题
第一种:首先 当用户操作影响库存,我们对库存进行增减 ,直接修改了solr之后 发送一条消息 消息内容是通知后台xx商品库存被修改为xx
后台有个消费者专门针对库存变化进行消费,根据队列消息内容将修改后的库存同步到mysql。
第二种:也可以使用定时方式进行同步,直接操作redis后,并不立即更新,后台有一个任务每隔固定的时间,执行任务将solr中的数据同步到mysql中。
solr6.3.3
mysql-connector-java-5.1.45-bin.jar
solr 以 jetty 方式部署 (非tomcat)
* 1.将 mysql-connector-java-5.1.45-bin.jar 放到 ./dist下
* 2. 修改数据仓库下的配置文件 ./collocation1/conf/solrconfig.xml
在大约70行左右的范围 加入以下几行
<lib dir="${solr.install.dir:../../../..}/dist/" regex="solr-dataimporthandler-.*\.jar"/>
<lib dir="${solr.install.dir:../../../..}/dist/" regex="mysql-connector-java-\d.*\.jar" />
<requestHandler name="/dataimport" class="org.apache.solr.handler.dataimport.DataImportHandler">
<lst name="defaults">
<str name="config">data-config.xml</str>
</lst>
</requestHandler>
* 3. 再在 ./collocation1/conf 创建 data-config.xml文件, 内容如下:
<?xml version="1.0" encoding="UTF-8" ?>
<dataConfig>
<dataSource type="JdbcDataSource" name="test" driver="com.mysql.jdbc.Driver" url="jdbc:mysql://127.0.0.1:3306/test" user="root" password="1234"/>
<document>
<entity
pk="id"
dataSource="test"
name="t_product"
query="select id,name,alias_name,old_product_id,top_product_id,type from t_product where status = '1' "
deltaQuery="select id from t_product where update_dt > '${dataimporter.last_index_time}'"
deletedPkQuery="select id from t_product where status = 10"
deltaImportQuery="select id,name,alias_name,old_product_id,top_product_id,type from t_product where id='${dataimporter.delta.id}'"
>
<field name="id" column="id" />
<field name="name" column="name" />
<field name="alias_name" column="alias_name" />
<field name="old_product_id" column="old_product_id" />
<field name="top_product_id" column="top_product_id" />
<field name="type" column="type" />
</entity>
</document>
</dataConfig>
# 同步数据语句
query="select id,name,alias_name,old_product_id,top_product_id,type from t_product where status = '1' "
# 增量同步语句 只去 id 用于 只同步被修改的 根据 ${dataimporter.last_index_time}'
# 这里注意时间 update_dt 为int, dataimporter.last_index_time为 timestamp
deltaQuery="select id from t_product where FROM_UNIXTIME(update_dt , '%Y-%m-%d %H:%i:%S') > '${dataimporter.last_index_time}'"
# 拉取 增量同步需要修改的数据
deltaImportQuery="select id,name,alias_name,old_product_id,top_product_id,type from t_product where id='${dataimporter.delta.id}'"
# 同步需要删除的数据
deletedPkQuery="select id from t_product where status = 10"
* 4. 在 ./bin 的 solr.in.sh 内 修改 solr 的默认时区为 PRC, 大约在 60+ 行
# By default the start script uses UTC; override the timezone if needed
SOLR_TIMEZONE="Asia/Shanghai"
# 不生效的话 可以修改 ./bin/solr
vim solr
搜索 /UTC/
改为 Asia/Shanghai
* 5. 定时脚本修复
# 在索引之前先清除索引
clean=true
# 无论做什么操作 都提交
commit=true
# 更新那个实体对应 的 配置文件中的 实体
entity=t_product
# 无论做什么操作 都优化
optimize=false
# 是否开启 debug
debug=false
# 是否阻塞所有请求
synchronous=false
# 增量更新数据
http://localhost:8983/solr/collocation1/dataimport?command=delta-import&clean=false&commit=true&wt=json&indent=true&entity=t_product&verbose=false&optimize=false&debug=false&synchronous=false&id=1
# 全量跟新数据
http://localhost:8983/solr/collocation1/dataimport?command=full-import&clean=true&commit=true&wt=json&indent=true&entity=t_product&verbose=false&optimize=false&debug=false&synchronous=false
# 重载配置
http://localhost:8983/solr/collocation1/dataimport?command=reload-config
# 查看运行的统计状态
http://localhost:8983/solr/collocation1/dataimport?command=status
# 查看配置
http://localhost:8983/solr/collocation1/dataimport?command=show-config
tips: 一定要注意改对文件(注意文件位置)
ps -aux | grep solr 可以查看 solr 启动的各种配置项.
中文分词器
中文分词器IKAnalyzer
ext.dic文件中添加自定义词组
数据导入
1.solr_home\server\solr\new_core\conf新建my-data-config.xml文件
配置连接的数据库跟要导入的表
2.用solr添加数据库字段对应的索引字段(不要加id字段)
3.打开该路径下文件:solr_home\server\solr\new_core\conf\solrconfig.xml
随便找一个与requestHandler同级节点上添加导入数据库的handle的配置
4.将solr_home\dist目录下的solr-dataimporthandler-7.5.0.jar和MySQL驱动(随便找个MySQL驱动)复制到solr_home\server\solr-webapp\webapp\WEB-INF\lib目录下。
5.重启solr服务
6.打开solr页面
Dataimport–>clean(√)、comit(√)–>Execute -->Refresh
7.检测数据是否导入成功
FastDFS
使用分布式文件系统FastDFS实现图片上传。
FastDFS两个主要的角色:Tracker Server 和 Storage Server 。
- Tracker server 作用是负载均衡和调度 ,被称为追踪服务器或调度服务器。Storage server 作用是文件存储
- Group:文件组,多台Storage Server的集群。上传一个文件到同组内的一台机器上后,FastDFS会将该文件即时同步到同组内的其它所有机器上,起到备份的作用。不同组的服务器,保存的数据不同,而且相互独立,不进行通信。
- Tracker Cluster:跟踪服务器的集群,有一组Tracker Server(跟踪服务器)组成。
- Storage Cluster :存储集群,有多个Group组成。
上传流程
1.Client通过Tracker server查找可用的Storage server。
2.Tracker server向Client返回一台可用的Storage server的IP地址和端口号。
3.Client直接通过Tracker server返回的IP地址和端口与其中一台Storage server建立连接并进行文件上传。
4.上传完成,Storage server返回Client一个文件ID,文件上传结束。
下载流程
1.Client通过Tracker server查找要下载文件所在的的Storage server。
2.Tracker server向Client返回包含指定文件的某个Storage server的IP地址和端口号。
3.Client直接通过Tracker server返回的IP地址和端口与其中一台Storage server建立连接并指定要下载文件。
4.下载文件成功。
private static TrackerClient client = null;
private static String httpPort = null;
/***
* 初始化fastdfs服务
*/
static {
try {
//获取tracker的配置文件fdfs_client.conf的位置
String filePath = new ClassPathResource("fdfs_client.conf").getPath();
//加载tracker配置信息
ClientGlobal.init(filePath);
} catch (Exception e) {
e.printStackTrace();
}
}
//所有的跟踪服务器信息在TrackerClient对象中。
//知道所有的跟踪服务器组
//创建TrackerClient客户端对象
TrackerClient trackerClient = new TrackerClient();
//从跟踪服务器组拿到一个具体的跟踪服务器
//通过TrackerClient获取TrackerServer对象
TrackerServer trackerServer = trackerClient.getConnection();
//即使传入的存储服务器对象为null也会自动分配
//也可以指定具体存储服务器对象 一般为null
//通过TrackerServer创建StorageClient
StorageClient storageClient = new StorageClient(trackerServer,null);
/****
* 文件上传
* @param file : 要上传的文件信息封装->FastDFSFile
* @return String[]
* 1:文件上传所存储的组名
* 2:文件存储路径
*group1/M00/00/12/wKgU_F9yra6AUyizAACoxG3uRvc315.jpg group1-存储服务器
* 获取文件信息
* @param groupName:组名 @param remoteFileName:文件存储完整名
* storageClient.get_file_info(groupName,remoteFileName);
*/
//执行文件上传
//bytes--上传的数组集合的名字
//extName--获取文件后缀名
//meta_list--元数据列表,(类似写的注释)上传文件习惯把上传日期,文件大小,文件真实名
String[] uploadResults = storageClient.upload_file(bytes, extName, meta_list);
/***
* 文件下载
*/
//通过StorageClient下载文件
byte[] fileByte = storageClient.download_file(groupName, remoteFileName);
//将字节数组转换成字节输入流
return new ByteArrayInputStream(fileByte);
/***
* 文件删除实现
*/
//表示删除文件,0成功,2失败
int i = storageClient.delete_file(groupName,remoteFileName);
/***
* 生成HttpUrl 用于图片展示等
* 得到http服务端口
* httpPort = ClientGlobal.getG_tracker_http_port() + "";
*/
String url ="http://"+trackerServer.getInetSocketAddress().getHostString()+":"+httpPort+"/"+path;
/***
* 获取组信息
*/
//通过trackerClient获取Storage组信息
return trackerClient.getStoreStorage(trackerServer,groupName);
/***
* 根据文件组名和文件存储路径获取Storage服务的IP、端口信息
*/
//获取服务信息
return trackerClient.getFetchStorages(trackerServer,groupName,remoteFileName);
linux常用命令
目录操作命令
目录切换 cd
命令:cd 目录
cd / 切换到根目录
cd /usr 切换到根目录下的usr目录
cd …/ 切换到上一级目录 或者 cd …
cd ~ 切换到home目录
cd - 切换到上次访问的目录
目录查看 ls [-al]
命令:ls [-al]
ls 查看当前目录下的所有目录和文件
ls -a 查看当前目录下的所有目录和文件(包括隐藏的文件)
ls -l 或 ll 列表查看当前目录下的所有目录和文件(列表查看,显示更多信息)
ls /dir 查看指定目录下的所有目录和文件 如:ls /usr
创建目录 mkdir
命令:mkdir 目录
mkdir aaa 在当前目录下创建一个名为aaa的目录
mkdir /usr/aaa 在指定目录下创建一个名为aaa的目录
删除目录或文件 rm
命令:rm [-rf] 目录
删除文件:
rm 文件 删除当前目录下的文件
rm -f 文件 删除当前目录的的文件(不询问)
删除目录:
rm -r aaa 递归删除当前目录下的aaa目录
rm -rf aaa 递归删除当前目录下的aaa目录(不询问)
全部删除:
rm -rf * 将当前目录下的所有目录和文件全部删除
rm -rf /* 【自杀命令!慎用!慎用!慎用!】将根目录下的所有文件全部删除
目录修改 mv 和 cp
一、重命名目录
命令:mv 当前目录 新目录
例如:mv aaa bbb 将目录aaa改为bbb
二、剪切目录
命令:mv 目录名称 目录的新位置
示例:将/usr/tmp目录下的aaa目录剪切到 /usr目录下面 mv /usr/tmp/aaa /usr
三、拷贝目录
命令:cp -r 目录名称 目录拷贝的目标位置 -r代表递归
示例:将/usr/tmp目录下的aaa目录复制到 /usr目录下面 cp /usr/tmp/aaa /usr
搜索目录 find
命令:find 目录 参数 文件名称
示例:find /usr/tmp -name ‘a*’ 查找/usr/tmp目录下的所有以a开头的目录或文件
文件操作命令
新建文件 touch
命令:touch 文件名
示例:在当前目录创建一个名为aa.txt的文件 touch aa.txt
删除文件 rm
命令:rm -rf 文件名
修改文件 vi或vim
vi可以分为三种状态,分别是命令模式、插入模式和底行模式
-
命令行模式command mode)
【1】控制光标移动:↑,↓,j
【2】删除当前行:dd
【3】查找:/字符
【4】进入编辑模式:i/o/ a
【5】进入底行模式:: -
编辑模式(Insert mode)
【1】ESC 退出编辑模式到命令行模式; -
底行模式(last line mode)
【1】退出编辑: :q
【2】强制退出: :q!
【3】保存并退出: :wq
文件的查看
文件的查看命令:cat/more/less/tail
cat:看最后一屏
more:百分比显示
less:翻页查看
tail:指定行数或者动态查看
示例:tail -10 sudo.conf
压缩文件操作
打包和压缩
命令:tar -zcvf 打包压缩后的文件名 要打包的文件
示例:打包并压缩/usr/tmp 下的所有文件 压缩后的压缩包指定名称为xxx.tar
tar -zcvf ab.tar aa.txt bb.txt 或:tar -zcvf ab.tar *
解压
命令:tar [-zxvf] 压缩文件
示例:将/usr/tmp 下的ab.tar解压到根目录/usr下
tar -xvf ab.tar -C /usr------C代表指定解压的位置
系统服务
service iptables status --查看iptables服务的状态
service iptables start --开启iptables服务
service iptables stop --停止iptables服务
service iptables restart --重启iptables服务
chkconfig iptables off --关闭iptables服务的开机自启动
chkconfig iptables on --开启iptables服务的开机自启动
其它
查看当前目录--------pwd 查看当前目录路径
查看进程--------进程号命令:ps -ef 查看所有正在运行的进程
结束进程 --------命令:kill pid(pid:进程号) 或者 kill -9 pid(强制杀死进程)
网络通信命令:
1.查看网卡信息 --------命令:ifconfig 或 ifconfig | more
2.查看与某台机器的连接情况 --------命令:ping ip
3.netstat -an:查看当前系统端口
搜索指定端口 --------命令:netstat -an | grep 8080
配置网络 --------命令:setup
重启网络 --------命令:service network restart
切换用户 --------命令:su - 用户名
关闭防火墙 --------命令:chkconfig iptables off
修改文件权限 --------命令:chmod 777
清屏 --------命令:ctrl + l
vi模式下快捷键
esc后:
保存并退出快捷键:shift+z+z
光标跳到最后一行快捷键:shift+g
删除一行:dd
复制一行内容:y+y
粘贴复制的内容:p
rpm -e --nodeps mysql-libs-5.1.71-1.el6.x86_64 卸载
rpm -ivh MySQL-server-5.6.34-1.rhel5.x86_64.rpm 安装
yum install glibc.i686 安装
source /etc/profile 使更改的配置立即生效
tail 命令tail [参数] [文件]
可用于查看文件的内容,有一个常用的参数 -f 常用于查阅正在改变的日志文件。