springboot启动流程

springboot 启动原理
参考文档:https://mp.weixin.qq.com/s/1yHtaoSqqIJItIXByvV5QA

@SpringBootApplication注解与@EnableAutoConfiguration与AutoConfigurationImportSelector.class 与 SpringFactoriesLoader#

@SpringBootApplication = @Configuration+ @EnableAutoConfiguration+ @ComponentScan

@Configuration#

JavaConfig形式的Spring Ioc容器的配置类,等同于xml方式的

@ComponentScan:#

自动扫描并加载符合条件的组件(比如@Component和@Repository等)或者bean定义,最终将这些bean定义加载到IoC容器中。

@EnableAutoConfiguration#

借助@Import的支持,收集和注册特定场景相关的bean定义。
@AutoConfigurationPackage + @Import(AutoConfigurationImportSelector.class)

AutoConfigurationPackage将 添加该注解的类所在的package 作为 自动配置package 进行管理。

@Import(EnableAutoConfigurationImportSelector.class),借助EnableAutoConfigurationImportSelector,@EnableAutoConfiguration可以帮助SpringBoot应用将所有符合条件的@Configuration配置都加载到当前SpringBoot创建并使用的IoC容器。借助于Spring框架原有的一个工具类:SpringFactoriesLoader的支持。实现智能的自动配置。

SpringFactoriesLoader:
SpringFactoriesLoader属于Spring框架私有的一种扩展方案,其主要功能就是从指定的配置文件META-INF/spring.factories加载配置。
配合@EnableAutoConfiguration使用的话,它更多是提供一种配置查找的功能支持,即根据@EnableAutoConfiguration的完整类名org.springframework.boot.autoconfigure.EnableAutoConfiguration作为查找的Key,获取对应的一组@Configuration类。

so:
@EnableAutoConfiguration自动配置的魔法骑士就变成了:从classpath中搜寻所有的META-INF/spring.factories配置文件,并将其中org.springframework.boot.autoconfigure.EnableutoConfiguration对应的配置项通过反射(Java Refletion)实例化为对应的标注了@Configuration的JavaConfig形式的IoC容器配置类,然后汇总为一个并加载到IoC容器。

Springboot启动执行详细流程#

  1. SpringApplication初始化
    A.根据classpath里面是否存在某个特征类(org.springframework.web.context.ConfigurableWebApplicationContext)来决定是否应该创建一个为Web应用使用的ApplicationContext类型。
    B.使用SpringFactoriesLoader在应用的classpath中查找并加载所有可用的ApplicationContextInitializer。
    C.使用SpringFactoriesLoader在应用的classpath中查找并加载所有可用的ApplicationListener。
    D.推断并设置main方法的定义类。

  2. SpringApplication run()
    A.加载所有SpringApplicationRunListener。调用它们的started()方法
    SpringApplicationRunListeners listeners = getRunListeners(args); listeners.starting(bootstrapContext, this.mainApplicationClass);
    B. 创建并配置当前Spring Boot应用将要使用的Environment(包括配置要使用的PropertySource以及Profile)。
    ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments); configureIgnoreBeanInfo(environment);
    C.遍历调用所有SpringApplicationRunListener的environmentPrepared()的方法,告诉他们:“当前SpringBoot应用使用的Environment准备好了咯!”
    D. 如果SpringApplication的showBanner属性被设置为true,则打印banner
    E. 根据用户是否明确设置了applicationContextClass类型以及初始化阶段的推断结果,决定该为当前SpringBoot应用创建什么类型的ApplicationContext并创建完成
    context = createApplicationContext(); F. 将之前准备好的Environment设置给创建好的ApplicationContext使用, 将遍历调用ApplicationContextInitializer的initialize(applicationContext)方法来对已经创建好的ApplicationContext进行进一步的处理 context.setEnvironment(environment);
    postProcessApplicationContext(context); applyInitializers(context);
    G. 遍历调用所有SpringApplicationRunListener的contextPrepared()方法。
    listeners.contextPrepared(context); H. 注册特殊定义bean(springApplicationArguments、springBootBanner), 添加LazyInitializationBeanFactoryPostProcessor,创建并初始化BeanDefinitionLoader(主要做的工作就是注册bean) I. 遍历调用所有SpringApplicationRunListener的contextLoaded()方法。 listeners.contextLoaded(context);
    J. 调用ApplicationContext的refresh()方法,完成IoC容器可用的最后一道工序.
    applicationContext.refresh(); K.执行ApplicationRunners,CommandLineRunners callRunners(context, applicationArguments);
    L.遍历执行SpringApplicationRunListener的started()方法

Springboot启动简要流程#

  1. SpringApplication 初始化,收集所有ApplicationContextInitializer和ApplicationListener。
  2. SpringApplication run
    调用started事件
    创建并准备Environment(PropertySource、Profile)
    调用environmentPrepared事件
    创建ApplicationContext,遍历调用ApplicationContextInitializer的initialize方法
    调用contextPrepared 事件
    创建并配置BeanDefinitionLoader
    调用contextLoaded 事件
    applicationContext的refresh()方法, 完成ioc容器初始化
    调用started事件
    callRunners(ApplicationRunners,CommandLineRunners)
    调用running事件

总结#

Springboot的启动,主要创建了配置环境(environment)、事件监听(listeners)、应用上下文(applicationContext),并基于以上条件,在容器中开始实例化我们需要的Bean
自动装配核心:@EnableAutoConfiguration中的AutoConfigurationImportSelector中的SpringFactoriesLoader 提供一种配置查找的功能支持,即根据@EnableAutoConfiguration的完整类名org.springframework.boot.autoconfigure.EnableAutoConfiguration作为查找的Key,获取对应的一组@Configuration类。并在后续加载到ioc容器。

Innodb 锁

Innodb支持哪些锁#

按兼容性分为 共享锁(Shared Locks,S锁)、排他锁(Exclusive Locks,X锁)
按锁范围分为:表级锁、行级锁
表锁:表共享读锁(Table Read Lock)、表排他写锁(Table Write Lock)、意向共享锁(intention shared lock, IS锁)、意向排他锁(intention exclusive lock, IX锁)
行锁:记录锁(Record Locks)、间隙锁(Gap Locks)、临键锁(Next-Key Locks)、插入意向锁(Insert Intention Locks)

共享锁(S)、排他锁(X)#

锁的模式,每种锁都有shared和exclusive两种模式。
特性:X锁与其他X,S锁不兼容,S与S锁兼容

锁定读#

SELECT … FOR SHARE 和 SELECT … FOR UPDATE
这两种锁定读在搜索时所遇到的每一条索引记录(index record)上设置共享锁或排它锁。

意向锁(IS、IX)#

意向锁,协调行锁和表锁之间的关系的,实现了“表锁是否冲突”的快速判断。协调表的读写锁和行的读写锁(不同粒度锁)之间关系。
场景:获取行的S锁,需要先获取表的IS锁;获取行的X锁,需要先获取表的IX锁。
注:与意向锁冲突的不是行的X锁,是表的X锁!
IX 与表的 X 和 S 冲突,意思当前表有行锁X锁,不能上表的X或S锁。
IS 与表的 X 冲突,意思当前表有行锁S锁,不能上表的X锁。
why意向锁:“表锁是否冲突”的快速判断。否则需要遍历表去判断行锁的存在,才能判断冲突。

索引记录锁(Record Locks)#

行锁,锁定的是索引记录。所谓的“锁定某个行”或“在某个行上设置锁”。就是在某个索引的特定索引记录(或称索引条目、索引项、索引入口)上设置锁。有shared或exclusive两种模式

间隙锁(Gap Locks)#

锁定尚未存在的记录,即索引记录之间的间隙。有shard或exclusive两种模式,但,两种模式没有任何区别,二者等价。
gap lock 之间可以共存 ! 两个事务可以共同持有。gap只阻塞插入意向锁。
why gap lock: 唯一目的就是阻止其他事务向gap中插入数据行。解决 phantom row问题。
gap lock与 插入意向锁冲突。rc 没有gap lock, rr使用gap与插入意向锁的冲突解决幻读。

下一个键锁(Next-Key Locks)#

next-key lock 是 (索引记录上的索引记录锁) + (该索引记录前面的间隙上的锁) 二者的合体,它锁定索引记录以及该索引记录前面的间隙。有shard或exclusive两种模式。
next-key lock 带的 gap lock固定锁记录前面的间隙!

插入意向锁(Insert Intention Locks)#

特殊的gap lock。
插入行之前,INSERT操作会首先在索引记录之间的间隙上设置insert intention lock,该锁的范围是(插入值, 向下的一个索引值)。有shard或exclusive两种模式,但,两种模式没有任何区别,二者等价。
插入意向锁 会和 gap锁冲突!
插入意向锁 本身多个不会冲突!插入意向锁也不会阻塞gap锁,只有先gap, 再insert Intention Lock 会阻塞。
why插入意向锁: 隔离级别为RR时正是利用插入意向锁与gap锁的冲突,来解决幻读问题。

不同索引加锁情况#

  1. 聚簇索引
    索引命中:对这个 id 聚簇索引加 X 锁
    索引未命中:对 id 这个聚簇索引加 GAP 锁,GAP的范围是两个索引的间隙
    范围条件: 对范围区间内命中的id聚簇索引加Next-Key 锁,即左开右闭的GAP锁+X锁。eg: UPDATE student SET age = 100 WHERE id < 3
  2. 唯一索引
    索引命中:二级索引的叶子节点中保存了主键索引的位置,在给二级索引加锁的时候,对应聚簇索引也会一并加锁。加两个X锁。
    索引未命中:只会在二级索引加GAP锁,不会在聚簇索引上加锁。
    范围条件:对范围区间内命中的唯一索引加Next-Key 锁(对条件范围内的索引加GAP锁+X锁),并且对命中索引对应的聚簇索引也会加 X锁。
  3. 非唯一索引
    索引命中:对命中的非唯一索引加 Next-Key锁,在非唯一索引相邻区间加 GAP 锁。并且对命中索引对应的聚簇索引加X锁。
    索引未命中: 只会在二级索引加GAP锁,不会在聚簇索引上加锁。
    范围条件:与唯一索引的范围条件加锁类似,对命中的索引加Next-Key锁(对条件范围内的索引加GAP锁+X锁),对应的聚簇索引加X锁。
  4. 无索引
    在没有索引的时候,只能走聚簇索引,对表中的记录进行全表扫描。会给所有记录加行锁,所有聚簇索引之间会加上 GAP 锁。

为什么无索引更新会锁全表?#

在没有索引的时候,只能走聚簇索引,对表中的记录进行全表扫描。会给所有记录加行锁,所有聚簇索引和聚簇索引之间还会加上 GAP 锁。
eg: UPDATE student SET age = 100 WHERE age = 33; age无索引。
why: RR 下要保证当前读时不出现幻读。需要锁全表!否则任何一个记录都可以变成age=33, 任何一个地方都可以insert一条age=33。再执行该sql的时候会发现多出记录了。

死锁场景#

死锁的可能性并不受隔离级别的影响。隔离级别改变的是读操作的行为,而死锁是由于写操作产生的。

  1. 两个行锁顺序不同的获取
    stu_no字段 是唯一索引_
    1
    2
    3
    4
    session1 : update student set age = 88 where stu_no = 1;
    session2 : update student set age = 99 where stu_no = 3;
    session1 : update student set age = 99 where stu_no = 3;
    session2 : update student set age = 88 where stu_no = 1;

死锁, 一个有stu_no=1的 X RECORD锁,一个有stu_no=3的。

  1. 两个gap锁各自阻塞另一个事务的插入意向锁
    stu_no字段 是唯一索引_

    1
    2
    3
    4
    session1 : delete from student where stu_no = 8;
    session2 : delete from student where stu_no= 7;
    session1 : insert student (stu_no) values (6);
    session2 : insert student (stu_no) values (9);
  2. 两个insert duplicate失败会加S RECORD, 若session1 commit, 两个返回duplicate-key error,若session1 rollback, 两个都想获取X锁,但对方都不释放S锁
    stu_no字段是唯一索引_

    1
    2
    3
    4
    5
    6
    session1: insert student (stu_no) values (6);
    session2: insert student (stu_no) values (6);
    session3: insert student (stu_no) values (6);
    session1: rollback;
    session2: commit;
    session3: commit;

参考文档:
https://mp.weixin.qq.com/s/IyeiP2t1TGxZlPqSPAWNZg
https://mp.weixin.qq.com/s/dRIfbVwAJfEuZ978VlyXIA
https://mp.weixin.qq.com/s/S9Fzwu7-g81DsWgjaARHvQ

线上稳定性保障

  1. 监控
    把各个环节的业务量级、成功失败原因、活动量级上报,这些数据是有因果关系的
    所以我们把这些数据汇总到大盘,提高问题定位的速度。

  2. 隔离
    区分业务,隔离餐团与买单业务,然后区分了环节,隔离查询与交易环节
    最终业务与业务隔离,交易与查询隔离,保障了系统的稳定性。

  3. 降级,首先是确认场景优先级,区分核心功能非核心功能,区分核心依赖非核心依赖

  • 系统容量问题,优先降级非核心功能,限流查询链路
  • 功能异常问题,按业务、场景类型进行降级,避免故障影响扩大
  1. 全链路压测
    需要定期进行全链路压测评估链路流量负载能力。
    压测前,会对压测流量进行ID偏移,防止污染线上,同步压测数据并进行状态改写。
    压测中读写分离,读线上数据,防止污染本地缓存,写影子表防止污染线上表。
    压测后扫描线上数据,确保无压测数据写到线上,然后清理压测数据。

总结:
事前准备,主要做链路梳理,明确强弱依赖,然后进行性能评估,主要进行容量评估和负载评估,然后完善监控,明确因果建设大盘,最后准备降级预案,区分场景,区分优先级。
事中快速响应,通过监控大盘定位问题,按预案进行扩容、限流、降级、熔断等操作。
事后进行性能优化,问题修复并进行复盘总结。

netty服务端启动原理

Netty 是一个事件驱动模式的高性能的 NIO 框架,对内封装了 Java NIO 的大量复杂且繁琐的实现细节,对外提供了高度抽象的组件和易用的 API,提供了针对市面上大部分通信协议的支持,是当下 Java 生态中最流行的远程通信框架之一。

Netty 框架本质上是对于 Java NIO 的封装,Java NIO 本质上是对于操作系统提供的 IO 多路复用功能的封装,因此其设计思路是一脉相承的,即采取了事件驱动的 Reactor 模式。

其中的 bossGroup 就是 mainReactor 的具体实现,主要用于监听服务端口,接收客户端的 TCP 连接请求。 workerGroup 就是 subReactor 的具体实现,主要用于消息的读取发送、编解码以及其他业务逻辑的处理。

它们本质上都可以视作一个线程池,里面的每个线程(NioEventLoop)都唯一绑定了一个 Java NIO 中的 Selector 对象,用于实现 IO 多路复用的功能,来监听多个(文件描述符)客户端的输入。

启动demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class DiscardServer {

private int port;

public DiscardServer(int port) {
this.port = port;
}

public void run() throws Exception {
// mainReactor(Acceptor)线程池,主要用于监听服务端口,处理客户端连接
EventLoopGroup bossGroup = new NioEventLoopGroup();
// subReactor线程池,主要用于实现对于消息的接收发送、编解码以及其他业务处理
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
// Netty的启动辅助类
ServerBootstrap b = new ServerBootstrap();
// 初始化ServerBootstrap实例
b.group(bossGroup, workerGroup)
// 指定Channel类型为NioServerSocketChannel
.channel(NioServerSocketChannel.class)
// 添加自定义Handler,会用于workerGroup中的Channel的处理
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new DiscardServerHandler());
}
})
// 添加Channel选项
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true);

// Bind and start to accept incoming connections.
// 服务端启动核心逻辑,初始化Channel并绑定指定服务端口开始监听
ChannelFuture f = b.bind(port).sync();

// Wait until the server socket is closed.
// In this example, this does not happen, but you can do that to gracefully
// shut down your server.
f.channel().closeFuture().sync();
} finally {
// 服务端关闭
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}

public static void main(String[] args) throws Exception {
int port = 8080;
if (args.length > 0) {
port = Integer.parseInt(args[0]);
}

new DiscardServer(port).run();
}
}
  1. 初始化: 创建 ServerBootstrap 实例并进行初始化
  2. 启动服务: ServerBootstrap 绑定服务端口并开始监听

分库分表组件原理

参考文档:
https://github.com/Meituan-Dianping/Zebra/wiki/%E6%95%B0%E6%8D%AE%E5%BA%93%E4%B8%AD%E9%97%B4%E4%BB%B6%E4%B8%BB%E6%B5%81%E8%AE%BE%E8%AE%A1

  1. 数据库中间件设计方案
    典型的数据库中间件设计方案有2种:
    服务端代理(proxy:代理数据库)、
    客户端代理(datasource:代理数据源)。
    下图演示了这两种方案的架构

可以看到不论是代理数据库还是代理数据源,底层都操作了多个数据库实例。不同的是:

服务端代理(proxy:代理数据库)中: 我们独立部署一个代理服务,这个代理服务背后管理多个数据库实例。而在应用中,我们通过一个普通的数据源(c3p0、druid、dbcp等)与代理服务器建立连接,所有的sql操作语句都是发送给这个代理,由这个代理去操作底层数据库,得到结果并返回给应用。在这种方案下,分库分表和读写分离的逻辑对开发人员是完全透明的。

客户端代理(datasource:代理数据源): 应用程序需要使用一个特定的数据源,其作用是代理,内部管理了多个普通的数据源(c3p0、druid、dbcp等),每个普通数据源各自与不同的库建立连接。应用程序产生的sql交给数据源代理进行处理,数据源内部对sql进行必要的操作,如sql改写等,然后交给各个普通的数据源去执行,将得到的结果进行合并,返回给应用。数据源代理通常也实现了JDBC规范定义的API,因此能够直接与orm框架整合。在这种方案下,用户的代码需要修改,使用这个代理的数据源,而不是直接使用c3p0、druid、dbcp这样的连接池

  1. 主流的数据库中间件实现对比
    无论是代理数据库,还是代理数据源,二者的作用都是类似的。以下列出了这两种方案目前已有的实现以及各自的优缺点:

数据库代理

目前的实现方案有:阿里巴巴开源的cobar,mycat团队在cobar基础上开发的mycat,mysql官方提供的mysql-proxy,奇虎360在mysql-proxy基础开发的atlas。目前除了mycat,其他几个项目基本已经没有维护。

优点:多语言支持。也就是说,不论你用的php、java或是其他语言,都可以支持。原因在于数据库代理本身就实现了mysql的通信协议,你可以就将其看成一个mysql 服务器。mysql官方团队为不同语言提供了不同的客户端驱动,如java语言的mysql-connector-java,python语言的mysql-connector-python等等。因此不同语言的开发者都可以使用mysql官方提供的对应的驱动来与这个代理服务器建通信。

缺点:实现复杂。因为代理服务器需要实现mysql服务端的通信协议,因此实现难度较大。

数据源代理
目前的实现方案有:阿里巴巴开源的tddl,大众点评开源的zebra,当当网开源的sharding-jdbc。

优点:更加轻量,可以与任何orm框架整合。这种方案不需要实现mysql的通信协议,因为底层管理的普通数据源,可以直接通过mysql-connector-java驱动与mysql服务器进行通信,因此实现相对简单。

缺点:仅支持某一种语言。例如tddl、zebra、sharding-jdbc都是使用java语言开发,因此对于使用其他语言的用户,就无法使用这些中间件。版本升级困难,因为应用使用数据源代理就是引入一个jar包的依赖,在有多个应用都对某个版本的jar包产生依赖时,一旦这个版本有bug,所有的应用都需要升级。而数据库代理升级则相对容易,因为服务是单独部署的,只要升级这个代理服务器,所有连接到这个代理的应用自然也就相当于都升级了。

ORM框架代理
目前有hibernate提供的hibernate-shards,也可以通过mybatis插件的方式编写。相对于前面两种方案,这种方案可以说是只有缺点,没有优点。

如何实现延迟队列

如何实现一个延迟队列?

(1) 数据库轮询#

例如对于订单支付失效要求比较高的,每2S扫表一次检查过期的订单进行主动关单操作
优点是简单
缺点是每分钟全局扫表,浪费资源,如果遇到表数据订单量即将过期的订单量很大,会造成关单延迟

(2) JDK的延迟队列#

一个无界阻塞队列,该队列只有在延迟期满的时候才能从中获取元素,放入DelayQueue中的对象,是必须实现Delayed接口的
优点:效率高,任务触发时间延迟低。
缺点:(1)服务器重启后,数据全部消失,怕宕机 (2)集群扩展相当麻烦 (3)因为内存条件限制的原因,比如下单未付款的订单数太多,那么很容易就出现OOM异常 (4)代码复杂度较高

(3) 时间轮算法#

时间轮算法可以类比于时钟,如上图箭头(指针)按某一个方向按固定频率轮动,每一次跳动称为一个 tick。这样可以看出定时轮由个3个重要的属性参数,ticksPerWheel(一轮的tick数),tickDuration(一个tick的持续时间)以及 timeUnit(时间单位),例如当ticksPerWheel=60,tickDuration=1,timeUnit=秒,这就和现实中的始终的秒针走动完全类似了。
如果当前指针指在1上面,我有一个任务需要4秒以后执行,那么这个执行的线程回调或者消息将会被放在5上。那如果需要在20秒之后执行怎么办,由于这个环形结构槽数只到8,如果要20秒,指针需要多转2圈。位置是在2圈之后的5上面(20 % 8 + 1)

优点:效率高,任务触发时间延迟时间比delayQueue低,代码复杂度比delayQueue低。
缺点:

  • 服务器重启后,数据全部消失,怕宕机
  • 集群扩展相当麻烦
  • 因为内存条件限制的原因,比如下单未付款的订单数太多,那么很容易就出现OOM异常

(4) redis zset实现#

利用redis的zset,zset是一个有序集合,每一个元素(member)都关联了一个score,通过score排序来取集合中的值。
zset常用命令
添加元素: ZADD key score member [[score member] [score member] …]
按顺序查询元素: ZRANGE key start stop [WITHSCORES]
查询元素score: ZSCORE key member
移除元素: ZREM key member [member …]

我们将订单超时时间戳与订单号分别设置为score和member,系统扫描第一个元素判断是否超时(zrange key 0 0 [withscores] )

存在一个致命的硬伤,在高并发条件下,多消费者会取到同一个订单号.
解决方案:
(1) 用分布式锁,但是用分布式锁,性能下降了,该方案不细说。
(2) 对ZREM的返回值进行判断,只有大于0的时候 (删除成功),才可消费数据

优点:
(1) 由于使用Redis作为消息通道,消息都存储在Redis中。如果发送程序或者任务处理程序挂了,重启之后,还有重新处理数据的可能性。
(2) 做集群扩展相当方便
(3) 时间准确度高
缺点:
(1) 需要额外进行redis维护

(5)使用消息队列#

我们可以采用rabbitMQ的延时队列。RabbitMQ具有以下两个特性,可以实现延迟队列

RabbitMQ可以针对Queue和Message设置 x-message-tt,来控制消息的生存时间,如果超时,则消息变为dead letter
lRabbitMQ的Queue可以配置x-dead-letter-exchange 和x-dead-letter-routing-key(可选)两个参数,用来控制队列内出现了deadletter,则按照这两个参数重新路由。

优缺点
优点: 高效,可以利用rabbitmq的分布式特性轻易的进行横向扩展,消息支持持久化增加了可靠性。缺点:本身的易用度要依赖于rabbitMq的运维.因为要引用rabbitMq,所以复杂度和成本变高

报表设计学习

es + clickhouse 搭配

总结#

es适合做全文检索需求,ch适合做大数据分析需求,本需求使用ch建立es视图表兼容es的维表,将维表数据和事实表数据分开处理,使用时连表查询,充分利用了两种引擎的特性,既支持全文检索,也保证了查询性能

es & clickhouse#

clickhouse除了不能全文检索,其他方面完全超过es, 尤其是在事实表,这种日志类型不断增加的业务类型上,主要在查询性能,稳定性,磁盘使用率 上要比es优秀很多,

因为ch本身的一个列式存储,编码压缩,向量化执行等特性。而且ch支持sql语言。

我们这个业务实际没有导入ch,而是ch上建立了一个es的视图表,从ch层兼容了es。

账户维度数据使用es,便于这块数据的更新,因为ch在update原数据方面支持的不算很好,且es支持全文检索
消耗事实数据使用ch,这部分量非常大,每个account每分钟一行, 且是纯insert, 适合ch处理。

es的存储特性,让他在全文检索的时候不能替代。

权限管理模型学习

本质上可以抽象出用户(User)、对象(Object)、行为(Action)和策略(Policy)的四个基本要素。策略(Policy)决定了用户(User)在一定的条件下对指定的对象(Object)有什么样的行为(Action)。

ACL#

ACL即访问控制列表,是基于资源的一种权限控制管理方式。实现原理非常简单,每一项资源都有一个访问列表,记录哪些用户可以对这个资源做CRUD的操作。当用户访问某个资源的时候,系统会检测用户是否在访问列表里,从而确定用户是否有对资源的操作权限

RBAC#

RBAC, 相比与ACL的直接用户上赋予权限

引入角色:
角色 —> 权限的集合,批量操作权限,权限可以统一分配和回收,角色可以和企业的岗位对应,能更加好理解和维护。

引入组织架构:
组织部门 —> 人员的集合, 批量操作人员 (eg: 赋予部门权限,或判断用户是否在组织中)

RBAC的特点如下:
优点是权限只和角色关联,可以统一分配和回收,使用简单,也降低了维护成本。角色可以和企业的岗位对应,能更加好理解和维护。
缺点是当某个角色下的某些用户需要做特别的权限限定,例如去除一些权限,就无能为力。另外,角色规划不好的情况下容易造成角色爆炸和混乱。

ABAC#

ABAC是基于属性的权限验证控制,动态的计算一组属性从而判断当前用户是否有操作权限。属性可以分为用户属性(岗位)、环境属性(公司、家里、时间)、对象属性(对象的类型,比如服务器、楼宇等)、操作属性(启动、创建、读取等)。一组属性可以叫做一个策略,策略可以配置在用户实体上,也可以配置在组织架构上

ABAC的特点如下:
非常灵活,可以根据不同的维度、粒度进行权限管控
无需预置判定逻辑,可以根据需要随时调整,在经常变化的系统中可以很好的支持
策略配置复杂时,会给管理者和问题追查时带来困难
策略判定需要实时计算,会带来性能损耗
从策略上不能很直观的看到人和对象的关系

个人总结:
ACL太复杂了,容易乱,操作起来级联严重
RABC, 适合公司类型,组织架构、人员角色的权限固定管理。
ABAC, 策略过于灵活,适合变化非常多的,策略复杂的

库存服务提性能

提性能#

读优化#

多级缓存:让数据靠近计算
链路优化:缩短耗时
周期库存⽅案:提⾼周期库存查询性能

写优化#

流量漏⽃:缓存预扣减少数据库的⽆效访问
分库分表:提升数据库的整体写⼊能⼒( 窄表设计下单分片物理机可承载4500,云服务器可承载3000,仅供参考,具体以业务测试为准 )
并⾏扣减:降低库存扣减的响应时间
分库存:解决单行更新能力不足问题,(一般单行更新的QPS在500以内)
合并请求:异步批量,将多次扣减累计计数,集中成一次扣减,从而实现了将串行处理变成了批处理。大大减轻更新压力。

当前峰值TPS超3000,年增⻓50%,未来5年期望⽀撑量级1万+;
综合以上分8库,部署2个集群,每个集群4个库

1、如果某个sku_id的库存扣减过热,单台实例支撑不了(mysql官方测评:一般单行更新的QPS在500以内),可以考虑将一个sku的大库存拆分成N份,放在不同的库中(也就是说所有子库的库存数总和才是一件sku的真实库存),由于前台的访问流量非常大,按照均分原则,每个子库分到的流量应该差不多。上层路由时只需要在sku_id后面拼接一个范围内的随机数,即可找到对应的子库,有效减轻系统压力。

2、单条sku库存记录更新过热,也可以采用批量提交方式,将多次扣减累计计数,集中成一次扣减,从而实现了将串行处理变成了批处理,也可以大大减轻数据库压力。

3、引入RocketMQ消息队列,经过前置校验后,如果有剩余库存,则把创建订单的操作封装成消息发送给MQ,订单系统从RocketMQ中以特定的频率消费,创建订单,该方案有一定的延迟性。

分布式锁

1、 基于MySQL的分布式锁实现
原理:直接创建一张锁表,然后通过操作该表中的数据来实现了.
。利用唯一索引的排他性来实现分布式锁,只有抢到锁的线程才能插入成功,释放锁删除该条记录即可

缺点:
1、强依赖数据库的可用性,存在单点问题。
2、数据库在高并发的场景下,性能上表现欠佳

2、基于Redis的分布式锁实现
可以通过set nx的特性来实现分布式锁

3、基于Zookeeper的分布式锁实现
可以利用临时有序节点的特性来实现分布式锁。
原理:1、先建一个目录lock 2、线程A想获取锁就在lock目录下创建一个带顺序的临时节点 3、然后获取比当前顺序号小的顺序号,获取到则获取锁失败,获取不到则获取锁成功。 4、解锁则是删除这个临时节点。

临时节点是根据客户端维护的session会话来维持生命周期的,当客户端不在维护session时,或者说客户端断开连接时,session消失,那么临时节点就会消失,这种特性正好满足了分布式锁要有过期时间的需求,不会产生死锁问题

缺点:抗不了多少并发,内存无法横向扩展

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×