wenmo8 发布的文章

本文分享自华为云社区
《【华为云MySQL技术专栏】TaurusDB透明压缩》
,作者: GaussDB 数据库。

背景介绍

某一部分特定比例的客户群体,对数据库的读写性能要求并不高。相比之下,他们反而更关注数据写入磁盘时的压缩能力,通过减小存储空间,来降低数据库的使用成本。

TaurusDB透明压缩特性就是通过在存储过程中引入轻微延迟,换取更小的存储空间,进而满足客户降低存储成本的需求。

本文主要从透明压缩特性的使用开启方法、实现原理、性能优化以及性能影响评估等这几个方面来进行介绍。

使用方法

新实例的来源分为两种:一种是通过主界面上的“创建实例”生成新实例,另一种是通过已有实例的备份恢复来创建一个新实例。

第一种创建新实例的方式,如图1所示,需要通过选择“存储压缩”选项开启。

新实例开启透明压缩

图1中,压缩实例的开启选项包括高压缩比和高压缩速度两种模式。高压缩比和高压缩速度分别指使用ZSTD压缩算法和LZ4压缩算法两种不同方式进行压缩。其中,高压缩比采用ZSTD压缩算法,能实现约2.1倍的压缩效率;而高压缩速度则运用LZ4压缩算法,其压缩比约为1.35倍。相较于ZSTD算法,LZ4算法对系统性能的影响较小。对于性能要求不高的用户而言,选择高压缩比模式能更有效地节省存储空间。

在备份恢复到新实例的场景中,如图2所示,压缩特性支持两种恢复方式: 一种是将非压缩存量实例,恢复到非压缩已有实例中;另一种是将压缩存量实例,恢复到压缩已有实例中。

压缩实例的备份恢复限制

在未来的透明压缩增强计划中,会提供支持将非压缩存量实例,恢复为压缩实例的功能。

原理介绍

透明压缩是一种通过页级别的粒度进行压缩和解压的技术。下面将分别介绍写入和读取页的对应流程。图3展示了压缩特性是如何与写入页的操作相适配的。

压缩实例与写入页的适配

在数据需要刷新到页上时,系统会调用flushByPageFlusher函数。该函数的底层实现是通过Ulog对secondary stream进行append操作,同时对页进行压缩处理,从而实现页级别的压缩能力。Ulog是TaurusDB存储底层提供的IO模型,它构成了数据库的数据存储单元。而secondary stream实际上是由Ulog组成的,它存储着数据库页面基本结构的信息。通过解析Ulog后的压缩字段,可以判断页面是否已被压缩,以及具体采用了哪种压缩方式。

与此相对对应的读取页面,也是类似,其流程如图4所示:

压缩实例与读取页的适配

在读取页面时,调用了slice侧的readPages函数,实际上是触发了Ulog的readInternalSync功能。在解析plog(构成ulog的基本存储单元)时,会存在两种场景:

1)如果plog header中的压缩字段为0,则表示该页面未经压缩,因此无需处理。

2)如果plog header中的压缩字段标记为LZ4/ZSTD,则说明该页已经过压缩,需要使用相应的LZ4/ZSTD算法进行解压缩。解压缩后即可获取所需的未压缩页面,其数据的读取结果与未压缩的页面相同。

然而,上述压缩特性存在一个明显问题,即在slice侧环境压力较大的情况下,压缩或解压缩都会占用一定的系统资源。特别是在高并发情况下,这可能会对slice侧造成严重的资源占用问题。

资源限制

为了解决可能因压缩和解压缩过程导致的资源问题,透明压缩特性采用线程队列和线程池来限制压缩操作对资源的占用。这样,即使在存储池压力极大的极端情况下,压缩操作也不会过多消耗资源。其中,页持久化压缩操作的处理方法,如图5所示。

页持久化时进行压缩处理

具体流程如下:

1)在初始化LRU线程队列LRUList时,压缩实例会生成一个压缩线程池。

2)当LRU页面需要被置换并且需要落盘时,会调用addPage方法,将需要压缩的页面放入压缩线程池队列compressDirty2Queue中,并有序地进行压缩操作。

3)当LRU队列出队时,我们进行真正的落盘操作,是通过调用flushByPageFlusher接口来实现最终的落盘。

从功能的角度来看,通过使用线程池有效地控制了压缩页操作的资源,从而实现了对刷盘性能的可控管理。

同样地,读取数据的流程也遵循了类似的资源限制设计原则,如图6所示:

读取压缩页进行解压缩处理

在初始化阶段,我们创建了ulog线程池,并同步创建了压缩处理线程池。当系统需要读取压缩页时,会利用在初始化阶段通过readCallback回调函数申请的压缩线程来执行读取操作。通过利用线程池的约束机制,我们成功地在资源受限的环境下实现了压缩页面的读取功能。

通过上述所述的方法,结合图5和图6所展示的流程,在确保资源消耗可控的前提下,成功实现了页面级别的读写透明压缩能力。

性能分析

使用sysbench工具来模拟真实业务大压力场景,以此评估压缩对业务TPS(交易处理速度)/QPS(查询处理速度)的影响。

场景一:测试LZ4高压缩速度算法,对业务TPS/QPS的影响

在硬件配置相同的8核32G内存机器上,对压缩和非压缩实例进行了sysbench测试。测试采用了64个表,每个表包含1000万条数据,来模拟大数据量的实际业务场景。测试过程中,分别在1到512个线程下,使用LZ4压缩算法,并记录不同模式下的QPS/TPS数值变化。结果如图7所示:

8U32G机器上采用高压缩速度(LZ4)性能影响

可以观察到,在最坏的情况下,根据TPS/QPS指标来衡量,性能下降不超过5%。

场景二:测试ZSTD高压缩比算法,对业务TPS/QPS的影响

同样,使用相同配置的8核32G内存的机器,在该机器上对压缩和非压缩实例进行了sysbench测试。测试采用了64个表,每个表包含1000万条数据,来模拟一个大数据量的sysbench测试模型。

通过进行压力测试实验,在1到512个线程的不同情况下,使用ZSTD压缩算法,记录QPS/TPS数值变化,如图8所示:

8U32G机器上采用高压缩比(ZSTD)性能影响

根据实验结果可以明确地观察到,相较于LZ4算法,ZSTD算法对性能的影响更为显著。从QPS/TPS的角度来看,在最糟糕的情况下,性能影响控制在10%以内。高压缩比意味着该算法具备更强的空间压缩能力,但同时也会给性能带来较大的影响。

总结

本文全面介绍了TaurusDB透明压缩特性。首先,介绍了用户如何通过界面开启压缩实例,并说明了如何实现页面级别的压缩能力。同时,针对压缩特性可能带来的资源占用问题,我们讨论了利用线程池进行优化的方法。

最后,通过一系类性能测试结果,展示了在高压缩速度和高压缩比两种场景下的压缩特性表现。具体而言,在使用高压缩速度的LZ4压缩模式时,其性能劣化控制在5%以内,对性能较敏感的用户,提供了一种既能节约空间又不显著影响性能的解决方案。而高压缩比的ZSTD模式则在空间上更加节省,性能劣化控制亦可控制在10%以内,更适用于对性能不敏感但希望大幅节约空间成本的客户群体。


华为开发者空间,汇聚鸿蒙、昇腾、鲲鹏、GaussDB、欧拉等各项根技术的开发资源及工具,致力于为每位开发者提供一台云主机、一套开发工具及云上存储空间,让开发者基于华为根生态创新。
点击链接
,免费领取您的专属云主机

.Net托管堆布局

image

加载堆

主要是供CLR内部使用,作为承载程序的元数据。

  1. HighFrequencyHeap
    存放CLR高频使用的内部数据,比如MethodTable,MethodDesc.

通过is判断类型之间的继承关系,调用接口的方法和虚方法,都需要访问MethodTable

  1. LowFrequencyHeap
    存放CLR低频使用的内部数据,比如EEClass,ClassLoader.

GC信息与异常处理表,它们都只在发生时才访问,因此访问频率不高。

  1. StringLiteralMap
    字符串驻留池:
    https://www.cnblogs.com/lmy5215006/p/18494483

字符串对象本身存储在FOH堆中,String Literal Map只是一个索引

  1. StubHeap
    函数入口的代码堆
  2. CodeHeap
    JIT编译代码使用的内部堆,比如生成IL。
  3. VirtualCallStubHeap
    虚方法调用的内部堆

使用!eeheap -loader可以查看

眼见为实

image

新版sos呈现方式不一样,可以使用老版sos展示文中所述内容

托管堆

大家的老朋友了,不做过多解释,由GC统一管理的内存堆.一个.NET程序中所有的Domain都会共用一个托管堆

  1. SOH
    略略略
  2. LOH
    略略略
  3. POH
    固定对象专属的堆,比如非托管线程访问托管对象,就需要把对象固定起来,避免被GC回收造成非托管代码的
    访问违例
    .

使用!eeheap -gc可以查看

眼见为实

image

冻结堆

.NET8推出来的一个新堆,用来存放永远不会被GC管理的永生对象,比如string 字面量。
简单来说,就是一个对象你都永远不会释放了,还放在托管堆就是浪费了。不如单独拎出来存。

眼见为实

image

https://www.cnblogs.com/lmy5215006/p/18515971

上述所说的各种堆,只是一个逻辑上的概念。作为内存的物理承载。由堆段(Heap Seg-ment)实现.
简单来说,段是托管堆的物理表示。

image

眼见为实

image

segment begin allocated committed allocated size committed size
段指针的对象地址 内存分配的起始点 内存分配的末尾点 已提交的分配大小 已提交的大小

SOH小对象堆

堆只是一个抽象的概念,在物理上的表现形式为内存段,作为CLR细化堆的一种管理单位。多个段组成了堆。

.NET8之前的段结构

在.NET 8 之前,段分为SOH,LOH,POH 三个段。
对于SOH段有点特殊,因为段上面还有分代逻辑。包含0代和1代的对象只会分配在新分配的内存段上(临时段),剩下的每个段都是2代的段
image
可以看到,代只是一个逻辑概念,并没有独立的段空间。0,1,2代共享段空间。

.NET8的段结构

到了.NET 8,代已经不是一个逻辑概念,而是一个物理概念。
每个代都有了自己独立的段空间。
image

代机制

每当GC触发时,所有对象(非固定)都会进行升代,直到gen2为止。

  1. obj对象刚创建,为0代
    内存地址为0x00000263ee009528,0x01fb08000028>0x000001fb080b71e0>01fb080b9068 说明obj放在0代里
    image
  2. 第一次GC,obj升为1代
    内存地址在1代空间范围内
    image
  3. 第二次GC,obj升为2代
    内存地址在2代空间范围内
    image

代边界

细心的朋友会发现一个盲点,就是obj刚刚创建的时候,0代内存起始点为0263ee000028,升为1代后,1代内存起始点也变为了0263ee000028,2代也同样。
这就引申出另一个概念,GC升代,不是简单的copy对象从0代到1代。而是移动代的边界。
每次GC触发时,代边界指针会在多个“地址段”上迁移,通过这种逻辑操作,达到性能的最高,可以观察上面的 Allocated 区,一会给了 0gen,一会又给了 1gen,一会又给了 2gen

LOH大对象堆

大对象堆存储所有>=85000byte的对象,但也是有例外。LOH堆上对象管理相对宽松,没有“代”机制,默认情况下也不会压缩。
image

例外1-32位环境下的double[]

        static void Main(string[] args)
        {
            double[] array1 = new double[999];
            Console.WriteLine(GC.GetGeneration(array1));

            double[] array2 = new double[1000];
            Console.WriteLine(GC.GetGeneration(array2));

            double[,] array3 = new double[32,32];
            Console.WriteLine(GC.GetGeneration(array3));

            long[] array4 = new long[1000];
            Console.WriteLine(GC.GetGeneration(array4));

            Debugger.Break();
            Console.ReadKey();
        }

image

这里有个很奇怪的现象,在
32位环境
下,array2的大小= 4b+4+4+1000*8=8012byte. 远远<=85000byte. 为什么被分配到了LOH堆?
这主要跟内存对齐有关,double的未对齐访问非常昂贵,远远超过long,ulong,int。这对于64位环境来说不是问题,总是对SOH与LOH使用8byte对齐。但对于4字节对齐的32位环境。这就是个大问题了.
所以CLR开发团队决定将阈值大于1000的double放入LOH堆(LOH堆总是8byte对齐)。避免了double未对齐访问的巨大成本

例外2-StringInter与静态成员以及元数据

https://www.cnblogs.com/lmy5215006/p/18515971
参考此文,在.NET5之前没有POH堆,所以CLR内部使用的三个数组也会进入LOH堆。
三个数组分别为

  1. static对象的object[]
  2. 字符串池 object[]
  3. 元数据 RuntimeType object[]

其实很好理解,这些都是低频变化的内容,放在LOH堆上好过放在SOH堆。

POH堆

POH堆解决了什么问题?
从.NET5开始,CLR团队给pinned的对象单独放入一个段中,这样pinned对象不会和普通对象混在一起。导致大量细小Free空间。从而降低托管堆碎片化,也降低了代降级的频次。

有点遗憾的是,非托管代码造成的对象固定,并不会移动到POH堆中。因此代降级的现象依旧存在。
感觉未来微软可以重点优化这块,固定对象是GC速度最大的阻碍。
image

如何使用POH堆?

在.NET 8中,将对象放入POH堆是一种
“有意为之”
行为,必须调用 GC 类提供的 AllocateArray 和 AllocateUninitializedArray 方法并设置 pinned=true
image

FOH

FOH堆解决了什么问题?
在.NET8中,如果一个对象在创建的时候,就明确知道是
“永生”
对象,那就没必要纳入托管堆的管理范围,只会徒增GC的工作量。因此干脆把对象放在托管堆之外,来提高性能
image

常见的例子就是字符串的字面量(literal)

static对象布局

静态的基元类型(short,int,long) ,它的值本身并不存放在托管堆上。而是存放在Domain中的高频堆中
image

静态的引用类型则不同。真正的对象存放在托管堆上,再由POH中一个object[]持有,最后被高频堆中的m_pGCStatics所管理
image

Domain下每一个Module都维护了一个DomainLocalModule结构,静态变量放在该Module中

眼见为实:静态基元类型分配在高频堆上?

    internal class Program
    {
        static long age = 10086;
        static void Main(string[] args)
        {
            age = 12;
            Console.WriteLine("done. " + age);
            Debugger.Break();
        }
    }

image
通过汇编得知,static a的地址为
00007ff9a618e4a8
image
观察高频堆地址可以发现,00007FF9A6180000<
00007ff9a618e4a8
<00007FF9A6190000 。明显属于高频堆

眼见为实:静态引用类型分配在哪?

    internal class Program
    {
        public static Person person = new Person();

        static void Main(string[] args)
        {
            var num = person.age;

            Console.WriteLine(num);

            Debugger.Break();
        }
    }

    public class Person
    {
        public int age = 12;
    }
  1. 使用!gcwhere命令来查看person对象属于0代中,说明对象本身分配在托管堆
    image

  2. 使用!gcroot命令查看它的引用根,发现它被一个object[]所持有
    image

  3. 再查看object[]的所属代,可以看到该对象属于POH堆
    image

  4. bp coreclr!JIT_GetSharedNonGCStaticBase_Helper 下断点来获取 DomainLocalModule 实例
    image
    注意,这里我重新运行了一遍,所以object[]内存地址有变

字符串驻留池布局

关于字符串的不可变性,参考此文:
https://www.cnblogs.com/lmy5215006/p/18494483

image

在.NET8之前,字符串驻留与静态引用类型处理模式无差别。
.NET 8加入FOH堆之后,会将编译期间就能确定的字符串放入FOH堆,以便提高GC性能。

眼见为实

        static void Main(string[] args)
        {
            var str1 = "hello FOH";//编译期间能确定
            var str2 = Console.ReadLine();
            string.Intern(str2);//运行期间才能确定

            Console.WriteLine($"str1={str1},str2={str2}");
            Debugger.Break();
        }
  1. 编译期间能确定的,直接加入了FOH
    image

  2. 运行期间确定,与静态引用类型处理流程一致
    image
    image

Spring

简介

一般来说,Spring指的是SpringFramework,它提供了很多功能,例如:
控制反转(IOC)、依赖注入

(DI)、切面编程(AOP)、事务管理(TX)

主要 jar 包

  • org.springframework.core:
    Spring的核心工具包,其他包依赖此包

  • org.springframework.beans:
    所有应用都用到,包含访问配置文件,创建和管理bean等

  • org.springframework.aop:
    Spring的面向切面编程,提供AOP的实现

  • org.springframework.context:
    提供在基础IOC功能上的扩展服务,此外还提供许多企业级服务的支持(邮件服务、任务调度、JNDI定位、EJB集成、远程访问、缓存等)

  • org.springframework.web.mvc:
    包含SpringMVC应用开发时所需的核心类

  • org.springframework.transaction:
    为JDBC、Hibernate、JPA提供一致的声明式和编程式事务管理

  • org.springframework.web:
    包含web应用开发时,用到的Spring框架时所需的核心类

  • org.springframework.aspect:
    Spring提供的对Aspect框架的整合

  • org.springframework.test:
    对JUNIT等测试框架的简单封装

  • org.springframework.context.support:
    Spring context的扩展支持,用于MVC方面

  • org.springframework.expression:
    Spring表达式语言

  • org.springframework.jdbc:
    对JDBC的简单封装

  • org.springframework.web.servlet:
    对J2EE6.0 servlet3.0的支持

IOC(控制反转)

组件和组件管理

整个项目由各种组件搭建而成

在Spring中,组件交给Spring容器进行管理,代替程序员之前new对象的操作,控制对象的创建,管理对象的生命周期

优势

  • 降低了组件之间的耦合性:
    Spring IoC容器通过依赖注入机制,将组件之间的依赖关系削弱,减少了程序组件之间的耦合性

  • 提高了代码的可重用性和可维护性:
    将组件的实例化过程、依赖关系的管理等功能交给Spring IoC容器处理,使得组件代码更加模块化、可重用、更易于维护。

  • 方便了配置和管理:
    Spring IoC容器通过XML文件或者注解,轻松的对组件进行配置和管理,使得组件的切换、替换等操作更加的方便和快捷

容器管理配置方式

  • XML配置文件:
    在XML文件中定义Bean及其依赖关系、Bean的作用域等信息,让Spring IoC容器来管理Bean之间的依赖关系

  • 注解方式配置:
    通过在Bean类上使用注解来代替XML配置文件中的配置信息。通过在Bean类上加上相应的注解(如@Component, @Service, @Autowired等),将Bean注册到Spring IoC容器中

  • Java配置类配置:
    通过Java类来定义Bean、Bean之间的依赖关系和配置信息,通过@Configuration、@Bean等注解来实现Bean和依赖关系的配置

实例化方式

  • 构造器实例化


    • 在配置文件中定义
      bean
      (对应bean中要有空参构造)

      <bean id="" class="">
          <property name="" value=""/>
      </bean>
      
    • 编写构造器

    • 获取实例化对象

      public void test1(){
              ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
              UserService userService = context.getBean(UserService.class);
          }
      
  • 静态工厂实例化

    <bean id="user4" class="com.sxny.pojo.User" factory-method="createInstance"/>
    
  • 实例化工厂实例化

    <bean id="userFactory" class="com.sxny.pojo.UserFactory"></bean>
    <bean id="user5" factory-bean="userFactory" factory-method="createUser"/>
    <bean id="book2" name="book3,book4" class="com.sxny.pojo.Book">
    

Spring Bean

指被Spring容器管理的对象

将一个类声明为Bean的注解

  • @Component
    :通用的注解,可标记任何类为Spring组件

  • @Service
    :对应服务层,主要涉及一些复杂的逻辑

  • @Repository
    :对应持久层,主要用于数据库相关操作

  • @Controller
    :对应SpringMVC控制层,主要用于接收用户请求,并调用Service层,返回数据给前端


    @Component

    @Bean

    @Component 作用于类,@Bean作用于方法

    @Bean 比 @Component 的自定义性更强,有些地方只能用 @Bean 注解来注册bean,如:当我们需要引用第三方库中的类,需要装配到Spring容器中时,只能通过 @Bean 注解实现

Bean 的作用域

  • singleton:
    容器中只有唯一的bean实例,Spring中bean默认都是单例的
    (默认)

  • prototype:
    每次获取bean时都会创建一个新的bean实例

  • request(仅web应用可用):
    每次http请求都会产生一个新的bean,仅在该HTTP Request内生效

  • session(仅web应用可用):
    每一次来自新 session 的 HTTP 请求都会产生一个新的 bean,仅在当前 HTTP session 内有效

Bean 的生命周期

实例化 -> 属性赋值 -> 初始化 -> 销毁

在实例化阶段,Spring容器通过
反射
机制创建Bean,然后进行依赖注入,如果Bean实现了
BeanNameAware
等接口,容器会调用相应方法

在初始化阶段,
BeanPostProcessor
会在前后进行拦截处理,最后调用初始化方法,例如
@PostConstruct

afterPropertiesSet

销毁时则会调用
DisposeableBean.destory()
或自定义的销毁方法

DI(依赖注入)

在组件之间传递依赖关系的过程中,将依赖关系放入容器内部进行处理,降低依赖关系,实现了对象间的解耦合

实现方式

  1. 通过构造器注入
    (对应类中必须有对应的构造函数)

    <bean id="user" class="com.pojo.User">
        <constructor-arg name="id" value="1"/>
        <constructor-arg name="name" value="张三"/>
        <constructor-arg name="sex" value="18"/>
    </bean>
    
  2. 通过setter方法注入

    <bean id="book" class="com.pojo.Book">
        <property name="name" value="1"/>
        <property name="bid" value="2"/>
    </bean>
    
  3. 通过字段注入


    通过字段注入,也就是通过注解进行注入(
    @Autowired

    @Resource

@Autowired 和 @Resource 的区别

  • @Autowired
    是 Spring 提供的注解,默认的注入方式是按照类型进行匹配,也就是说会优先通过接口类型去匹配并注入bean,当接口存在多个实现类时,byType这种方式就无法正确注入对象了,可以通过结合
    @Qualifier
    注解来显式指定名称

  • @Resource
    是JDK提供的注解,默认注入方式是按照名称进行匹配,如果无法通过名称匹配到对应的bean,就会按照类型来匹配

  • @Autowired
    支持在构造方法、方法、字段、参数上使用,
    @Resource
    用于字段和方法上的使用,不支持在构造方法和参数使用

AOP(面向切面编程)

横向编程,在不改变核心业务逻辑的情况下,对一些功能进行增强(常用在日志管理、权限控制、事务处理、安全控制)

Spring AOP的底层原理

基于JDK的动态代理实现、基于cglib的字节码生成实现

AOP 的底层是两种动态代理都有,默认情况下,如果目标类实现了一个或多个接口,AOP自动采用的是JDK动态代理进行增强,如果目标类没有实现接口,AOP采用cglib字节码生成的方式来增强,当然,也可用强制使用cglib字节码生成的方式进行增强,这样就不用考虑目标类是否实现了接口

基本概念

  • join point(连接点):
    目标类中的所有方法

  • pointcut(切点):
    对哪些方法进行拦截

  • advice(通知):
    拦截到方法要干什么

  • aspect(切面):
    通知和切点的结合

  • target(目标):
    被代理的目标对象

Spring AOP 和AspectJ AOP

Spring AOP属于运行时增强,AspectJAOP属于编译时增强,SpringAOP是基于代理,AspectJAOP是基于字节码操作,切面比较少时,没有太大区别,切面较多时,建议使用AspectJAOP

通知类型

  • 前置通知:
    在被代理的目标方法前执行

  • 后置通知:
    在被代理的目标方法最终结束后执行

  • 环绕通知:
    使用try...catch...finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置

  • 异常通知:
    在被代理的目标方法异常结束后执行

  • 返回通知:
    在被代理的目标方法成功结束后执行

可以通过XML配置或者注解的方式实现

实现

  1. 导包(添加依赖)

  2. 通过注解或者XML配置

    // 通过注解方式
    // @Aspect表示这个类是一个切面类
    @Aspect
    // @Component注解保证这个切面类能够放入IOC容器
    @Component
    public class LogAspect {
    
        // @Before注解:声明当前方法是前置通知方法
        // value属性:指定切入点表达式,由切入点表达式控制当前通知方法要作用在哪一个目标方法上
        @Before(value = "execution(public int com.atguigu.proxy.CalculatorPureImpl.add(int,int))")
        public void printLogBeforeCore() {
            System.out.println("[AOP前置通知] 方法开始了");
        }
        
        @AfterReturning(value = "execution(public int com.atguigu.proxy.CalculatorPureImpl.add(int,int))")
        public void printLogAfterSuccess() {
            System.out.println("[AOP返回通知] 方法成功返回了");
        }
        
        @AfterThrowing(value = "execution(public int com.atguigu.proxy.CalculatorPureImpl.add(int,int))")
        public void printLogAfterException() {
            System.out.println("[AOP异常通知] 方法抛异常了");
        }
        
        @After(value = "execution(public int com.atguigu.proxy.CalculatorPureImpl.add(int,int))")
        public void printLogFinallyEnd() {
            System.out.println("[AOP后置通知] 方法最终结束了");
        }
    }
    
  3. 开启
    aspectj
    注解支持

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:aop="http://www.springframework.org/schema/aop"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
    
        <!-- 进行包扫描-->
        <context:component-scan base-package="com.atguigu" />
        <!-- 开启aspectj框架注解支持-->
        <aop:aspectj-autoproxy />
    </beans>
    

TX(事务)

一系列动作中,只要有一个出现问题,所有动作全部回滚到事务开始的状态,避免了数据不一致导致的错误

对应依赖

  • spring-tx:
    包含声明式事务实现的基本规范(事务管理器规范接口和事务增强等等)

  • spring-jdbc:
    包含DataSource方式事务管理器实现类DataSourceTransactionManager

  • spring-orm:
    包含其他持久层框架的事务管理器实现类例如:Hibernate/Jpa等

底层原理

对事务的操作本来是由数据库进行的,但是为了方便用户进行业务逻辑的控制,Spring对事务进行了扩展实现; 在Spring中,事务是通过
AOP代理
实现的,对被代理对象的每个方法进行拦截,在方法执行前启动事务,在方法执行完成后根据是否有异常及异常的类型进行提交或回滚。

核心接口:
PlatFormTransactionManager

编程式事务和声明式事务

  • 编程式事务:
    在代码中硬编码,通过
    TransactionTemplate
    或者
    TransactionManager
    手动管理事务

  • 声明式事务:
    在XML文件中进行配置或通过注解实现

四个特性

  • 原子性:
    事务包含的所有数据库操作,要么全部成功,要么失败全部回滚

  • 一致性:
    事务必须使数据库从一个一致性状态转换为另一个一致性状态

  • 持久性:
    事务一旦提交,对数据的改变是永久性的

  • 隔离性:
    当多个用户并发访问数据库时,多个并发事务之间要相互隔离,不被其他事务操作所干扰

事务属性

只读(readonly)

对一个查询操作来说,如果我们把它设置成只读,就能够明确告诉数据库,这个操作不涉及写操作。

// readOnly = true把当前事务设置为只读 默认是false!
@Transactional(readOnly = true)

超时(timeout)

事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,从而长时间占用数据库资源。设置超时属性,超时回滚,释放资源

@Service
public class StudentService {

    @Autowired
    private StudentDao studentDao;

    /**
     * timeout设置事务超时时间,单位秒! 默认: -1 永不超时,不限制事务时间!
     */
    @Transactional(readOnly = false,timeout = 3)
    public void changeInfo(){
        studentDao.updateAgeById(100,1);
        //休眠4秒,等待方法超时!
        try {
            Thread.sleep(4000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        studentDao.updateNameById("test1",1);
    }
}

事务异常(rollbackFor)

默认只针对运行时异常,编译时异常不回滚

  • 设置回滚异常
    (rollbackFor)

    /**
     * timeout设置事务超时时间,单位秒! 默认: -1 永不超时,不限制事务时间!
     * rollbackFor = 指定哪些异常才会回滚,默认是 RuntimeException and Error 异常方可回滚!
     * noRollbackFor = 指定哪些异常不会回滚, 默认没有指定,如果指定,应该在rollbackFor的范围内!
     */
    @Transactional(readOnly = false,timeout = 3,rollbackFor = Exception.class)
    public void changeInfo() throws FileNotFoundException {
        studentDao.updateAgeById(100,1);
        //主动抛出一个检查异常,测试! 发现不会回滚,因为不在rollbackFor的默认范围内! 
        new FileInputStream("xxxx");
        studentDao.updateNameById("test1",1);
    }
    
  • 设置不回滚的异常(norollbackFor)

    @Service
    public class StudentService {
    
        @Autowired
        private StudentDao studentDao;
    
        /**
         * timeout设置事务超时时间,单位秒! 默认: -1 永不超时,不限制事务时间!
         * rollbackFor = 指定哪些异常才会回滚,默认是 RuntimeException and Error 异常方可回滚!
         * noRollbackFor = 指定哪些异常不会回滚, 默认没有指定,如果指定,应该在rollbackFor的范围内!
         */
        @Transactional(readOnly = false,timeout = 3,rollbackFor = Exception.class,noRollbackFor = FileNotFoundException.class)
        public void changeInfo() throws FileNotFoundException {
            studentDao.updateAgeById(100,1);
            //主动抛出一个检查异常,测试! 发现不会回滚,因为不在rollbackFor的默认范围内!
            new FileInputStream("xxxx");
            studentDao.updateNameById("test1",1);
        }
    }
    

事务隔离级别

  • 读未提交:
    事务可以读取未被提交的数据,容易产生脏读、不可重复读和幻读等问题。实现简单但不太安全,一般不用。

  • 读已提交:
    事务只能读取已经提交的数据,可以避免脏读问题,但可能引发不可重复读和幻读。

  • 可重复读:
    在一个事务中,相同的查询将返回相同的结果集,不管其他事务对数据做了什么修改。可以避免脏读和不可重复读,但仍有幻读的问题。

  • 可串行化:
    最高的隔离级别,完全禁止了并发,只允许一个事务执行完毕之后才能执行另一个事务。可以避免以上所有问题,但效率较低,不适用于高并发场景。

隔离级别的设置
(isolation)

@Service
public class StudentService {

    @Autowired
    private StudentDao studentDao;

    /**
     * timeout设置事务超时时间,单位秒! 默认: -1 永不超时,不限制事务时间!
     * rollbackFor = 指定哪些异常才会回滚,默认是 RuntimeException and Error 异常方可回滚!
     * noRollbackFor = 指定哪些异常不会回滚, 默认没有指定,如果指定,应该在rollbackFor的范围内!
     * isolation = 设置事务的隔离级别,mysql默认是repeatable read!
     */
    @Transactional(readOnly = false,
                   timeout = 3,
                   rollbackFor = Exception.class,
                   noRollbackFor = FileNotFoundException.class,
                   isolation = Isolation.REPEATABLE_READ)
    public void changeInfo() throws FileNotFoundException {
        studentDao.updateAgeById(100,1);
        //主动抛出一个检查异常,测试! 发现不会回滚,因为不在rollbackFor的默认范围内!
        new FileInputStream("xxxx");
        studentDao.updateNameById("test1",1);
    }
}

事务传播行为

  • REQUIRED:
    如果存在一个事务,则支持当前事务,不存在,则创建一个新事务

  • REQUIRED_NEW:
    开启一个新事务,如果已存在事务,则挂起当前事务

  • SUPPORTS:
    如果当前存在事务,则支持当前事务,不存在,则以非事务的方式执行

  • NOT_SUPPORTED:
    总是以非事务的方式执行,并挂起任何已存在的事务

  • NAVER:
    总是以非事务的方式执行,若已存在事务,则抛出异常

  • NESTED:
    如果一个事务已存在,则运行在一个嵌套事务中

  • MANDATORY:
    如果已存在事务,则支持当前事务,如果不存在,则抛出异常


通过XML进行配置

<!--    配置事务管理器-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
<!--    <tx:annotation-driven transaction-manager="transactionManager"/>-->
    <tx:advice id="interceptor" transaction-manager="transactionManager">
        <tx:attributes>
<!--            精确-->
            <tx:method name="addUser" propagation="MANDATORY"/>
<!--            半模糊-->
            <tx:method name="update" propagation="REQUIRES_NEW"/>
<!--            全模糊-->
            <tx:method name="*" read-only="true"/>
        </tx:attributes>
    </tx:advice>

通过注解实现

@Transactional(rollbackFor = Exception.class,propagation = Propagation.NESTED)
@Override
public void updateBook() {
    bookMapper.updateBook();
}

懒加载

默认为false,Spring会在容器初始化时,解析XML或注解,创建配置为单例的Bean并放入map中,懒加载可以使bean在被调用时才被创建(只对单例的生效,因为多例的bean本来就是在被调用时才会创建)

前言

Vue.js 使用虚拟 DOM 处理单页面,然后使用 Webpack 打包。通过上一篇文章的例子,读者也许已经发现,无论语法和写法如何不同,Vue.js 程序打包后都是一个单一的 HTML 文件,同时会引入一个标准的 JavaScript 文件。

Vue.js 中编写的所有代码都被 Webpack 自动打包成可以被浏览器解析的 HTML 和 JavaScript 代码,并且项目本身就只有一个页面。这意味着所有的用户对服务器发出进入页面的请求时,只会对服务器发出一次请求。

传统的 HTML 网页应用如果进行页面跳转,会根据网页地址(URL)来刷新页面,在网速极大提高的今天,这类跳转仍会不可避免地出现“白屏”现象,这显然不是 Vue.js 单页面应用想要的效果。而应用本身又需要 URL 来控制页面,在这种情况下,Vue.js 提供了 vue-router 来实现页面跳转。

本文内容

本文只是对 vue-router 有个初步的了解,后续的文章中会有更深入的使用

  • vue-router 介绍
  • 两种路由模式以及实现原理
  • 编写三个简单的 vue 页面
  • vue-router 的安装与配置
  • 测试与验证

关于 vue-router

本项目使用的 vue-router 版本是 3.x,官方文档:
https://v3.router.vuejs.org/zh/

Vue Router 是官方为 Vue.js 提供的路由管理器,它与 Vue.js 深度集成,让开发者能够轻松构建单页面应用。

通过 Vue Router,可以使用基于组件的方式定义路由,以实现灵活的页面结构和嵌套视图组织。

它的主要功能包括:

  • 灵活的路由定义
    :支持嵌套路由、通过 URL 参数与查询字符串传递数据,以及使用通配符实现动态匹配。
  • 简洁的组件化配置
    :将路由与组件有机结合,更好地组织项目结构。
  • 多种模式选择
    :在现代浏览器下可以使用历史模式,为用户带来更佳的 URL 体验;在不支持的环境中则自动降级为哈希模式。
  • 平滑的过渡动画
    :与 Vue 的过渡系统集成,可以轻松实现页面切换时的流畅过渡动画。
  • 细粒度的导航控制
    :通过导航守卫,你可以在路由切换前后进行权限检查、数据预加载等逻辑处理。
  • 个性化体验
    :包括可自定义的滚动行为,以及在活跃链接上自动添加 CSS 类,为用户提供更好的交互与界面反馈。

URL 模式

vue-router 有两种模式模拟 URL:hash 模式和 history 模式。(本项目使用默认的 hash 模式)

  • hash 模式是默认模式,使用网页的 URL 模拟一个完整的 URL,当
    #
    后的哈希值发生变化时,重新获取 hash 对应的页面(在 Vue.js 中是需要显示的组件),并将这些内容显示在页面中。
  • history 模式针对的是支持 HTML 5 新特性
    history
    的浏览器,其本身就是用户访问页面时浏览记录的堆栈,HTML 5 允许操作 history 栈中的内容。

PS:无论采用何种方式配置 vue-router,Vue.js 单页面应用都不会刷新页面。

实现方式的区别

  • hash 模式的实现是通过
    history.pushState()
    跳转路由;通过
    hashchange event
    监听路由变化。
  • history 模式通过
    history.pushState

    history.replaceState
    改变 URL;通过
    popstate event
    监听路由变化,但无法监听到
    history.pushState()
    时的路由变化。

体验上的区别

  • hash 只能改变#后的值,而 history 模式可以随意设置同源 url;
  • hash 只能添加字符串类的数据,而 history 可以通过 API 添加多种类型的数据;
  • hash 无需后端配置且兼容性好,而 history 需要配置
    index.html
    用于匹配不到资源的情况;
  • hash 的历史记录只显示之前的 www.a.com 而不会显示 hash 值,而 history 的每条记录都会进入到历史记录
    (在 Chrome102 版本之后,hash 模式的完整 URL 也会加入浏览器的历史记录了)

添加页面

先回顾一下我们的项目目录结构

starblog-admin-ui
├── build
├── config
├── node_modules
├── src
│   ├── assets
│   ├── router
│   ├── components
│   ├── App.vue
│   └── main.js
├── static
├── test
├── README.md
├── index.html
├── package.json
└── yarn.lock

现在我要把
src/components
这个目录改成
views

然后在里面分别添加三个页面

  • Login.vue
  • Home.vue
  • 404.vue

主页面

Home.vue
代码

<template>
  <el-alert
    title="主页"
    type="success">
  </el-alert>
</template>

<script>
export default {
  name: "Home"
}
</script>

<style scoped>

</style>

登录页面

然后是
Login.vue

<template>
  <el-alert
    title="登录"
    type="info">
  </el-alert>
</template>

404 页面

还有
404.vue

<template>
  <el-alert
    title="未找到"
    type="error">
  </el-alert>
</template>

这时控制台应该会报错了,不过问题不大,先来配置路由

安装和配置

之前我们在使用 vue-cli 创建项目的时候选择了使用 vue-router,所以不需要安装了。

如果没有的话,需要手动安装进去(本项目使用的 vue-router 版本是 3.x,现在最新版已经是 4.x 了,安装的时候请注意选择对应的版本)

yarn add vue-router

然后在 src 目录下新建一个
router
目录,里面再新建个
index.js
,代码如下

import Vue from 'vue'
import Router from 'vue-router'
import Login from "@/views/Login";
import Home from "@/views/Home";
import NotFound from '@/views/404'

Vue.use(Router)

export default new Router({
  mode: 'history',		// 这里可以设置模拟URL的模式
  routes: [
    {
      path: '/',
      name: 'Home',
      component: Home,
    },
    {
      path: '/Login',
      name: 'Login',
      component: Login
    },
    {
      path: '/404',
      name: 'NotFound',
      component: NotFound
    },
  ]
})

这里面有个
mode
设置,就是前面提到的两种模式模拟 URL,如果设置了
history
,这样跳转页面的时候,浏览器的地址栏里面就是这个形式:

  • localhost:8080/login
  • localhost:8080/404

用默认的 hash 模式的话,就是这样:

  • localhost:8080/#/login
  • localhost:8080/#/404

都差不多,不过 history 模式好看一点,不过要
支持 HTML5 新特性 history 的浏览器,并且还需要在后端做一些配置
才可以实现~

测试页面

分别打开以下地址:

  • http://localhost:8080/#/
  • http://localhost:8080/#/Login
  • http://localhost:8080/#/404

可以看到不同的信息就说明这一步完成了

老规矩,不贴图哈哈哈

收工~

1.概述

Kubernetes 集群巡检是一种监测和评估底层系统运行状况的重要手段,旨在快速发现系统中存在的潜在风险并提供修复建议。通过对 Kubernetes(K8s)集群进行定期巡检,可以有效保障集群稳定性、优化资源利用率、提升安全性,并降低运维风险,特别是在生产环境中,这种预防性措施尤为重要。

2.为什么要对 Kubernetes 集群巡检

(1) 确保集群稳定性

  • Kubernetes 是一个动态系统,运行时会不断调度和管理容器。由于配置错误、资源不足或节点故障等问题,可能影响集群的稳定性。
  • 巡检有助于识别潜在问题并在故障发生前解决。

(2) 提升集群性能

  • 通过检查资源分配和使用情况,可以优化集群资源利用率。
  • 找到性能瓶颈,防止 Pod 调度延迟或应用性能下降。

(3) 保障安全性

  • Kubernetes 环境需要定期审查安全配置,如 RBAC 权限、网络策略、防火墙规则等,以防止安全漏洞。

(4) 降低运维风险

  • 通过定期巡检,可以提前发现和解决问题,避免出现生产环境中的严重故障。

3.巡检内容

3.1
集群总览

  • Kubernetes 版本

  • 集群节点数

  • 资源总量:已分配 CPU 和内存

  • 资源使用率:最低、平均、最高 CPU 和内存使用率

  • 证书过期时间

3.2
K8s
核心组件状态

  • 核心组件
    :kube-apiserver、kube-controller-manager、kube-scheduler、etcd、kubelet 等。

  • etcd 备份情况
    :确保关键数据安全。

3.3
K8s
存储网络
组件状态

  • 网络连通性
    :检查服务的 DNS 解析和网络连通性。
  • CNI 插件状态
    :确认网络插件(如 Flannel、Calico)是否正常运行。
  • 存储状态
    :检查 Persistent Volume(PV)和 Persistent Volume Claim(PVC)是否正常,确保数据存储和挂载没有问题。

3.4 K8s
节点健康状态

检查集群中所有节点的健康状态,包括节点的运行状态、节点可用性、节点文件系统状态等。此外,还需要检查节点内核是否有死锁、
docker 是否正常等,以确保整个集群的稳定性和可用性。

  • 节点运行状态
    :检查节点是否处于 Ready 状态。

  • 文件系统状态
    :检查节点文件系统健康状况。

  • 关键服务
    :确认节点内核、Docker 服务是否正常。

3.5 K8s
节点资源状况

K8s 是一个高度动态的系统,它需要确保节点资源的可用性以支持应用程序的正常运行。因此,在对集群进行巡检时,需要检查节点的资源使用情况,包括CPU、内存和磁盘等。通过检查资源使用情况,可以确保节点资源的可用性和可扩展性,并及时发现可能会影响应用程序性能的问题。

  • 资源使用情况
    :检查节点的 CPU、内存、磁盘使用率,确保资源分配合理。

  • 容量规划
    :发现资源瓶颈并进行扩展规划。

3.6 K8s集群之上
容器云平台组件

巡检

主要检查集群核心组件及附加组件的状态以及重启状况。

  • 运行状态
    :检查容器云底层的核心组件及集成的附属组件(日志、网关、微服务治理等)的运行状态,以确保容器云平台的正常运行。
  • 重启状况
    :检查容器云底层的核心组件及集成的附属组件(日志、网关、微服务治理等)的重启情况,如重启次数、重启原因等,可以确保及时发现容器云平台自身组件的问题并进行修复。

3.7
运行

巡检

运行巡检主要针对于平台上运行的业务进行巡检,当进行运行巡检时,主要检查组件
pod 的运行状态和重启状况。

  • 运行状态
    :检查集群内
    各个
    Pod 的运行状态,例如 Pod 是否处于 Running 状态、Pod 是否处于 CrashLoopBackOff 状态、Pod 是否处于 Pending 状态等,以确保及时发现异常 Pod。
  • 重启状况

    检查集群内各个
    Pod 的重启情况,如重启次数、重启原因等,以确保及时发现 Pod 的问题并进行修复。

3.8
配置巡检

配置巡检主要针对于平台上运行的业务资源配置、健康检测配置等进行巡检。主要检查容器镜像标签、容器运行时参数、资源限制设置、存储挂载设置、容器健康检测设置。

  • 容器镜像标签

    检查容器镜像的标签是否合规,包括是否使用了
    latest 标签、是否使用了明确的版本号等。
  • 容器运行时参数
    :检查容器运行时参数是否安全,包括是否禁止使用特权模式、是否开启了安全策略等。
  • 资源限制设置
    :检查容器资源限制设置是否合理,包括
    CPU 和内存限制是否设置合理。
  • 存储卷挂载设置
    :检查容器存储卷挂载设置是否合理,包括是否禁止了对主机文件系统的挂载、是否使用了
    ReadOnlyRootFilesystem 等。
  • 容器健康检测设置
    :检查容器健康检测设置是否合理,包括是否设置了
    liveness 和 readiness 探针、探针的检测间隔是否设置合理等。

通过对这些配置进行扫描和分析,生成的配置巡检报告可以给出针对每个组件的配置建议和优化方案,帮助用户提高系统的安全性和可靠性。

3.9
安全巡检

安全巡检主要针对于平台上运行的业务镜像进行扫描,并对镜像中的各类安全漏洞,给出详细信息链接,以便用户进行修复。具体来说,安全巡检主要包含了扫描镜像安全漏洞、漏洞报告、建议和解决方案、自动化定期检测。

  • 扫描镜像安全漏洞

    :通过对集群内已部署的业务镜像进行扫描,获取镜像存在的安全漏洞信息。
  • 分析漏洞影响

    :对扫描出的安全漏洞进行分析,评估其对业务的影响程度,并给出相应的风险评级。
  • 提供建议和解决方案

    :根据扫描出的安全漏洞和评估结果,给出相应的建议和漏洞详细信息,帮助用户修复漏洞,提升业务的安全性。
  • 定期检测和自动化

    :安全巡检需要定期进行,以保持业务的安全性。目前,安全巡检支持自动化运行,针对每个新部署的业务组件均会自动进行检测,并生成相关报告。

4. 小结

Kubernetes 集群巡检不仅是确保系统健康的重要环节,也是优化集群性能、保障安全性和提升运维效率的关键措施。通过定期巡检,运维团队可以迅速发现和解决潜在问题,确保生产环境的高可用性和安全性。借助自动化工具和标准化流程,巡检工作可以更高效、更全面地完成,为 Kubernetes 集群的长期稳定运行提供坚实保障。

参考:
https://blog.csdn.net/qq_21127151/article/details/141905827

参考:
https://www.rainbond.com/docs/enterprise/scanner/