爱撒谎的男孩

爱撒谎的男孩 查看完整档案

杭州编辑清华大学  |  软件工程 编辑蚂蚁金服  |  Java工程师 编辑 chenjiabing666.github.io/ 编辑
编辑

笔名不才陈某,现就职于蚂蚁金服,从事Java后端开发,运营自己的微信公众号【码猿技术专栏】,定时分享从小白到大牛的进阶文章,喜欢的朋友支持一下!!!

个人动态

爱撒谎的男孩 发布了文章 · 9月14日

Mybatis如何执行Select语句,你真的知道吗?

持续原创输出,点击上方蓝字关注我吧

作者:不才陈某

博客:https://chenjiabing666.github.io

前言

  • 本篇文章是Myabtis源码分析的第三篇,前两篇分别介绍了Mybatis的重要组件和围绕着Mybatis中的重要组件教大家如何阅读源码的一些方法,有了前面两篇文章的基础,来看这篇文章的才不会觉得吃力,如果没有看过的朋友,陈某建议去看看,两篇文章分别是Mybatis源码解析之六剑客Mybatis源码如何阅读,教你一招!!!
  • 今天接上一篇,围绕Mybatis中的selectList()来看一看Mybatis底层到底做了什么,有什么高级的地方。

环境准备

  • 本篇文章讲的一切内容都是基于Mybatis3.5SpringBoot-2.3.3.RELEASE
  • 由于此篇文章是基于前两篇文章的基础之上,因此重复的内容不再详细赘述了。

撸起袖子就是干

  • 二话不说,先来一张流程图,Mybatis六剑客,如下: 六剑客执行流程图
  • 上图中的这六剑客在前面两篇文章中已经介绍的非常清楚了,此处略过。为什么源码解析的每一篇文章中都要放一张这个流程图呢?因为Mybatis底层就是围绕着这六剑客展开的,我们需要从全局掌握Mybatis的源码究竟如何执行的。

测试环境搭建

  • 举个栗子:根据用户id查询用户信息,Mapper定义如下:

List<UserInfo> selectList(@Param("userIds") List<String> userIds);

  • 对应XML配置如下:

`<mapper namespace="cn.cb.demo.dao.UserMapper">
<!--开启二级缓存-->
<cache/>
<select id="selectList" resultType="cn.cb.demo.domain.UserInfo">
select * from user_info where status=1
and user_id in
<foreach collection="userIds" item="item" open="(" separator="," close=")" >
#{item}
</foreach>
</select>
</mapper>`

  • 单元测试如下:

    `@Test
    void contextLoads() {
    List<UserInfo> userInfos = userMapper.selectList(Arrays.asList("192","198"));
    System.out.println(userInfos);
    }`

DEBUG走起

  • 具体在哪里打上断点,上篇文章已经讲过了,不再赘述了。
  • 由于SpringBoot与Mybatis整合之后,自动注入的是SqlSessionTemplate,因此代码执行到org.mybatis.spring.SqlSessionTemplate#selectList(java.lang.String, java.lang.Object),如图1
  • 从源码可以看到,实际调用的还是DefaultSqlSession中的selectList方法。如下图2
  • 「具体的逻辑如下」

    1. 根据Mapper方法的全类名从Mybatis的配置中获取到这条SQL的详细信息,比如paramterType,resultMap等等。
    2. 既然开启了二级缓存,肯定先要判断这条SQL是否缓存过,因此实际调用的是CachingExecutor这个缓存执行器。
  • DefaultSqlSession只是简单的获取SQL的详细配置,最终还是把任务交给了Executor(当然这里走的是二级缓存,因此交给了缓存执行器)。下面DEBUG走到CachingExecutor#query(MappedStatement, java.lang.Object, RowBounds,ResultHandler),源码如下图3
  • 上图中的query方法实际做了两件事,实际执行的查询还是其中重载的方法List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql),如下图4
  • 根据上图源码的分析,其实CachingExecutor执行的逻辑并不是很难,反倒很容易理解,「具体的逻辑如下」

    1. 如果开启了二级缓存,先根据cacheKey从二级缓存中查询,如果查询到了直接返回
    2. 如果未开启二级缓存,再执行BaseExecutor中的query方法从一级缓存中查询。
    3. 如果二级缓存中未查询到数据,再执行BaseExecutor中的query方法从一级缓存中查询。
    4. 将查询到的结果存入到二级缓存中。
  • BaseExecutor中的query方法无非就是从一级缓存中取数据,没查到再从数据库中取数据,一级缓存实际就是一个Map结构,这里不再细说,真正执行SQL从数据库中取数据的是SimpleExecutor中的public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)方法,源码如下图5
  • 从上面的源码也是可以知道,在真正执行SQL之前,是要调用prepareStatement(handler, ms.getStatementLog())方法做一些参数的预处理的,其中涉及到了六大剑客的另外两位,分别是ParameterHandlerTypeHandler,源码如图6
  • 从上图可以知道设置SQL参数的真正方法是handler.parameterize(stmt),真正执行的是DefaultParameterHandler中的setParameters方法,由于篇幅较长,简单的说一下思路:

    1. 获取所有参数的映射
    2. 循环遍历,获取参数的值,使用对应的TypeHandler将其转换成相应类型的参数。
    3. 真正的设置参数的方法是TypeHandlersetParameter方法
  • 继续图6的逻辑,参数已经设置完了,此时就该执行SQL了,真正执行SQL的是PreparedStatementHandler中的<E> List<E> query(Statement statement, ResultHandler resultHandler)方法,源码如下图7
  • 上图的逻辑其实很简单,一个是JDBC执行SQL语句,一个是调用六剑客之一的ResultSetHandler对结果进行处理。
  • 真正对结果进行处理的是DefaultResultSetHandler中的handleResultSets方法,源码比较复杂,这里就不再展示了,具体的逻辑如下:

    1. 获取结果映射(resultMap),如果没有指定,使用内置的结果映射
    2. 遍历结果集,对SQL返回的每个结果通过结果集和TypeHandler进行结果映射。
    3. 返回结果
  • ResultSetHandler对结果处理结束之后就会返回。至此一条selectList()如何执行的大概心里已经有了把握,其他的更新,删除都是大同小异。

总结

  • Mybatis的源码算是几种常用框架中比较简单的,都是围绕六大组件进行的,只要搞懂了每个组件是什么角色,有什么作用,一切都会很简单。
  • 一条select语句简单执行的逻辑总结如下(前提:「默认配置」):

    1. 「SqlSesion」#SqlSessionTemplate.selectList()实际调用#DefaultSqlSession.selectList()
    2. 「Executor」#DefaultSqlSession.quer()实际调用的是#CachingExecutor().query(),如果二级缓存中存在直接返回,不存在调用#BaseExecutor.quer()查询一级缓存,如果一级缓存中存在直接返回。不存在调用#SimpleExecutor.doQuery()方法查询数据库。
    3. 「StatementHandler」#SimpleExecutor.doQuery()生成StatementHandler实例,执行#PreparedStatementHandler.parameterize()方法设置参数,实际调用的是#ParamterHandler.setParameters()方法,该方法内部调用TypeHandler.setParameter()方法进行类型转换;参数设置成功后,调用#PreparedStatementHandler.parameterize().query()方法执行SQL,返回结果
    4. 「ResultSetHandler」#DefaultResultSetHandler.handleResultSets()对返回的结果进行处理,内部调用#TypeHandler.getResult()对结果进行类型转换。全部映射完成,返回结果。
  • 以上就是六剑客在Select的执行流程,如果有错误之处欢迎指正,如果觉得陈某写得不错,有所收获,关注分享一波。
查看原文

赞 1 收藏 1 评论 0

爱撒谎的男孩 发布了文章 · 9月4日

Mybatis入门篇之结果映射,你射准了吗?

持续原创输出,点击上方蓝字关注我吧

目录

  • 前言
  • 什么是结果映射?
  • 如何映射?

    • 别名映射
    • 驼峰映射

      • 配置文件开启驼峰映射
      • 配置类中开启驼峰映射
    • resultMap映射
    • 总结
  • 高级结果映射

    • 关联(association)

      • 例子
      • 关联的嵌套 Select 查询
      • 关联的嵌套结果映射
      • 总结
    • 集合collection

      • 集合的嵌套 Select 查询
      • 集合的嵌套结果映射
  • 总结

前言

  • 上一篇文章介绍了Mybatis基础的CRUD操作、常用的标签、属性等内容,如果对部分不熟悉的朋友可以看Mybatis入门之基本操作
  • 本篇文章继续讲解Mybatis的结果映射的内容,想要在企业开发中灵活的使用Mybatis,这部分的内容是必须要精通的。

什么是结果映射?

  • 简单的来说就是一条SQL查询语句返回的字段如何与Java实体类中的属性相对应。
  • 如下一条SQL语句,查询患者的用户id,科室id,主治医生id:
  <select id='selectPatientInfos' resultType='com.xxx.domain.PatientInfo'>
    select user_id,dept_id,doc_id from patient_info;
  </select>
  • Java实体类PatientInfo如下:
@Data
public class PatientInfo{
  private String userId;
  private String deptId;
  private String docId;
}
  • 程序员写这条SQL的目的就是想查询出来的user_id,dept_id,doc_id分别赋值给实体类中的userId,deptId,docId。这就是简单的结果映射。

如何映射?

  • Myabtis中的结果映射有很多种方式,下面会逐一介绍。

别名映射

  • 这个简单,保持查询的SQL返回的字段和Java实体类一样即可,比如上面例子的SQL可以写成:
<select id='selectPatientInfos' resultType='com.xxx.domain.PatientInfo'>
   select user_id as userId,
   dept_id as deptId,
   doc_id as docId
   from patient_info; 
</select>
  • 这样就能和实体类中的属性映射成功了。

驼峰映射

  • Mybatis提供了驼峰命名映射的方式,比如数据库中的user_id这个字段,能够自动映射到userId属性。那么此时的查询的SQL变成如下即可:
<select id='selectPatientInfos' resultType='com.xxx.domain.PatientInfo'>
    select user_id,dept_id,doc_id from patient_info;
  </select>
  • 如何开启呢?与SpringBoot整合后开启其实很简单,有两种方式,一个是配置文件中开启,一个是配置类开启。

配置文件开启驼峰映射

  • 只需要在application.properties文件中添加如下一行代码即可:
mybatis.configuration.map-underscore-to-camel-case=true

配置类中开启驼峰映射【简单了解,后续源码章节着重介绍】

  • 这种方式需要你对源码有一定的了解,上一篇入门教程中有提到,Mybatis与Springboot整合后适配了一个starter,那么肯定会有自动配置类,Mybatis的自动配置类是MybatisAutoConfiguration,其中有这么一段代码,如下:

  • @ConditionalOnMissingBean这个注解的意思就是当IOC容器中没有SqlSessionFactory这个Bean对象这个配置才会生效;applyConfiguration(factory)这行代码就是创建一个org.apache.ibatis.session.Configuration赋值给SqlSessionFactoryBean。源码分析到这,应该很清楚了,无非就是自己在容器中创建一个SqlSessionFactory,然后设置属性即可,如下代码:
    @Bean("sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        //设置数据源
        sqlSessionFactoryBean.setDataSource(dataSource);
        //设置xml文件的位置
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(MAPPER_LOCATOIN));
        //创建Configuration
        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        // 开启驼峰命名映射
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.setDefaultFetchSize(100);
        configuration.setDefaultStatementTimeout(30);
        sqlSessionFactoryBean.setConfiguration(configuration);
        //将typehandler注册到mybatis
        sqlSessionFactoryBean.setTypeHandlers(typeHandlers());
        return sqlSessionFactoryBean.getObject();
    }
  • 注意:如果对SqlSessionFactory没有特殊定制,不介意重写,因为这会自动覆盖自动配置类中的配置。

resultMap映射

  • 什么是resultMap?简单的说就是一个类似Map的结构,将数据库中的字段和JavaBean中的属性字段对应起来,这样就能做到一一映射了。
  • 上述的例子使用resultMap又会怎么写呢?如下:

<!--创建一个resultMap映射-->
<resultMap id="patResultMap" type="com.xxx.domain.PatientInfo">
  <id property="userId" column="user_id" />
  <result property="docId" column="doc_id"/>
  <result property="deptId" column="dept_id"/>
</resultMap>

<!--使用resultMap映射结果到com.xxx.domain.PatientInfo这个Bean中-->
<select id='selectPatientInfos' resultMap='patResultMap'>
    select user_id,dept_id,doc_id from patient_info;
  </select>
  • 其实很简单,就是创建一个<resultMap>,然后<select>标签指定这个resultMap即可。
  • <resultMap>的属性如下:

    • id:唯一标识这个resultMap,同一个Mapper.xml中不能重复
    • type:指定JavaBean的类型,可以是全类名,也可以是别名
  • 子标签<result>的属性如下:

    • column:SQL返回的字段名称
    • property:JavaBean中属性的名称
    • javaType:一个 Java 类的全限定名,或一个类型别名(关于内置的类型别名,可以参考上面的表格)。 如果你映射到一个 JavaBean,MyBatis 通常可以推断类型。然而,如果你映射到的是 HashMap,那么你应该明确地指定 javaType 来保证行为与期望的相一致。
    • jdbcType:JDBC 类型,所支持的 JDBC 类型参见这个表格之后的“支持的 JDBC 类型”。 只需要在可能执行插入、更新和删除的且允许空值的列上指定 JDBC 类型。这是 JDBC 的要求而非 MyBatis 的要求。如果你直接面向 JDBC 编程,你需要对可以为空值的列指定这个类型。
    • typeHandler: 这个属性值是一个类型处理器实现类的全限定名,或者是类型别名。
    • resultMap:结果映射的 ID,可以将此关联的嵌套结果集映射到一个合适的对象树中。 它可以作为使用额外 select 语句的替代方案。

总结

  • 以上列举了三种映射的方式,分别是别名映射驼峰映射resultMap映射
  • 你以为这就结束了?要是世界这么简单多好,做梦吧,哈哈!!!

高级结果映射

  • MyBatis 创建时的一个思想是:数据库不可能永远是你所想或所需的那个样子。 我们希望每个数据库都具备良好的第三范式或 BCNF 范式,可惜它们并不都是那样。 如果能有一种数据库映射模式,完美适配所有的应用程序,那就太好了,但可惜也没有。 而 ResultMap 就是 MyBatis 对这个问题的答案。
  • 我们知道在数据库的关系中一对一,多对一,一对多,多对多的关系,那么这种关系如何在Mybatis中体现并映射成功呢?

关联(association)

  • 关联(association)元素处理有一个类型的关系。 比如,在我们的示例中,一个员工属于一个部门。关联结果映射和其它类型的映射工作方式差不多。 你需要指定目标属性名以及属性的javaType(很多时候 MyBatis 可以自己推断出来),在必要的情况下你还可以设置 JDBC 类型,如果你想覆盖获取结果值的过程,还可以设置类型处理器。
  • 关联的不同之处是,你需要告诉 MyBatis 如何加载关联。MyBatis 有两种不同的方式加载关联:

    • 嵌套 Select 查询:通过执行另外一个 SQL 映射语句来加载期望的复杂类型。
    • 嵌套结果映射:使用嵌套的结果映射来处理连接结果的重复子集。
  • 首先,先让我们来看看这个元素的属性。你将会发现,和普通的结果映射相比,它只在 selectresultMap 属性上有所不同。

    • property: 映射到列结果的字段或属性。如果用来匹配的 JavaBean 存在给定名字的属性,那么它将会被使用。
    • javaType:一个 Java 类的完全限定名,或一个类型别名(关于内置的类型别名,可以参考上面的表格)
    • jdbcType: JDBC 类型, 只需要在可能执行插入、更新和删除的且允许空值的列上指定 JDBC 类型
    • typeHandler:使用这个属性,你可以覆盖默认的类型处理器。 这个属性值是一个类型处理器实现类的完全限定名,或者是类型别名。
    • column: 数据库中的列名,或者是列的别名。一般情况下,这和传递给 resultSet.getString(columnName) 方法的参数一样。 注意:在使用复合主键的时候,你可以使用 column="{prop1=col1,prop2=col2}" 这样的语法来指定多个传递给嵌套 Select 查询语句的列名。这会使得 prop1 prop2 作为参数对象,被设置为对应嵌套 Select 语句的参数。
    • select:用于加载复杂类型属性的映射语句的 ID,它会从 column 属性指定的列中检索数据,作为参数传递给目标 select 语句。 具体请参考下面的例子。注意:在使用复合主键的时候,你可以使用column="{prop1=col1,prop2=col2}" 这样的语法来指定多个传递给嵌套 Select 查询语句的列名。这会使得 prop1 和 prop2 作为参数对象,被设置为对应嵌套 Select 语句的参数。
    • fetchType:可选的。有效值为 lazyeager。 指定属性后,将在映射中忽略全局配置参数 lazyLoadingEnabled,使用属性的值。

例子

  • 一对一的关系比如:一个员工属于一个部门,那么数据库表就会在员工表中加一个部门的id作为逻辑外键。
  • 创建员工JavaBean
@Data
public class User {
    private Integer id;
    private String username;
    private String password;
    private Integer age;
  private Integer deptId;
  //部门
    private Department department;   
}
  • 部门JavaBean
@Data
public class Department {
    private Integer id;
    private String name;
}
  • 那么我们想要查询所有的用户信息和其所在的部门信息,此时的sql语句为:select * from user u left join department d on u.department_id=d.id;。但是我们在mybaits中如果使用这条语句查询,那么返回的结果类型是什么呢?如果是User类型的,那么查询结果返回的还有Department类型的数据,那么肯定会对应不上的。此时<resultMap>来了,它来了!!!

关联的嵌套 Select 查询【可以忽略】

  • 查询员工和所在的部门在Mybatis如何写呢?代码如下:
<resultMap id="userResult" type="com.xxx.domain.User">
    <id column="id" property="id"/>
    <result column="password" property="password"/>
    <result column="age" property="age"/>
    <result column="username" property="username"/>
  <result column="dept_id" property="deptId"/>
  <!--关联查询,select嵌套查询-->
  <association property="department" column="dept_id" javaType="com.xxx.domain.Department" select="selectDept"/>
</resultMap>

<!--查询员工-->
<select id="selectUser" resultMap="userResult">
  SELECT * FROM user WHERE id = #{id}
</select>

<!--查询部门-->
<select id="selectDept" resultType="com.xxx.domain.Department ">
  SELECT * FROM department WHERE ID = #{id}
</select>
  • 就是这么简单,两个select语句,一个用来加载员工,一个用来加载部门。
  • 这种方式虽然很简单,但在大型数据集或大型数据表上表现不佳。这个问题被称为N+1 查询问题。 概括地讲,N+1 查询问题是这样子的:

    • 你执行了一个单独的 SQL 语句来获取结果的一个列表(就是+1)。
    • 对列表返回的每条记录,你执行一个 select 查询语句来为每条记录加载详细信息(就是N)。
  • 这个问题会导致成百上千的 SQL 语句被执行。有时候,我们不希望产生这样的后果。

关联的嵌套结果映射【重点】

  • <association >标签中还可以直接嵌套结果映射,此时的Mybatis的查询如下:
<!-- 定义resultMap -->
<resultMap id="UserDepartment" type="com.xxx.domain.User" >
    <id column="user_id" property="id"/>
    <result column="password" property="password"/>
    <result column="age" property="age"/>
    <result column="username" property="username"/>
  <result column="dept_id" property="deptId"/>
    
    <!--
        property: 指定User中对应的部门属性名称
        javaType: 指定类型,可以是全类名或者别名
     -->
    <association property="department" javaType="com.xx.domain.Department">
    <!--指定Department中的属性映射,这里也可以使用单独拎出来,然后使用association中的resultMap属性指定-->
        <id column="id" property="id"/>
        <result column="dept_name" property="name"/>
    </association>
</resultMap>

<!-- 
    resultMap: 指定上面resultMap的id的值
 -->
 <select id="findUserAndDepartment" resultMap="UserDepartment">
     select 
   u.id as user_id,
   u.dept_id,
   u.name,
   u.password,
   u.age,
   d.id,
   d.name as dept_name
   from user u left join department d on u.department_id=d.id
 </select>

总结

  • 至此有一个类型的关联已经完成了,学会一个<association>使用即能完成。
  • 注意: 关联的嵌套 Select 查询不建议使用,N+1是个重大问题,虽说Mybatis提供了延迟加载的功能,但是仍然不建议使用,企业开发中也是不常用的。

集合collection

  • 集合,顾名思义,就是处理有很多个类型的关联。
  • 其中的属性和association中的属性类似,不再重复了。
  • 比如这样一个例子:查询一个部门中的全部员工,查询SQL如何写呢?如下:
select * from department d left join user u on u.department_id=d.id;
  • 此时的User实体类如下:
@Data
public class User {
 private Integer id;
 private String username;
 private String password;
 private Integer age;
 private Integer deptId; 
}
  • 此时的Department实体类如下:
@Data
public class Department {
 private Integer id;
 private String name;
 private List<User> users;
}
  • association类似,同样有两种方式,我们可以使用嵌套 Select 查询,或基于连接的嵌套结果映射集合。

集合的嵌套 Select 查询【可以忽略】

  • 不太重要,查询如下:
<resultMap id="deptResult" type="com.xxx.domain.Department">
  <!--指定Department中的属性映射,这里也可以使用单独拎出来,然后使用association中的resultMap属性指定-->
        <id column="id" property="id"/>
        <result column="name" property="name"/>
  <!--
  ofType:指定实际的JavaBean的全类型或者别名
  select:指定嵌套的select查询
  javaType:集合的类型,可以不写,Mybatis可以推测出来
-->
  <collection property="users" javaType="java.util.ArrayList" column="id" ofType="com.xxx.doamin.User" select="selectByDeptId"/>
</resultMap>

<select id="selectDept" resultMap="deptResult">
  SELECT * FROM department  WHERE ID = #{id}
</select>

<select id="selectByDeptId" resultType="com.xxx.domain.User">
  SELECT * FROM user WHERE dept_id = #{id}
</select>
  • 注意:这里出现了一个不同于association的属性ofType,这个属性非常重要,它用来将 JavaBean(或字段)属性的类型和集合存储的类型区分开来。

集合的嵌套结果映射【重点】

  • 现在你可能已经猜到了集合的嵌套结果映射是怎样工作的——除了新增的 ofType 属性,它和关联的完全相同。
  • 此时的Mybatis查询如下:

<!--部门的resultMap-->
<resultMap id="deptResult" type="com.xxx.domain.Department">
  <!--指定Department中的属性映射,这里也可以使用单独拎出来,然后使用association中的resultMap属性指定-->
        <id column="dept_id" property="id"/>
        <result column="dept_name" property="name"/>
  <!--
  ofType:指定实际的JavaBean的全类型或者别名
  resultMap:指定员工的resultMap
-->
  <collection property="users" ofType="com.xxx.doamin.User" resultMap='userResult'/>
</resultMap>

<!--员工的resultMap-->
<resultMap id="userResult" type="com.xxx.domain.User">
    <id column="user_id" property="id"/>
   <result column="password" property="password"/>
   <result column="age" property="age"/>
   <result column="username" property="username"/>
</resultMap>

<select id="selectDeptById" resultType="com.xxx.domain.Department">
  select 
  d.id as dept_id,
  d.name as dept_name,
  u.id as user_id,
  u.password,
  u.name
  from department d left join user u on u.department_id=d.id
  where d.id=#{id}
</select>

总结

  • 至此Mybatis第二弹之结果映射已经写完了,如果觉得作者写的不错,给个在看关注一波,后续还有更多精彩内容推出。
查看原文

赞 0 收藏 0 评论 0

爱撒谎的男孩 发布了文章 · 9月3日

Mybatis入门之基础CRUD

前言

  • 作为一个资深后端码农天天都要和数据库打交道,最早使用的是 Hiberate,一个封装性极强的持久性框架。自从接触到 Mybatis 就被它的灵活性所折服了,可以自己写 SQL,虽然轻量级,但是麻雀虽小,五脏俱全。这篇文章就来讲讲什么是 Mybatis,如何简单的使用 Mybatis。

什么是 Mybatis

  • MyBatis 是一款优秀的持久层框架,它支持自定义 SQL存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。

环境搭建

  • 本篇文章使用的环境是SpringBoot+Mybatis+Mysql

Maven 依赖

  • MySQL 驱动依赖和 Druid 连接池的依赖
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.40</version>
            <scope>runtime</scope>
        </dependency>

        <!--druid连接池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.9</version>
        </dependency>
  • Mybatis 启动包依赖,此处导入的是 SpringBoot 和 Mybatis 整合启动器的依赖,点击去可以看到,这个启动包依赖了mybatismybatis-spring(Mybatis 和 Spring 整合的 Jar 包),因此使用 SpringBoot 之后只需要导入这个启动器的依赖即可。
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.0.0</version>
        </dependency>
  • 以上两个依赖添加成功后,Maven 环境就已经配置完了。

数据库连接池配置(Druid)

  • 这个不是本文的重点,而且网上很多教程,我就简单的配置一下,在 SpringBoot 的application.properties中配置即可。
##单一数据源
spring.datasource.url=jdbc\:mysql\://127.0.0.1\:3306/vivachekcloud_pzhdermyy?useUnicode\=true&characterEncoding\=UTF-8&zeroDateTimeBehavior\=convertToNull&useSSL\=false
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
#初始化连接大小
spring.datasource.druid.initial-size=0
#连接池最大使用连接数量
spring.datasource.druid.max-active=20
#连接池最小空闲
spring.datasource.druid.min-idle=0
#获取连接最大等待时间
spring.datasource.druid.max-wait=6000
spring.datasource.druid.validation-query=SELECT 1
#spring.datasource.druid.validation-query-timeout=6000
spring.datasource.druid.test-on-borrow=false
spring.datasource.druid.test-on-return=false
spring.datasource.druid.test-while-idle=true
#配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
spring.datasource.druid.time-between-eviction-runs-millis=60000
#置一个连接在池中最小生存的时间,单位是毫秒
spring.datasource.druid.min-evictable-idle-time-millis=25200000
#spring.datasource.druid.max-evictable-idle-time-millis=
#打开removeAbandoned功能,多少时间内必须关闭连接
spring.datasource.druid.removeAbandoned=true
#1800秒,也就是30分钟
spring.datasource.druid.remove-abandoned-timeout=1800
#<!-- 1800秒,也就是30分钟 -->
spring.datasource.druid.log-abandoned=true
spring.datasource.druid.filters=mergeStat
#spring.datasource.druid.verifyServerCertificate
#spring.datasource.filters=stat,wall,log4j
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
spring.datasource.connectionProperties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000

基础概念

  • dao层:用于存放和数据库交互的文件,Mybatis 的interface都放在此层
  • service层:用于存放业务逻辑的文件。

配置 xml 文件存放的位置

  • Mybatis 中xml的文件默认是要和interface放在一个包下的,并且文件的名称要一样。
  • 在和 SpringBoot 整合后有两种配置方式,下面详细介绍。

application.properties 中设置

  • 既然是和 SpringBoot 整合,那么万变不离xxxAutoConfiguration这个配置类了,Mybatis 的配置类就是MybatisAutoConfiguration,如下:
@org.springframework.context.annotation.Configuration
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties(MybatisProperties.class)
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
public class MybatisAutoConfiguration implements InitializingBean {}
  • 可以看到@EnableConfigurationProperties(MybatisProperties.class)这行代码,就是将 properties 中的属性映射到 MybatisProperties 这个成员属性中,因此设置的方式就要看其中的属性。
public class MybatisProperties {
  //前缀
  public static final String MYBATIS_PREFIX = "mybatis";

  /**
   * Mybatis配置文件的位置
   */
  private String configLocation;

  /**
   * Mybatis的Mapper的xml文件的位置
   */
  private String[] mapperLocations;
  • 因此设置的方式很简单,如下:
## xml文件放置在/src/main/resource/mapper/文件夹下
mybatis.mapper-locations=classpath*:/mapper/**/*.xml

配置类中设置

  • 不是本章重点,后面在讲 Mybatis 和 SpringBoot 整合的文章会涉及到该内容。

配置扫描 Mybatis 的 interface

  • 在和 SpringBoot 整合后,扫描 Mybatis 的接口,生成代理对象是一件很简单的事,只需要一个注解即可。

@Mapper

  • 该注解标注在 Mybatis 的interface类上,SpringBoot 启动之后会扫描后会自动生成代理对象。实例如下:
@Mapper
public interface UserInfoMapper {

    int insert(UserInfo record);

    int insertSelective(UserInfo record);
    }
  • 缺点:每个interface都要标注一个,很鸡肋,一个项目中的 interface 少说也有上百个吧。

@MapperScan

  • @Mapper注解的升级版,标注在配置类上,用于一键扫描 Mybatis 的interface
  • 使用也是很简单的,直接指定接口所在的包即可,如下:
@MapperScan({"com.xxx.dao"})
public class ApiApplication {}
  • @MapperScan@Mapper这两个注解千万不要重复使用。
  • 优点:一键扫描,不用每个 interface 配置。

基本的 crud

  • 既然和数据库交互,避免不了 crud 操作,就安心做一个妥妥的crud boy吧。
  • 针对 Mybatis 其实有两套方法映射,一个是 XML 文件的方式,一个是注解的方式。但是今天只讲 XML 文件的方式,原因很简单,注解的方式企业不用,谁用谁倒霉,哈哈。

查询

  • 查询语句是 MyBatis 中最常用的元素之一——光能把数据存到数据库中价值并不大,还要能重新取出来才有用,多数应用也都是查询比修改要频繁。 MyBatis 的基本原则之一是:在每个插入、更新或删除操作之间,通常会执行多个查询操作。因此,MyBatis 在查询和结果映射做了相当多的改进。一个简单查询的 select 元素是非常简单的。
<select id="selectPersonById" parameterType="int" resultType="com.myjszl.domain.Person">
  SELECT name,age,id FROM PERSON WHERE ID = #{id}
</select>
  • 对应的interface的方法如下:
Person selectPersonById(int id);
  • <select>这个标签有很多属性,比较常用的属性如下:

    • id(必填):在命名空间中唯一的标识符,可以被用来引用这条语句。和interface中的方法名要一致。
    • parameterType(可选):将会传入这条语句的参数的类全限定名或别名。这个属性是可选的,因为 MyBatis 可以通过类型处理器(TypeHandler)推断出具体传入语句的参数,默认值为未设置(unset)。
    • resultType:期望从这条语句中返回结果的类全限定名或别名。 注意,如果返回的是集合,那应该设置为集合包含的类型,而不是集合本身的类型。 resultTyperesultMap 之间只能同时使用一个。
    • resultMap:对外部 resultMap 的命名引用。结果映射是 MyBatis 最强大的特性,如果你对其理解透彻,许多复杂的映射问题都能迎刃而解。 resultTyperesultMap 之间只能同时使用一个。

变更

  • 数据变更语句 insert,update 和 delete 的实现非常接近。
  • 下面是 insert,update 和 delete 语句的示例:
<insert id="insertAuthor">
  insert into Author (id,username,password,email,bio)
  values (#{id},#{username},#{password},#{email},#{bio})
</insert>

<update id="updateAuthor">
  update Author set
    username = #{username},
    password = #{password},
    email = #{email},
    bio = #{bio}
  where id = #{id}
</update>

<delete id="deleteAuthor">
  delete from Author where id = #{id}
</delete>

{}和&dollar;{}的区别

  • 上面的例子中我们可以看到使用的都是#{},关于#{}${}的区别也是在很多初级工程师的面试最常被问到的,现在只需要记住区别就是#{}使用了 JDBC 的预编译,可以防止 SQL 注入,提高了安全性,${}并没有预编译,安全性不够。在后面 Mybatis 的源码讲解中将会涉及到为什么一个用了预编译,一个没用。

自增 ID 的返回

  • 关于 Mysql 的文章中有提到,设计一个表最好要有一个自增 ID,无论这个 ID 你是否用到,具体原因不在解释,可以翻看之前的文章。
  • 有了自增 ID,插入之后并不能自动返回,但是我们又需要这个 ID 值,那么如何返回呢?
  • <insert>标签提供了两个属性用来解决这个问题,如下:

    • useGeneratedKeys:设置为 true,表示使用自增主键返回
    • keyProperty:指定返回的自增主键映射到parameterType的哪个属性中。
  • 假设插入Person,并且 person 表中的自增主键 id 需要返回,XML 文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xxx.dao.PersonMapper">
  <insert id='addPerson' parameterType='com.xxx.domain.Person' useGeneratedKeys="true"
    keyProperty="id" >
    insert into person(name,age)
    values(#{name},#{age});
  </insert>
</mapper>

SQL 代码片段

  • 这个元素可以用来定义可重用的 SQL 代码片段,以便在其它语句中使用。 参数可以静态地(在加载的时候)确定下来,并且可以在不同的 include 元素中定义不同的参数值。比如:
<sql id="userColumns"> ${alias}.id,${alias}.username,${alias}.password </sql>
  • 这个 SQL 片段可以在其它语句中使用,例如:
<select id="selectUsers" resultType="map">
  select
    <include refid="userColumns"><property name="alias" value="t1"/></include>,
    <include refid="userColumns"><property name="alias" value="t2"/></include>
  from some_table t1
    cross join some_table t2
</select>

开启驼峰映射

  • DBA 在设计数据库的时候,往往使用的是下划线(_)的方式,比如user_id。但是 Java 是不规范的,我们通常将它转换为userId,这就是驼峰命名方法。
  • 但是在使用 Mybatis 查询的时候,比如:
<select id='selectById' resultType='com.xxx.doamin.User'>
  select user_id from user_info
</select>
  • 上面的user_idUser中的userId根本不对应,也就映射不进去,此时查询的结果就是 userId 是 null,当然我们可以使用别名的方式,SQL 可以改写为select user_id as userId from user_info
  • 另外一种方式是不用别名,直接开启 Mybatis 的驼峰映射规则,会自动映射,开启的方式很简单,就是在application.properties文件配置一下,如下:
mybatis.configuration.map-underscore-to-camel-case=true

总结

  • 本文主要讲了 Mybatis 与 SpringBoot 的整合过程,基本的 crud,各种标签的属性等内容,属于一个入门级别的教程,后续的内容会逐渐深入。
  • 另外,MySQL 进阶的教程已经写了五篇文章了,每一篇都是经典,已经出了一个专辑,感兴趣的可以收藏一下MySQL 进阶
  • 感谢你的阅读,作者会定时的更新原创文章,如果觉得写的不错的话,可以关注一下本公众号。
查看原文

赞 0 收藏 0 评论 0

爱撒谎的男孩 发布了文章 · 4月20日

天天在用Redis,你知道哪些持久化方案吗?

前言

  • 文章首发于微信公众号【码猿技术专栏】:天天用Redis,持久化方案有哪些你知道吗?
  • Redis目前已经成为主流的内存数据库了,但是大部分人仅仅是停留在会用的阶段,你真的了解Redis内部的工作原理吗?
  • 今天这篇文章将为大家介绍Redis持久化的两种方案,文章将会从以下五个方面介绍:

    1. 什么是RDB,RDB如何实现持久化?
    2. 什么是AOF,AOF如何实现持久化?
    3. AOF和RDB的区别。
    4. 如何重启恢复数据?
    5. 持久化性能问题和解决方案

RDB

  • RDB持久化是把当前进程数据生成快照保存到硬盘的过程, 触发RDB持久化过程分为手动触发和自动触发。
  • RDB完成后会自动生成一个文件,保存在dir配置的指定目录下,文件名是dbfileName指定。
  • Redis默认会采用LZF算法对生成的RDB文件做压缩处理,压缩后的文件远远小于内存大小,默认开启。

手动触发

  • 手动触发的命令有savebgsave
  • save:该命令会阻塞Redis服务器,直到RDB的过程完成,已经被废弃,因此线上不建议使用。
  • bgsave:每次进行RDB过程都会fork一个子进程,由子进程完成RDB的操作,因此阻塞只会发生在fork阶段,一般时间很短。

自动触发

  • 除了手动触发RDB,Redis服务器内部还有如下几个场景能够自动触发RDB:

    1. 根据我们的 save m n 配置规则自动触发。
    2. 如果从节点执行全量复制操作, 主节点自动执行bgsave生成RDB文件并发送给从节点。
    3. 执行debug reload命令重新加载Redis时, 也会自动触发save操作。
    4. 默认情况下执行shutdown命令时, 如果没有开启AOF持久化功能则自动执行bgsave

RDB执行流程

  • RDB的主流方式就是bgsave,通过下图我们来看看RDB的执行流程:

RDB执行流程

  • 通过上图可以很清楚RDB的执行流程,如下:

    1. 执行bgsave命令后,会先判断是否存在AOF或者RDB的子进程,如果存在,直接返回。
    2. 父进程fork操作创建一个子进程,fork操作中父进程会被阻塞。
    3. fork完成后,子进程开始根据父进程的内存生成临时快照文件,完成后对原有的RDB文件进行替换。执行lastsave命令可以查看最近一次的RDB时间。
    4. 子进程完成后发送信号给父进程,父进程更新统计信息。

RDB的优点

  • RDB是一个紧凑压缩的二进制文件, 代表Redis在某个时间点上的数据快照。 非常适用于备份, 全量复制等场景。 比如每6小时执行bgsave备份,并把RDB文件拷贝到远程机器或者文件系统中,用于灾难恢复。
  • Redis加载RDB恢复数据远远快于AOF的方式。

RDB的缺点

  • RDB方式数据没办法做到实时持久化/秒级持久化。 因为bgsave每次运行都要执行fork操作创建子进程,属于重量级操作,频繁执行成本过高。
  • RDB文件使用特定二进制格式保存, Redis版本演进过程中有多个格式的RDB版本, 存在老版本Redis服务无法兼容新版RDB格式的问题。

AOF

  • AOF(append only file) 持久化: 以独立日志的方式记录每次写命令,重启时再重新执行AOF文件中的命令达到恢复数据的目的。 AOF的主要作用是解决了数据持久化的实时性, 目前已经是Redis持久化的主流方式

如何开启AOF

  • 开启AOF功能需要设置配置:appendonly yes, 默认不开启。 AOF文件名通过appendfilename配置设置, 默认文件名是appendonly.aof。 保存路径同RDB持久化方式一致,通过dir配置指定。

AOF整体的执行流程

  • AOF执行的流程大致分为命令写入文件同步文件重写重启加载四个步骤,如下图:

AOF执行流程

  • 从上图大致了解了AOF的执行流程,下面一一分析上述的四个步骤。

命令写入

  • AOF命令写入的内容直接是文本协议格式。 例如set hello world这条命

令, 在AOF缓冲区会追加如下文本:

*3\r\n$3\r\nset\r\n$5\r\nhello\r\n$5\r\nworld\r\n
  • 命令写入是直接写入到AOF的缓冲区中,至于为什么?原因很简单,Redis使用单线程响应命令,如果每次写AOF文件命令都直接追加到硬盘, 那么性能完全取决于当前硬盘负载。先写入缓冲区aof_buf中, 还有另一个好处, Redis可以提供多种缓冲区

同步硬盘的策略,在性能和安全性方面做出平衡。

文件同步

  • Redis提供了多种AOF缓冲区同步文件策略, 由参数appendfsync控制,如下:

    • 配置为always时, 每次写入都要同步AOF文件, 在一般的SATA硬盘上,Redis只能支持大约几百TPS写入, 显然跟Redis高性能特性背道而驰,不建议配置。
    • 配置为no,由于操作系统每次同步AOF文件的周期不可控,而且会加大每次同步硬盘的数据量,虽然提升了性能,但数据安全性无法保证。
    • 配置为everysec(默认的配置),是建议的同步策略, 也是默认配置,做到兼顾性能和数据安全性。理论上只有在系统突然宕机的情况下丢失1秒的数据(当然,这是不太准确的)。

文件重写机制

  • 随着命令不断写入AOF, 文件会越来越大, 为了解决这个问题, Redis引入AOF重写机制压缩文件体积。 AOF文件重写是把Redis进程内的数据转化为写命令同步到新AOF文件的过程。
  • 为什么要文件重写呢? 因为文件重写能够使得AOF文件的体积变得更小,从而使得可以更快的被Redis加载。
  • 重写过程分为手动触发和自动触发。

    • 手动触发直接使用bgrewriteaof命令。
    • 根据auto-aof-rewrite-min-sizeauto-aof-rewrite-percentage参数确定自动触发时机。
  • auto-aof-rewrite-min-size:表示运行AOF重写时文件最小体积, 默认为64MB。
  • auto-aof-rewrite-percentage:代表当前AOF文件空间(aof_current_size) 和上一次重写后AOF文件空间(aof_base_size) 的比值。
  • 自动触发时机相当于aof_current_size>auto-aof-rewrite-minsize&&(aof_current_size-aof_base_size) /aof_base_size>=auto-aof-rewritepercentage。其中aof_current_sizeaof_base_size可以在info Persistence统计信息中查看。
  • 那么文件重写后的AOF文件为什么会变小呢? 有如下几个原因:

    1. 进程内已经超时的数据将不会再次写入AOF文件中。
    2. 旧的AOF文件含有无效命令,如del key1hdel key2等。重写使用进程内数据直接生成,这样新的AOF文件只保留最终数据的写入命令。
    3. 多条写命令可以合并为一个, 如:lpush list alpush list blpush listc可以转化为:lpush list a b c。为了防止单条命令过大造成客户端缓冲区溢出,对于listsethashzset等类型操作,以64个元素为界拆分为多条。
  • 介绍了文件重写的系列知识,下面来看看Redis内部是如何进行文件重写的,如下图:

文件重写

  • 看完上图,大致了解了文件重写的流程,对于重写的流程,补充如下:

    1. 重写期间,主线程并没有阻塞,而是在执行其他的操作命令,依然会向旧的AOF文件写入数据,这样能够保证备份的最终完整性,如果数据重写失败,也能保证数据不会丢失。
    2. 为了把重写期间响应的写入信息也写入到新的文件中,因此也会为子进程保留一个缓冲区,防止新写的文件丢失数据。
    3. 重写是直接把当前内存的数据生成对应命令,并不需要读取老的AOF文件进行分析、命令合并。
    4. AOF文件直接采用的文本协议,主要是兼容性好、追加方便、可读性高可认为修改修复。
    5. 无论是RDB还是AOF都是先写入一个临时文件,然后通过重命名完成文件的替换。

AOF的优点

  • 使用 AOF 持久化会让 Redis 变得非常耐久:你可以设置不同的 fsync 策略,比如无 fsync ,每秒钟一次 fsync ,或者每次执行写入命令时 fsync 。 AOF 的默认策略为每秒钟 fsync 一次,在这种配置下,Redis 仍然可以保持良好的性能,并且就算发生故障停机,也最多只会丢失一秒钟的数据( fsync 会在后台线程执行,所以主线程可以继续努力地处理命令请求)。

AOF的缺点

  • 对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积。根据所使用的 fsync 策略,AOF 的速度可能会慢于 RDB。 在一般情况下, 每秒 fsync 的性能依然非常高, 而关闭 fsync 可以让 AOF 的速度和 RDB 一样快, 即使在高负荷之下也是如此。不过在处理巨大的写入载入时,RDB 可以提供更有保证的最大延迟时间。
  • 数据恢复速度相对于RDB比较慢。

AOF和RDB的区别

  • RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。
  • AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。

重启加载

  • 无论是RDB还是AOF都可用于服务器重启时的数据恢复,执行流程如下图:

重启加载流程

  • 上图很清晰的分析了Redis启动恢复数据的流程,先检查AOF文件是否开启,文件是否存在,再检查RDB是否开启,文件是否存在。

性能问题与解决方案

  • 通过上面的分析,我们都知道RDB的快照、AOF的重写都需要fork,这是一个重量级操作,会对Redis造成阻塞。因此为了不影响Redis主进程响应,我们需要尽可能降低阻塞。
  • 那么如何减少fork操作的阻塞呢?

    1. 优先使用物理机或者高效支持fork操作的虚拟化技术。
    2. 控制Redis实例最大可用内存, fork耗时跟内存量成正比, 线上建议每个Redis实例内存控制在10GB以内。
    3. 合理配置Linux内存分配策略,避免物理内存不足导致fork失败。
    4. 降低fork操作的频率,如适度放宽AOF自动触发时机,避免不必要的全量复制等。

总结

  • 本文介绍了Redis持久化的两种不同的策略,大部分内容是运维人员需要掌握的,当然作为后端人员也是需要了解一下,毕竟小公司都是一人搞全栈,哈哈。
  • 如果觉得陈某写的不错,有所收获的话,关注分享一波,你的关注将是陈某写作的最大动力,谢谢支持!!!
查看原文

赞 1 收藏 1 评论 0

爱撒谎的男孩 发布了文章 · 4月19日

面试官:你知道哪几种事务失效的场景?

面试官:你知道哪些事务失效的场景?

前言

  • 文章首发于面试官:你知道哪些事务失效的场景?
  • 声明式事务是Spring功能中最爽之一,可是有些时候,我们在使用声明式事务并未生效,这是为什么呢?
  • 今天陈某带大家来聊一聊声明事务的几种失效场景。本文将会从以下两个方面来说一下事务为什么会失效?

    1. @Transactional介绍
    2. @Transactional失效场景

@Transactional介绍

  • @Transactional是声明式事务的注解,可以被标记在类上接口方法上。
  • 该注解中有很多值得深入了解的几种属性,我们来看一下。

transactionManager

  • 指定事务管理器,值为bean的名称,这个主要用于多事务管理器情况下指定。比如多数据源配置的情况下。

isolation

  • 事务的隔离级别,默认是Isolation.DEFAULT
  • 几种值的含义如下:

    • Isolation.DEFAULT:事务默认的隔离级别,使用数据库默认的隔离级别。
    • Isolation.READ_UNCOMMITTED:这是事务最低的隔离级别,它充许别外一个事务可以看到这个事务未提交的数据。这种隔离级别会产生脏读,不可重复读和幻读。
    • Isolation.READ_COMMITTED:保证一个事务修改的数据提交后才能被另外一个事务读取。另外一个事务不能读取该事务未提交的数据。这种事务隔离级别可以避免脏读出现,但是可能会出现不可重复读和幻读。
    • Isolation.REPEATABLE_READ:这种事务隔离级别可以防止脏读,不可重复读。但是可能出现幻读。
    • Isolation.SERIALIZABLE:这是花费最高代价但是最可靠的事务隔离级别。事务被处理为顺序执行。除了防止脏读,不可重复读外,还避免了幻读。

propagation

  • 代表事务的传播行为,默认值为Propagation.REQUIRED
  • Propagation.REQUIRED:如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务。比如A方法内部调用了B方法,此时B方法将会使用A方法的事务。
  • Propagation.MANDATORY:支持当前事务,如果当前没有事务,就抛出异常。
  • Propagation.NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
  • Propagation.NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
  • Propagation.REQUIRES_NEW:新建事务,如果当前存在事务,把当前事务挂起。比如A方法使用默认的事务传播属性,B方法使用REQUIRES_NEW,此时A方法在内部调用B方法,一旦A方法出现异常,A方法中的事务回滚了,但是B方法并没有回滚,因为A和B方法使用的不是同一个事务,B方法新建了一个事务。
  • Propagation.NESTED:支持当前事务,新增Savepoint点,也就是在进入子事务之前,父事务建立一个回滚点,与当前事务同步提交或回滚。 子事务是父事务的一部分,在父事务还未提交时,子事务一定没有提交。嵌套事务一个非常重要的概念就是内层事务依赖于外层事务。外层事务失败时,会回滚内层事务所做的动作。而内层事务操作失败并不会引起外层事务的回滚。

timeout

  • 事务的超时时间,单位为秒。

readOnly

  • 该属性用于设置当前事务是否为只读事务,设置为true表示只读,false则表示可读写,默认值为false。如果一个事务只涉及到只读,可以设置为true。

rollbackFor 属性

  • 用于指定能够触发事务回滚的异常类型,可以指定多个异常类型。
  • 默认是在RuntimeExceptionError上回滚。

noRollbackFor

  • 抛出指定的异常类型,不回滚事务,也可以指定多个异常类型。

@Transactional失效场景

  • 声明式事务失效的场景有很多,陈某这里只是罗列一下几种常见的场景。

底层数据库引擎不支持事务

  • 如果数据库引擎不支持事务,则Spring自然无法支持事务。

在非public修饰的方法使用

  • @Transactional注解使用的是AOP,在使用动态代理的时候只能针对public方法进行代理,源码依据在AbstractFallbackTransactionAttributeSource类中的computeTransactionAttribute方法中,如下:
protected TransactionAttribute computeTransactionAttribute(Method method,
    Class<?> targetClass) {
        // Don't allow no-public methods as required.
        if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
        return null;
}
复制代码
  • 此处如果不是标注在public修饰的方法上并不会抛出异常,但是会导致事务失效。

异常被 " 踹死了 "

  • 这种情况小白是最容易犯错的,在整个事务的方法中使用try-catch,导致异常无法抛出,自然会导致事务失效。伪代码如下:
@Transactional
public void method(){
  try{
    //插入一条数据
    //更改一条数据
  }catch(Exception ex){
    return;
  }
}
复制代码

方法中调用同类的方法

  • 简单的说就是一个类中的A方法(未标注声明式事务)在内部调用了B方法(标注了声明式事务),这样会导致B方法中的事务失效。
  • 代码如下:
public class Test{
  public void A(){
    //插入一条数据
    //调用B方法
    B();
  }
  
  @Transactional
  public void B(){
    //插入数据
  }
}
复制代码
  • 为什么会失效呢?:其实原因很简单,Spring在扫描Bean的时候会自动为标注了@Transactional注解的类生成一个代理类(proxy),当有注解的方法被调用的时候,实际上是代理类调用的,代理类在调用之前会开启事务,执行事务的操作,但是同类中的方法互相调用,相当于this.B(),此时的B方法并非是代理类调用,而是直接通过原有的Bean直接调用,所以注解会失效。
  • 如何解决呢?:这就涉及到注解失效的原因了,后续文章会介绍到,这里不过多介绍了。

rollbackFor属性设置错误

  • 很容易理解,指定异常触发回滚,一旦设置错误,导致一些异常不能触发回滚,此时的声明式事务不就失效了吗。

noRollbackFor属性设置错误

  • 这个和rollbackFor属性设置错误类似,一旦设置错误,也会导致异常不能触发回滚,此时的声明式事务会失效。

propagation属性设置错误

  • 事务的传播属性在上面已经介绍了,默认的事务传播属性是Propagation.REQUIRED,但是一旦配置了错误的传播属性,也是会导致事务失效,如下三种配置将会导致事务失效:

    • Propagation.SUPPORTS
    • Propagation.NOT_SUPPORTED
    • Propagation.NEVER

原始SSM项目,重复扫描导致事务失效

  • 在原始的SSM项目中都配置了context:component-scan并且同时扫描了service层,此时事务将会失效。
  • 按照Spring配置文件的加载顺序来说,会先加载Springmvc的配置文件,如果在加载Springmvc配置文件的时候把service也加载了,但是此时事务还没加载,将会导致事务无法成功生效。
  • 解决方法很简单,把扫描service层的配置设置在Spring配置文件或者其他配置文件中即可。

总结

  • 事务失效的原因很多,但是千万不要做到一知半解,只有深入理解了,才能在面试过程中对答如流。
  • 今天的文章就到此结束了,如果觉得陈某写得不错,有所收获的,关注在看来一波,欢迎各位朋友关注陈某的公众号!!!
查看原文

赞 0 收藏 0 评论 0

爱撒谎的男孩 发布了文章 · 4月11日

拜托,别再问我Zookeeper如何实现分布式锁了!!!

导读

  • 文章首发于微信公众号拜托,别再问我Zookeeper如何实现分布式锁了!!!
  • 真是有人()的地方就有江湖(事务),今天不谈江湖,来撩撩人。
  • 分布式锁的概念、为什么使用分布式锁,想必大家已经很清楚了。前段时间作者写过Redis是如何实现分布式锁,今天这篇文章来谈谈Zookeeper是如何实现分布式锁的。
  • 陈某今天分别从如下几个方面来详细讲讲ZK如何实现分布式锁:

    1. 「ZK的四种节点」
    2. 「排它锁的实现」
    3. 「读写锁的实现」
    4. 「Curator实现分步式锁」

ZK的四种节点

  • 持久性节点:节点创建后将会一直存在
  • 临时节点:临时节点的生命周期和当前会话绑定,一旦当前会话断开临时节点也会删除,当然可以主动删除。
  • 持久有序节点:节点创建一直存在,并且zk会自动为节点加上一个自增的后缀作为新的节点名称。
  • 临时有序节点:保留临时节点的特性,并且zk会自动为节点加上一个自增的后缀作为新的节点名称。

排它锁的实现

  • 排他锁的实现相对简单一点,利用了「zk的创建节点不能重名的特性」。如下图:

  • 根据上图分析大致分为如下步骤:

    1. 尝试获取锁:创建临时节点,zk会保证只有一个客户端创建成功。
    2. 创建临时节点成功,获取锁成功,执行业务逻辑,业务执行完成后删除锁。
    3. 创建临时节点失败,阻塞等待。
    4. 监听删除事件,一旦临时节点删除了,表示互斥操作完成了,可以再次尝试获取锁。
    5. 递归:获取锁的过程是一个递归的操作,获取锁->监听->获取锁
  • 「如何避免死锁」:创建的是临时节点,当服务宕机会话关闭后临时节点将会被删除,锁自动释放。

代码实现

  • 作者参照JDK锁的实现方式加上模板方法模式的封装,封装接口如下:
/**
 * @Description ZK分布式锁的接口
 * @Author 陈某
 * @Date 2020/4/7 22:52
 */
public interface ZKLock {
    /**
     * 获取锁
     */
    void lock() throws Exception;

    /**
     * 解锁
     */
    void unlock() throws Exception;
}
复制代码
  • 模板抽象类如下:
/**
 * @Description 排他锁,模板类
 * @Author 陈某
 * @Date 2020/4/7 22:55
 */
public abstract class AbstractZKLockMutex implements ZKLock {

    /**
     * 节点路径
     */
    protected String lockPath;

    /**
     * zk客户端
     */
    protected CuratorFramework zkClient;

    private AbstractZKLockMutex(){}

    public AbstractZKLockMutex(String lockPath,CuratorFramework client){
        this.lockPath=lockPath;
        this.zkClient=client;
    }

    /**
     * 模板方法,搭建的获取锁的框架,具体逻辑交于子类实现
     * @throws Exception
     */
    @Override
    public final void lock() throws Exception {
        //获取锁成功
        if (tryLock()){
            System.out.println(Thread.currentThread().getName()+"获取锁成功");
        }else{  //获取锁失败
            //阻塞一直等待
            waitLock();
            //递归,再次获取锁
            lock();
        }
    }

    /**
     * 尝试获取锁,子类实现
     */
    protected abstract boolean tryLock() ;


    /**
     * 等待获取锁,子类实现
     */
    protected abstract void waitLock() throws Exception;


    /**
     * 解锁:删除节点或者直接断开连接
     */
    @Override
    public  abstract void unlock() throws Exception;
}
复制代码
  • 排他锁的具体实现类如下:
/**
 * @Description 排他锁的实现类,继承模板类 AbstractZKLockMutex
 * @Author 陈某
 * @Date 2020/4/7 23:23
 */
@Data
public class ZKLockMutex extends AbstractZKLockMutex {

    /**
     * 用于实现线程阻塞
     */
    private CountDownLatch countDownLatch;

    public ZKLockMutex(String lockPath,CuratorFramework zkClient){
        super(lockPath,zkClient);
    }

    /**
     * 尝试获取锁:直接创建一个临时节点,如果这个节点存在创建失败抛出异常,表示已经互斥了,
     * 反之创建成功
     * @throws Exception
     */
    @Override
    protected boolean tryLock()  {
        try {
            zkClient.create()
                    //临时节点
                    .withMode(CreateMode.EPHEMERAL)
                    //权限列表 world:anyone:crdwa
                    .withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)
                    .forPath(lockPath,"lock".getBytes());
            return true;
        }catch (Exception ex){
            return false;
        }
    }


    /**
     * 等待锁,一直阻塞监听
     * @return  成功获取锁返回true,反之返回false
     */
    @Override
    protected void waitLock() throws Exception {
        //监听节点的新增、更新、删除
        final NodeCache nodeCache = new NodeCache(zkClient, lockPath);
        //启动监听
        nodeCache.start();
        ListenerContainer<NodeCacheListener> listenable = nodeCache.getListenable();

        //监听器
        NodeCacheListener listener=()-> {
            //节点被删除,此时获取锁
            if (nodeCache.getCurrentData() == null) {
                //countDownLatch不为null,表示节点存在,此时监听到节点删除了,因此-1
                if (countDownLatch != null)
                    countDownLatch.countDown();
            }
        };
        //添加监听器
        listenable.addListener(listener);

        //判断节点是否存在
        Stat stat = zkClient.checkExists().forPath(lockPath);
        //节点存在
        if (stat!=null){
            countDownLatch=new CountDownLatch(1);
            //阻塞主线程,监听
            countDownLatch.await();
        }
        //移除监听器
        listenable.removeListener(listener);
    }

    /**
     * 解锁,直接删除节点
     * @throws Exception
     */
    @Override
    public void unlock() throws Exception {
        zkClient.delete().forPath(lockPath);
    }
}
复制代码

可重入性排他锁如何设计

  • 可重入的逻辑很简单,在本地保存一个ConcurrentMapkey是当前线程,value是定义的数据,结构如下:
 private final ConcurrentMap<Thread, LockData> threadData = Maps.newConcurrentMap();
复制代码
  • 重入的伪代码如下:
public boolean tryLock(){
    //判断当前线程是否在threadData保存过
    //存在,直接return true
    //不存在执行获取锁的逻辑
    //获取成功保存在threadData中
}
复制代码

读写锁的实现

  • 读写锁分为读锁和写锁,区别如下:

    • 读锁允许多个线程同时读数据,但是在读的同时不允许写线程修改。
    • 写锁在获取后,不允许多个线程同时写或者读。
  • 如何实现读写锁?ZK中有一类节点叫临时有序节点,上文有介绍。下面我们来利用临时有序节点来实现读写锁的功能。

读锁的设计

  • 读锁允许多个线程同时进行读,并且在读的同时不允许线程进行写操作,实现原理如下图:

  • 根据上图,获取一个读锁分为以下步骤:

    1. 创建临时有序节点(当前线程拥有的读锁或称作读节点)。
    2. 获取路径下所有的子节点,并进行从小到大排序
    3. 获取当前节点前的临近写节点(写锁)。
    4. 如果不存在的临近写节点,则成功获取读锁。
    5. 如果存在临近写节点,对其监听删除事件。
    6. 一旦监听到删除事件,「重复2,3,4,5的步骤(递归)」

写锁的设计

  • 线程一旦获取了写锁,不允许其他线程读和写。实现原理如下:

  • 从上图可以看出唯一和写锁不同的就是监听的节点,这里是监听临近节点(读节点或者写节点),读锁只需要监听写节点,步骤如下:

    1. 创建临时有序节点(当前线程拥有的写锁或称作写节点)。
    2. 获取路径下的所有子节点,并进行从小到大排序。
    3. 获取当前节点的临近节点(读节点和写节点)。
    4. 如果不存在临近节点,则成功获取锁。
    5. 如果存在临近节点,对其进行监听删除事件。
    6. 一旦监听到删除事件,「重复2,3,4,5的步骤(递归)」

如何监听

  • 无论是写锁还是读锁都需要监听前面的节点,不同的是读锁只监听临近的写节点,写锁是监听临近的所有节点,抽象出来看其实是一种链式的监听,如下图:

  • 每一个节点都在监听前面的临近节点,一旦前面一个节点删除了,再从新排序后监听前面的节点,这样递归下去。

代码实现

  • 作者简单的写了读写锁的实现,先造出来再优化,不建议用在生产环境。代码如下:
public class ZKLockRW  {

    /**
     * 节点路径
     */
    protected String lockPath;

    /**
     * zk客户端
     */
    protected CuratorFramework zkClient;

    /**
     * 用于阻塞线程
     */
    private CountDownLatch countDownLatch=new CountDownLatch(1);


    private final static String WRITE_NAME="_W_LOCK";

    private final static String READ_NAME="_R_LOCK";


    public ZKLockRW(String lockPath, CuratorFramework client) {
        this.lockPath=lockPath;
        this.zkClient=client;
    }

    /**
     * 获取锁,如果获取失败一直阻塞
     * @throws Exception
     */
    public void lock() throws Exception {
        //创建节点
        String node = createNode();
        //阻塞等待获取锁
        tryLock(node);
        countDownLatch.await();
    }

    /**
     * 创建临时有序节点
     * @return
     * @throws Exception
     */
    private String createNode() throws Exception {
        //创建临时有序节点
       return zkClient.create()
                .withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
                .withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)
                .forPath(lockPath);
    }

    /**
     * 获取写锁
     * @return
     */
    public  ZKLockRW writeLock(){
        return new ZKLockRW(lockPath+WRITE_NAME,zkClient);
    }

    /**
     * 获取读锁
     * @return
     */
    public  ZKLockRW readLock(){
        return new ZKLockRW(lockPath+READ_NAME,zkClient);
    }

    private void tryLock(String nodePath) throws Exception {
        //获取所有的子节点
        List<String> childPaths = zkClient.getChildren()
                .forPath("/")
                .stream().sorted().map(o->"/"+o).collect(Collectors.toList());


        //第一个节点就是当前的锁,直接获取锁。递归结束的条件
        if (nodePath.equals(childPaths.get(0))){
            countDownLatch.countDown();
            return;
        }

        //1. 读锁:监听最前面的写锁,写锁释放了,自然能够读了
        if (nodePath.contains(READ_NAME)){
            //查找临近的写锁
            String preNode = getNearWriteNode(childPaths, childPaths.indexOf(nodePath));
            if (preNode==null){
                countDownLatch.countDown();
                return;
            }
            NodeCache nodeCache=new NodeCache(zkClient,preNode);
            nodeCache.start();
            ListenerContainer<NodeCacheListener> listenable = nodeCache.getListenable();
            listenable.addListener(() -> {
                //节点删除事件
                if (nodeCache.getCurrentData()==null){
                    //继续监听前一个节点
                    String nearWriteNode = getNearWriteNode(childPaths, childPaths.indexOf(preNode));
                    if (nearWriteNode==null){
                        countDownLatch.countDown();
                        return;
                    }
                    tryLock(nearWriteNode);
                }
            });
        }

        //如果是写锁,前面无论是什么锁都不能读,直接循环监听上一个节点即可,直到前面无锁
        if (nodePath.contains(WRITE_NAME)){
            String preNode = childPaths.get(childPaths.indexOf(nodePath) - 1);
            NodeCache nodeCache=new NodeCache(zkClient,preNode);
            nodeCache.start();
            ListenerContainer<NodeCacheListener> listenable = nodeCache.getListenable();
            listenable.addListener(() -> {
                //节点删除事件
                if (nodeCache.getCurrentData()==null){
                    //继续监听前一个节点
                    tryLock(childPaths.get(childPaths.indexOf(preNode) - 1<0?0:childPaths.indexOf(preNode) - 1));
                }
            });
        }
    }

    /**
     * 查找临近的写节点
     * @param childPath 全部的子节点
     * @param index 右边界
     * @return
     */
    private String  getNearWriteNode(List<String> childPath,Integer index){
        for (int i = 0; i < index; i++) {
            String node = childPath.get(i);
            if (node.contains(WRITE_NAME))
                return node;

        }
        return null;
    }

}
复制代码

Curator实现分步式锁

  • Curator是Netflix公司开源的一个Zookeeper客户端,与Zookeeper提供的原生客户端相比,Curator的抽象层次更高,简化了Zookeeper客户端的开发量。
  • Curator在分布式锁方面已经为我们封装好了,大致实现的思路就是按照作者上述的思路实现的。中小型互联网公司还是建议直接使用框架封装好的,毕竟稳定,有些大型的互联公司都是手写的,牛逼啊。
  • 创建一个排他锁很简单,如下:
//arg1:CuratorFramework连接对象,arg2:节点路径
lock=new InterProcessMutex(client,path);
//获取锁
lock.acquire();
//释放锁
lock.release();
复制代码
  • 更多的API请参照官方文档,不是此篇文章重点。
  • 「至此ZK实现分布式锁就介绍完了,如有想要源码的朋友,老规矩,关注微信公众号【码猿技术专栏】,回复关键词分布式锁获取。」

一点小福利

  • 对于Zookeeper不太熟悉的朋友,陈某特地花费两天时间总结了ZK的常用知识点,包括ZK常用shell命令、ZK权限控制、Curator的基本操作API。目录如下:
  • 「需要上面PDF文件的朋友,老规矩,关注微信公众号【码猿技术专栏】回复关键词ZK总结。」
查看原文

赞 0 收藏 0 评论 0

爱撒谎的男孩 发布了文章 · 4月11日

一文带你入门Zookeeper

一文带你入门Zookeeper

导读

  • 文章首发于万字长文带你入门Zookeeper
  • Zookeeper 相信大家都听说过,最典型的使用就是作为服务注册中心。今天陈某带大家从零基础入门 Zookeeper,看了本文,你将会对 Zookeeper 有了初步的了解和认识。
  • 注意:本文基于 Zookeeper 的版本是 3.4.14,最新版本的在使用上会有一些出入,但是企业现在使用的大部分都是 3.4x 版本的。

Zookeeper 概述

  • Zookeeper 是一个分布式协调服务的开源框架。主要用来解决分布式集群中应用系统的一致性问题,例如怎样避免同时操作同一数据造成脏读的问题。
  • ZooKeeper 本质上是一个分布式的小文件存储系统。提供基于类似于文件系 统的目录树方式的数据存储,并且可以对树中的节点进行有效管理。从而用来维护和监控你存储的数据的状态变化。通过监控这些数据状态的变化,从而可以达 到基于数据的集群管理。诸如:统一命名服务分布式配置管理分布式消息队列分布式锁分布式协调等功能。

Zookeeper 特性

  1. 全局数据一致:每个 server 保存一份相同的数据副本,client 无论连 接到哪个 server,展示的数据都是一致的,这是最重要的特征;
  2. 可靠性:如果消息被其中一台服务器接受,那么将被所有的服务器接受。
  3. 顺序性:包括全局有序和偏序两种:全局有序是指如果在一台服务器上 消息 a 在消息 b 前发布,则在所有 Server 上消息 a 都将在消息 b 前被 发布;偏序是指如果一个消息 b 在消息 a 后被同一个发送者发布,a 必将排在 b 前面。
  4. 数据更新原子性:一次数据更新要么成功(半数以上节点成功),要么失 败,不存在中间状态;
  5. 实时性:Zookeeper 保证客户端将在一个时间间隔范围内获得服务器的更新信息,或者服务器失效的信息。

Zookeeper 节点类型

  • Znode 有两种,分别为临时节点和永久节点。

    • 临时节点:该节点的生命周期依赖于创建它们的会话。一旦会话结束,临时节点将被自动删除,当然可以也可以手动删除。临时节点不允许拥有子节点。
    • 永久节点:该节点的生命周期不依赖于会话,并且只有在客户端显示执行删除操作的时候,他们才能被删除。
  • 节点的类型在创建时即被确定,并且不能改变。
  • Znode 还有一个序列化的特性,如果创建的时候指定的话,该 Znode 的名字后面会自动追加一个不断增加的序列号。序列号对于此节点的父节点来说是唯一的,这样便会记录每个子节点创建的先后顺序。它的格式为"%10d"(10 位数字,没有数值的数位用 0 补充,例如“0000000001”)。
  • 这样便会存在四种类型的 Znode 节点,分类如下:

    • PERSISTENT:永久节点
    • EPHEMERAL:临时节点
    • PERSISTENT_SEQUENTIAL:永久节点、序列化
    • EPHEMERAL_SEQUENTIAL:临时节点、序列化

ZooKeeper Watcher

  • ZooKeeper 提供了分布式数据发布/订阅功能,一个典型的发布/订阅模型系统定义了一种一对多的订阅关系,能让多个订阅者同时监听某一个主题对象,当这个主题对象自身状态变化时,会通知所有订阅者,使他们能够做出相应的处理。
  • 触发事件种类很多,如:节点创建,节点删除,节点改变,子节点改变等。
  • 总的来说可以概括 Watcher 为以下三个过程:客户端向服务端注册 Watcher、服务端事件发生触发 Watcher、客户端回调 Watcher 得到触发事件情况。

Watcher 机制特点

  • 一次性触发:事件发生触发监听,一个 watcher event 就会被发送到设置监听的客户端,这种效果是一次性的,后续再次发生同样的事件,不会再次触发。
  • 事件封装:ZooKeeper 使用 WatchedEvent 对象来封装服务端事件并传递。WatchedEvent 包含了每一个事件的三个基本属性:通知状态(keeperState),事件类型(EventType)和节点路径(path)。
  • event 异步发送:watcher 的通知事件从服务端发送到客户端是异步的。
  • 先注册再触发:Zookeeper 中的 watch 机制,必须客户端先去服务端注册监听,这样事件发送才会触发监听,通知给客户端。

常用 Shell 命令

新增节点

create [-s] [-e] path data
复制代码
  • -s:表示创建有序节点
  • -e:表示创建临时节点
  • 创建持久化节点:
create /test 1234

## 子节点
create /test/node1 node1
复制代码
  • 创建持久化有序节点:
## 完整的节点名称是a0000000001
create /a a
Created /a0000000001

## 完整的节点名称是b0000000002
create /b b
Created /b0000000002
复制代码
  • 创建临时节点:
create -e /a a
复制代码
  • 创建临时有序节点:
## 完整的节点名称是a0000000001
create -e -s /a a
Created /a0000000001
复制代码

更新节点

set [path] [data] [version]
复制代码
  • path:节点路径
  • data:数据
  • version:版本号
  • 修改节点数据:
set /test aaa

## 修改子节点
set /test/node1 bbb
复制代码
  • 基于数据版本号修改,如果修改的节点的版本号(dataVersion)不正确,拒绝修改
set /test aaa 1
复制代码

删除节点

delete [path] [version]
复制代码
  • path:节点路径
  • version:版本号,版本号不正确拒绝删除
  • 删除节点
delete /test

## 版本号删除
delete /test 2
复制代码
  • 递归删除,删除某个节点及后代
rmr /test
复制代码

查看节点数据和状态

  • 命令格式如下:
get path
复制代码
  • 获取节点详情:
## 获取节点详情
get /node1

## 节点内容
aaa
cZxid = 0x6
ctime = Sun Apr 05 14:50:10 CST 2020
mZxid = 0x6
mtime = Sun Apr 05 14:50:10 CST 2020
pZxid = 0x7
cversion = 1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 3
numChildren = 1
复制代码
  • 节点各个属性对应的含义如下:

    • cZxid:数据节点创建时的事务 ID。
    • ctime:数据节点创建时间。
    • mZxid:数据节点最后一次更新时的事务 ID。
    • mtime:数据节点最后一次更新的时间。
    • pZxid:数据节点的子节点最后一次被修改时的事务 ID。
    • cversion:子节点的更改次数。
    • dataVersion:节点数据的更改次数。
    • aclVersion:节点 ACL 的更改次数。
    • ephemeralOwner:如果节点是临时节点,则表示创建该节点的会话的 SessionID。如果节点是持久化节点,值为 0。
    • dataLength:节点数据内容的长度。
    • numChildren:数据节点当前的子节点的个数。

查看节点状态

stat path
复制代码
  • stat命令和get命令相似,不过这个命令不会返回节点的数据,只返回节点的状态属性。
stat /node1

## 节点状态信息,没有节点数据
cZxid = 0x6
ctime = Sun Apr 05 14:50:10 CST 2020
mZxid = 0x6
mtime = Sun Apr 05 14:50:10 CST 2020
pZxid = 0x7
cversion = 1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 3
numChildren = 1

复制代码

查看节点列表

  • 查看节点列表有ls pathls2 path两个命令。后者是前者的增强,不仅会返回节点列表还会返回当前节点的状态信息。
  • ls path
ls /

## 仅仅返回节点列表
[zookeeper, node1]
复制代码
  • ls2 path
ls2 /

## 返回节点列表和当前节点的状态信息
[zookeeper, node1]
cZxid = 0x0
ctime = Thu Jan 01 08:00:00 CST 1970
mZxid = 0x0
mtime = Thu Jan 01 08:00:00 CST 1970
pZxid = 0x6
cversion = 2
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 0
numChildren = 2
复制代码

监听器 get path watch

  • 使用get path watch注册的监听器在节点内容发生改变时,向客户端发送通知,注意 Zookeeper 的触发器是一次性的,触发一次后会立即生效。
get /node1 watch

## 改变节点数据
set /node1 bbb

## 监听到节点内容改变了
WATCHER::
WatchedEvent state:SyncConnected type:NodeDataChanged path:/node1
复制代码

监听器 stat path watch

  • stat path watch注册的监听器能够在节点状态发生改变时向客户端发出通知。比如节点数据改变、节点被删除等。
stat /node2 watch

## 删除节点node2
delete /node2

## 监听器监听到了节点删除
WATCHER::
WatchedEvent state:SyncConnected type:NodeDeleted path:/node2
复制代码

监听器 ls/ls2 path watch

  • 使用ls path watch或者ls2 path watch注册的监听器,能够监听到该节点下的子节点的增加删除操作。
ls /node1 watch

## 创建子节点
create /node1/b b

## 监听到了子节点的新增
WATCHER::
WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/node1
复制代码

Zookeeper 的 ACL 权限控制

  • zookeeper 类似文件控制系统,client 可以创建,删除,修改,查看节点,那么如何做到权限控制的呢?zookeeper 的access control list访问控制列表可以做到这一点。
  • ACL 权限控制,使用scheme:id:permission来标识。

    • 权限模式(scheme):授权的策略
    • 授权对象(id):授权的对象
    • 权限(permission):授予的权限
  • 权限控制是基于每个节点的,需要对每个节点设置权限。
  • 每个节点支持设置多种权限控制方案和多个权限。
  • 子节点不会继承父节点的权限,客户端无权访问某节点,但可能可以访问它的子节点。
  • 例如:根据 IP 地址进行授权,命令如下:
setACl /node1 ip:192.168.10.1:crdwa
复制代码

权限模式

  • 权限模式即是采用何种方式授权。
  • world:只有一个用户,anyone,表示登录 zookeeper 所有人(默认的模式)。
  • ip:对客户端使用 IP 地址认证。
  • auth:使用已添加认证的用户认证。
  • digest:使用用户名:密码方式认证。

授权对象

  • 给谁授权,授权对象的 ID 指的是权限赋予的实体,例如 IP 地址或用户。

授予的权限

  • 授予的权限包括createdeletereadwriteradmin。也就是增、删、改、查、管理的权限,简写cdrwa
  • 注意:以上 5 种权限中,delete是指对子节点的删除权限,其他 4 种权限是对自身节点的操作权限。
  • create:简写c,可以创建子节点。
  • delete:简写d,可以删除子节点(仅下一级节点)。
  • read:简写r,可以读取节点数据以及显示子节点列表。
  • write:简写w,可以更改节点数据。
  • admin:简写a,可以设置节点访问控制列表权限。

授权相关命令

  • getAcl [path]:读取指定节点的 ACL 权限。
  • setAcl [path] [acl]:设置 ACL
  • addauth <scheme> <auth>:添加认证用户,和 auth,digest 授权模式相关。

world 授权模式案例

  • zookeeper 中默认的授权模式,针对登录 zookeeper 的任何用户授予指定的权限。命令如下:
setAcl [path] world:anyone:[permission]
复制代码
  • path:节点
  • permission:授予的权限,比如cdrwa
  • 去掉不能读取节点数据的权限:
## 获取权限列表(默认的)
getAcl /node2

'world,'anyone
: cdrwa

## 去掉读取节点数据的的权限,去掉r
setAcl /node2 world:anyone:cdwa

## 再次获取权限列表
getAcl /node2

'world,'anyone
: cdwa

## 获取节点数据,没有权限,失败
get /node2

Authentication is not valid : /node2
复制代码

IP 授权模式案例

  • 针对登录用户的 ip 进行限制权限。命令如下:
setAcl [path] ip:[ip]:[acl]
复制代码
  • 远程登录 zookeeper 的命令如下:
./zkCli.sh -server ip
复制代码
  • 设置192.168.10.1这个 ip 的增删改查管理的权限。
setAcl /node2 ip:192.168.10.1:crdwa
复制代码

Auth 授权模式案例

  • auth 授权模式需要有一个认证用户,添加命令如下:
addauth digest [username]:[password]
复制代码
  • 设置 auth 授权模式命令如下:
setAcl [path] auth:[user]:[acl]
复制代码
  • chenmou这个账户添加 cdrwa 权限:
## 添加一个认证账户
addauth digest chenmou:123456

## 添加权限
setAcl /node2 auth:chenmou:crdwa
复制代码

多种模式授权

  • zookeeper 中同一个节点可以使用多种授权模式,多种授权模式用,分隔。
## 创建节点
create /node3

## 添加认证用户
addauth chenmou:123456

## 添加多种授权模式
setAcl /node3 ip:192.178.10.1:crdwa,auth:chenmou:crdwa
复制代码

ACL 超级管理员

  • zookeeper 的权限管理模式有一种叫做super,该模式提供一个超管可以方便的访问任何权限的节点。
  • 假设这个超管是super:admin,需要先为超管生成密码的密文:
echo -n super:admin | openssl dgst  -binary -sha1 |openssl base64

## 执行完生成了秘钥
xQJmxLMiHGwaqBvst5y6rkB6HQs=
复制代码
  • 打开zookeeper目录下/bin/zkServer.sh,找到如下一行:
nohup JAVA&quot;−Dzookeeper.log.dir=JAVA"−Dzookeeper.log.dir={ZOO_LOG_DIR}" "-Dzookeeper.root.logger=${ZOO_LOG4J_PROP}"
复制代码
  • 在后面添加一行脚本,如下:
"-Dzookeeper.DigestAuthenticationProvider.superDigest=super:xQJmxLMiHGwaqBvst5y6rkB6HQs="
复制代码
  • 此时完整的脚本如下:
nohup "$JAVA" "-Dzookeeper.log.dir=${ZOO_LOG_DIR}" "-Dzookeeper.root.logger=${ZOO_LOG4J_PROP}" "-Dzookeeper.DigestAuthenticationProvider.superDigest=super:xQJmxLMiHGwaqBvst5y6rkB6HQs=" \
    -cp "$CLASSPATH" $JVMFLAGS $ZOOMAIN "$ZOOCFG" > "$_ZOO_DAEMON_OUT" 2>&1 < /dev/null &
复制代码
  • 重启 zookeeper
  • 重启完成之后此时超管即配置完成,如果需要使用,则使用如下命令:
 addauth digest super:admin
复制代码

Curator 客户端

  • Curator 是 Netflix 公司开源的一个 Zookeeper 客户端,与 Zookeeper 提供的原生客户端相比,Curator 的抽象层次更高,简化了 Zookeeper 客户端的开发量。

添加依赖

 <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>4.0.0</version>
            <exclusions>
                <exclusion>
                    <groupId>org.apache.zookeeper</groupId>
                    <artifactId>zookeeper</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
            <version>3.4.10</version>
        </dependency>
复制代码

建立连接

  • 客户端建立与 Zookeeper 的连接,这里仅仅演示单机版本的连接,如下:
//创建CuratorFramework,用来操作api
CuratorFramework  client = CuratorFrameworkFactory.builder()
    //ip地址+端口号,如果是集群,逗号分隔
    .connectString("120.26.101.207:2181")
    //会话超时时间
    .sessionTimeoutMs(5000)
    //超时重试策略,RetryOneTime:超时重连仅仅一次
    .retryPolicy(new RetryOneTime(3000))
    //命名空间,父节点,如果不指定是在根节点下
    .namespace("node4")
    .build();
//启动
client.start();
复制代码

重连策略

  • 会话连接策略,即是当客户端与 Zookeeper 断开连接之后,客户端重新连接 Zookeeper 时使用的策略,比如重新连接一次。
  • RetryOneTime:N 秒后重连一次,仅仅一次,演示如下:
.retryPolicy(new RetryOneTime(3000))
复制代码
  • RetryNTimes:每 n 秒重连一次,重连 m 次。演示如下:
//每三秒重连一次,重连3次。arg1:多长时间后重连,单位毫秒,arg2:总共重连几次
.retryPolicy(new RetryNTimes(3000,3))
复制代码
  • RetryUntilElapsed:设置了最大等待时间,如果超过这个最大等待时间将会不再连接。
//每三秒重连一次,等待时间超过10秒不再重连。arg1:总等待时间,arg2:多长时间重连,单位毫秒
.retryPolicy(new RetryUntilElapsed(10000,3000))
复制代码

新增节点

  • 新增节点
client.create()
    //指定节点的类型。PERSISTENT:持久化节点,PERSISTENT_SEQUENTIAL:持久化有序节点,EPHEMERAL:临时节点,EPHEMERAL_SEQUENTIAL临时有序节点
    .withMode(CreateMode.PERSISTENT)
    //指定权限列表,OPEN_ACL_UNSAFE:world:anyone:crdwa
    .withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)
    //写入节点数据,arg1:节点名称 arg2:节点数据
    .forPath("/a", "a".getBytes());
复制代码
  • 自定义权限列表:withACL(acls)方法中可以设置自定义的权限列表,代码如下:
//自定义权限列表
List<ACL> acls=new ArrayList<>();
//指定授权模式和授权对象 arg1:授权模式,arg2授权对象
Id id=new Id("ip","127.0.0.1");
//指定授予的权限,ZooDefs.Perms.ALL:crdwa
acls.add(new ACL(ZooDefs.Perms.ALL,id));
client.create()
    .withMode(CreateMode.PERSISTENT)
    //指定自定义权限列表
    .withACL(acls)
    .forPath("/b", "b".getBytes());
复制代码
  • 递归创建节点:creatingParentsIfNeeded()方法对于创建多层节点,如果其中一个节点不存在的话会自动创建
//递归创建节点
client.create()
    //递归方法,如果节点不存在,那么创建该节点
    .creatingParentsIfNeeded()
    .withMode(CreateMode.PERSISTENT)
    .withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)
    //test节点和b节点不存在,递归创建出来
    .forPath("/test/a", "a".getBytes());
复制代码
  • 异步创建节点:inBackground()方法可以异步回调创建节点,创建完成后会自动回调实现的方法
 //异步创建节点
client.create()
    .withMode(CreateMode.PERSISTENT)
    .withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)
    //异步创建
    .inBackground(new BackgroundCallback() {
        /**
        * @param curatorFramework 客户端对象
        * @param curatorEvent 事件对象
        */
        @Override
        public void processResult(CuratorFramework curatorFramework, CuratorEvent curatorEvent) throws Exception {
            //打印事件类型
            System.out.println(curatorEvent.getType());
            }
    })
    .forPath("/test1", "a".getBytes());
复制代码

更新节点数据

  • 更新节点,当节点不存在会报错,代码如下:
client.setData()
      .forPath("/a","a".getBytes());
复制代码
  • 携带版本号更新节点,当版本错误拒绝更新
client.setData()
    //指定版本号更新,如果版本号错误则拒绝更新
    .withVersion(1)
    .forPath("/a","a".getBytes());
复制代码
  • 异步更新节点数据:
client.setData()
        //异步更新
        .inBackground(new BackgroundCallback() {
        //回调方法
        @Override
        public void processResult(CuratorFramework curatorFramework, CuratorEvent curatorEvent) throws Exception {
                   }
        })
  .forPath("/a","a".getBytes());
复制代码

删除节点

  • 删除当前节点,如果有子节点则拒绝删除
client.delete()
    //删除节点,如果是该节点包含子节点,那么不能删除
    .forPath("/a");
复制代码
  • 指定版本号删除,如果版本错误则拒绝删除
client.delete()
    //指定版本号删除
    .withVersion(1)
    //删除节点,如果是该节点包含子节点,那么不能删除
    .forPath("/a");
复制代码
  • 如果当前节点包含子节点则一并删除,使用deletingChildrenIfNeeded()方法
client.delete()
    //如果删除的节点包含子节点则一起删除
    .deletingChildrenIfNeeded()
    //删除节点,如果是该节点包含子节点,那么不能删除
    .forPath("/a");
复制代码
  • 异步删除节点,使用inBackground()
client.delete()
 .deletingChildrenIfNeeded()
 //异步删除节点
    .inBackground(new BackgroundCallback() {
        @Override
        public void processResult(CuratorFramework curatorFramework, CuratorEvent curatorEvent) throws Exception {
        //回调监听
        }
       })
    //删除节点,如果是该节点包含子节点,那么不能删除
    .forPath("/a");
复制代码

获取节点数据

  • 同步获取节点数据
byte[] bytes = client.getData().forPath("/node1");
System.out.println(new String(bytes));
复制代码
  • 获取节点状态和数据
//保存节点状态
Stat stat=new Stat();
byte[] bytes = client.getData()
 //获取节点状态存储在stat对象中
    .storingStatIn(stat)
    .forPath("/node1");
System.out.println(new String(bytes));
//获取节点数据的长度
System.out.println(stat.getDataLength());
复制代码
  • 异步获取节点数据
client.getData()
    //异步获取节点数据,回调监听
     .inBackground((curatorFramework, curatorEvent) -> {
          //节点数据
          System.out.println(new String(curatorEvent.getData()));
      })
     .forPath("/node1");
复制代码

获取子节点

  • 同步获取全部子节点
 List<String> strs = client.getChildren().forPath("/");
        for (String str:strs) {
            System.out.println(str);
        }
复制代码
  • 异步获取全部子节点
client.getChildren()
//异步获取
.inBackground((curatorFramework, curatorEvent) -> {
        List<String> strs = curatorEvent.getChildren();
        for (String str:strs) {
              System.out.println(str);
        }
  })
.forPath("/");
复制代码

查看节点是否存在

  • 同步查看
//如果节点不存在,stat为null
Stat stat = client.checkExists().forPath("/node");
复制代码
  • 异步查看
//如果节点不存在,stat为null
client.checkExists()
    .inBackground((curatorFramework, curatorEvent) -> {
    //如果为null则不存在
    System.out.println(curatorEvent.getStat());
    })
    .forPath("/node");
复制代码

Watcher API

  • curator 提供了两种 watcher 来监听节点的变化

    • NodeCache:监听一个特定的节点,监听新增和修改
    • PathChildrenCache:监听一个节点的子节点,当一个子节点增加、删除、更新时,path Cache 会改变他的状态,会包含最新的子节点的数据和状态。
  • NodeCache 演示:
//arg1:连接对象 arg2:监听的节点路径,/namespace/path
final NodeCache nodeCache = new NodeCache(client, "/w1");
//启动监听
nodeCache.start();
//添加监听器
nodeCache.getListenable().addListener(() -> {
    //节点路径
    System.out.println(nodeCache.getCurrentData().getPath());
    //节点数据
    System.out.println(new String(nodeCache.getCurrentData().getData()));
});
//睡眠100秒
Thread.sleep(1000000);
//关闭监听
nodeCache.close();
复制代码
  • PathChildrenCache演示:
//arg1:连接对象 arg2:节点路径  arg3:是否能够获取节点数据
PathChildrenCache cache=new PathChildrenCache(client,"/w1", true);
cache.start();
cache.getListenable().addListener((curatorFramework, pathChildrenCacheEvent) -> {
 //节点路径
 System.out.println(pathChildrenCacheEvent.getData().getPath());
 //节点状态
 System.out.println(pathChildrenCacheEvent.getData().getStat());
 //节点数据
 System.out.println(new String(pathChildrenCacheEvent.getData().getData()));
});
cache.close();
复制代码

小福利

  • 是不是觉得文章太长看得头晕脑胀,为此陈某特地将本篇文章制作成 PDF 文本,需要回去仔细研究的朋友,老规矩,关注微信公众号【码猿技术专栏】回复关键词ZK入门指南

查看原文

赞 0 收藏 0 评论 0

爱撒谎的男孩 分享了头条 · 4月6日

文章详细讲述了Mysql中的三类锁,对于Mysql深入的研究的朋友有很大的帮助!!!

赞 0 收藏 0 评论 0

爱撒谎的男孩 发布了文章 · 4月6日

Spring中的设计模式:工厂方法模式

导读

  • 文章首发于微信公众号:Spring中的设计模式:工厂方法模式
  • 工厂方法模式是所有设计模式中比较常用的一种模式,但是真正能搞懂用好的少之又少,Spring底层大量的使用该设计模式来进行封装,以致开发者阅读源代码的时候晕头转向。
  • 今天陈某分别从以下五个方面详细讲述一下工厂方法模式:

    1. 「从什么是工厂方法模式」
    2. 「通用框架实现」
    3. 「工厂方法模式的优点」
    4. 「工厂方法模式的升级」
    5. 「Spring底层如何使用工厂方法模式」

什么是工厂方法模式?

  • 定义:定义一个用于创建对象的 接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。
  • 工厂方法模式通用类图如下:
  • 在工厂方法模式中,抽象产品Product负责定义产品的特性,实现对事物的抽象定义。
  • AbstractFactory是抽象工厂类,定义了一个抽象工厂方法。具体的如何创建产品由工厂实现类ConcreteFactory完成。

通用框架实现

  • 工厂方法模式的变种有很多,陈某给出一个比较实用的通用框架。
  • 抽象产品类
public abstract class Product {  
    /**  
     * 公共逻辑方法  
     */  
    public void method1(){}  
  
    /**  
     * 抽象方法:由子类实现,根据业务逻辑定义多个  
     */  
    public abstract void method2();  
} 
  • 具体产品类1,继承抽象产品类,如下:
public class Product1 extends Product {  
    /**  
     * 实现抽象产品类的抽象方法  
     */  
    @Override  
    public void method2() {  
  
    }  
}  
  • 具体产品类2,继承抽象产品类,如下:
public class Product2 extends Product {  
  
    /**  
     * 实现抽象产品类的抽象方法  
     */  
    @Override  
    public void method2() {  
  
    }  
}  
  • 抽象工厂类,必须定义一个工厂方法来自己实现具体的创建逻辑,如下:
public abstract class AbstractFactory {  
    /**  
     * 工厂方法,需要子类实现  
     * @param cls  
     * @param <T>  
     * @return  
     */  
    public abstract <T extends Product> T create(Class<T> cls);  
}  
  • 具体工厂类,使用了反射对具体产品的实例化,如下:
public class ConcreteFactory extends AbstractFactory {  
    @Override  
    public <T extends Product> T create(Class<T> cls) {  
        Product product=null;  
        try{  
            product= (Product) Class.forName(cls.getName()).newInstance();  
        }catch (Exception ex){  
            ex.printStackTrace();  
        }  
        return (T) product;  
    }  
}  
  • 测试如下:
public static void main(String[] args) {  
        //创建具体工厂类  
        ConcreteFactory factory = new ConcreteFactory();  
        //调用工厂方法获取产品类1的实例  
        Product1 product1 = factory.create(Product1.class);  
        System.out.println(product1);  
    }  
  • 以上是简单的一个通用框架,读者可以根据自己的业务在其上拓展。

工厂方法模式的优点

  • 良好的封装性,代码结构清晰,调用者不用关系具体的实现过程,只需要提供对应的产品类名称即可。
  • 易扩展性,在增加产品类的情况下,只需要适当的修改工厂类逻辑或者重新拓展一个工厂类即可。
  • 屏蔽了产品类,产品类的变化调用者不用关心。比如在使用JDBC连接数据库时,只需要改动一个驱动的名称,数据库就会从Mysql切换到Oracle,极其灵活。

工厂方法模式的升级

  • 在复杂的系统中,一个产品的初始化过程是及其复杂的,仅仅一个具体工厂实现可能有些吃力,此时最好的做法就是为每个产品实现一个工厂,达到一个工厂类只负责生产一个产品。
  • 此时工厂方法模式的类图如下:

  • 如上图,每个产品类都对应了一个工厂,一个工厂只负责生产一个产品,非常符合单一职责原则。
  • 针对上述的升级过程,那么工厂方法中不需要传入抽象产品类了,因为一个工厂只负责一个产品的生产,此时的抽象工厂类如下:
public abstract class AbstractFactory {  
    /**  
     * 工厂方法,需要子类实现  
     */  
    public abstract <T extends Product> T create();  
}  

Spring底层如何使用工厂方法模式?

  • 工厂方法模式在Spring底层被广泛的使用,陈某今天举个最常用的例子就是AbstractFactoryBean
  • 这个抽象工厂很熟悉了,这里不再讨论具体的作用。其实现了FactoryBean接口,这个接口中getObject()方法返回真正的Bean实例。
  • AbstractFactoryBean中的getObject()方法如下:
public final T getObject() throws Exception {  
    //单例,从缓存中取,或者暴露一个早期实例解决循环引用  
  if (isSingleton()) {  
   return (this.initialized ? this.singletonInstance : getEarlySingletonInstance());  
  }  
    //多实例  
  else {   
      //调用createInstance  
   return createInstance();  
  }  
 }  
  //创建对象  
  protected abstract T createInstance() throws Exception;
  • 从以上代码可以看出,创建对象的职责交给了createInstance这个抽象方法,由其子类去定制自己的创建逻辑。
  • 下图显示了继承了AbstractFactoryBean的具体工厂类,如下:

  • 其实与其说AbstractFactoryBean是抽象工厂类,不如说FactoryBean是真正的抽象工厂类,前者只是对后者的一种增强,完成大部分的可复用的逻辑。比如常用的sqlSessionFactoryBean只是简单的实现了FactoryBean,并未继承AbstractFactoryBean,至于结论如何,具体看你从哪方面看了。

总结

  • 工厂方法模式是一种常见的设计模式,但是真正能够用的高级,用的透彻还是有些难度的,开发者所能做的就是在此模式基础上思考如何优化自己的代码,达到易扩展、封装性强的效果了。
查看原文

赞 0 收藏 0 评论 0

爱撒谎的男孩 发布了文章 · 4月5日

Spring中的设计模式:模板模式

导读

  • 模板模式在是Spring底层被广泛的应用,比如事务管理器的实现,JDBC模板的实现。
  • 文章首发于微信公众号:设计模式:模板模式
  • 今天就来谈谈「什么是模板模式」「模板模式的优缺点」「模板模式的简单演示」「模板模式在Spring底层的实现」

什么是模板模式

  • 模板模式首先要有一个抽象类,这个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。这种类型的设计模式属于行为型模式。
  • 定义:「定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。」
  • 比如在造房子一样,地基铺线房子户型都是一样的,由开发商决定,但是在交房之后,室内的装修风格场景布置却是由业主决定,在这个场景中,开发商其实就是一个抽象类,地基,铺线,房子户型都是可以复用的,但是装修却是不可复用的,必须由业主决定,此时的每一个业主的房子就是一个实现的子类。
  • 模板方法的实现条件注意:

    1. 必须是一个抽象类。
    2. 抽象类有一个模板方法,其中定义了算法骨架。
    3. 为了防止恶意操作,模板方法必须加上final关键词。
    4. 模板方法中除了复用的代码,其他的关键代码必须是抽象的,子类可以继承实现。

优点

  • 它封装了不变部分,扩展可变部分。它把认为是不变部分的算法封装到父类中实现,而把可变部分算法由子类继承实现,便于子类继续扩展。
  • 它在父类中提取了公共的部分代码,便于代码复用。
  • 部分方法是由子类实现的,因此子类可以通过扩展方式增加相应的功能,符合开闭原则。

缺点

  • 对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象。
  • 父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度。

简单演示

  • 比如游戏的运行需要如下几个步骤:

    1. 初始化游戏
    2. 开始游戏
    3. 结束游戏
  • 上述的三个步骤可以是模板类的抽象方法,由具体的子类实现,比如足球游戏。
  • 定义模板类,必须是一个抽象类,模板方法必须是final修饰。
public abstract class Game {

 //抽象方法

 abstract void initialize();

 abstract void startPlay();

 abstract void endPlay();

 //模板方法

 public final void play(){

 //初始化游戏

 initialize();

 //开始游戏

 startPlay();

 //结束游戏

 endPlay();

 }

}
  • 定义实现类,足球游戏,继承模板类,实现其中的三个抽象方法
public class Football extends Game {
 
   @Override
   void endPlay() {
      System.out.println("足球游戏结束......");
   }
 
   @Override
   void initialize() {
      System.out.println("足球游戏初始化中......");
   }
 
   @Override
   void startPlay() {
      System.out.println("足球游侠开始了......");
   }
}
  • 此时写一个测试方法,运行足球游戏,如下:
public class TemplatePatternDemo {
   public static void main(String[] args) {
      //创建足球游戏实例
      Game game = new Football();
      //开始游戏
      game.play();      
   }
}
  • 输出结果如下:
足球游戏初始化中......
足球游侠开始了......
足球游戏结束......

Spring中的模板模式

  • Spring底层对于模板模式的使用有很多处,今天陈某带大家康康事务管理器是如何使用模板模式的。

模板抽象类

  • AbstractPlatformTransactionManager是Spring中的模板抽象类,来看看它的继承关系图:
  • 实现了PlatformTransactionManager接口,重载了接口中的方法。

模板方法

  • 事务管理器中抽象类中的模板方法不止一个,比如以下两个方法
//提交事务
public final void commit()

//获取TransactionStatus
public final TransactionStatus getTransaction()
  • 这两个方法都对于自己要实现的逻辑搭建了一个骨架,主要的功能是由抽象方法完成,由子类来完成。

抽象方法

  • 事务管理器抽象类中的抽象方法定义了多个,分别用于处理不同的业务逻辑,由子类实现其中具体的逻辑,如下:
//提交事务
protected abstract void doCommit(DefaultTransactionStatus status);

//回滚事务
protected abstract void doRollback(DefaultTransactionStatus status);

//开始事务
protected abstract void doBegin(Object transaction, TransactionDefinition definition)

//获取当前的事务对象
protected abstract Object doGetTransaction()
  • 抽象方法的定义便于子类去扩展,在保证算法逻辑不变的情况下,子类能够定制自己的实现。

具体子类

  • 事务管理器的模板类有很多的具体子类,如下图:
  • 其中我们熟悉的有DataSourceTransactionManagerJtaTransactionManagerRabbitTransactionManager。具体承担什么样的角色和责任不是本节的重点,不再细说。

总结

  • 模板模式是一个很重要,易扩展的模式,提高了代码复用性,在Spring中有着广泛的应用,比如JDBCTemplate,AbstractPlatformTransactionManager,这些实现都用到了模板模式。
  • 如果觉得陈某的文章能够对你有所帮助,有所启发,关注分享一波,点个在看,谢谢支持!!!
查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 74 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-03-07
个人主页被 947 人浏览