码猿技术专栏

微信公众号:码猿技术专栏

前言

  • 在实际的开发中一定会碰到根据某个字段进行排序后来显示结果的需求,但是你真的理解order by在 Mysql 底层是如何执行的吗?
  • 假设你要查询城市是苏州的所有人名字,并且按照姓名进行排序返回前 1000 个人的姓名、年龄,这条 sql 语句应该如何写?
  • 首先创建一张用户表,sql 语句如下:
1
2
3
4
5
6
7
8
CREATE TABLE user (
id int(11) NOT NULL,
city varchar(16) NOT NULL,
name varchar(16) NOT NULL,
age int(11) NOT NULL,
PRIMARY KEY (id),
KEY city (city)
) ENGINE=InnoDB;
  • 则上述需求的 sql 查询语句如下:
1
select city,name,age from user where city='苏州' order by name limit 1000;
  • 这条 sql 查询语句相信大家都能写出来,但是你了解它在 Mysql 底层的执行流程吗?今天陈某来大家聊一聊这条 sql 语句是如何执行的以及有什么参数会影响执行的流程。
  • 本篇文章分为如下几个部分进行详细的阐述:
    1. 全字段排序
    2. rowid 排序
    3. 全字段排序 VS rowid 排序
    4. 如何避免排序

全字段排序

  • 前面聊过索引能够避免全表扫描,因此我们给city这个字段上添加了索引,当然城市的字段很小,不用考虑字符串的索引问题,之前有写过一篇关于如何给字符串的加索引的文章,有不了解朋友看一下这篇文章:Mysql 性能优化:如何给字符串加索引?
  • 此时用Explain来分析一下的这条查询语句的执行情况,结果如下图:
    Explain分析结果
  • Extra这个字段中的Using filesort表示的就是需要排序,MySQL 会给每个线程分配一块内存用于排序,称为sort_buffer
  • 既然使用了索引进行查询,我们来简单的画一下city这棵索引树的结构,如下图:
    city索引树
  • 从上图可以看出,满足city='苏州'是从ID3IDX这些记录。
  • 通常情况下,此条 sql 语句执行流程如下:
    1. 初始化 sort_buffer,确定放入 name、city、age 这三个字段。
    2. 从索引 city 找到第一个满足city='苏州'条件的主键id,也就是图中的ID3
    3. 主键id索引取出整行,取namecityage三个字段的值,存入sort_buffer中。
    4. 从索引city取下一个记录的主键 id。
    5. 重复步骤 3、4 直到 city 的值不满足查询条件为止,对应的主键 id 也就是图中的IDX
    6. sort_buffer中的数据按照字段name做快速排序。
    7. 按照排序结果取前 1000 行返回给客户端。
  • 我们称这个排序过程为全字段排序,执行的流程图如下:
    全字段排序
  • 图中按name排序这个动作,可能在内存中完成,也可能需要使用外部排序,这取决于排序所需的内存和参数sort_buffer_size
  • sort_buffer_size:就是 MySQL 为排序开辟的内存(sort_buffer)的大小。如果要排序的数据量小于 sort_buffer_size,排序就在内存中完成。但如果排序数据量太大,内存放不下,则不得不利用磁盘临时文件辅助排序。

rowid 排序

  • 在上面这个算法过程里面,只对原表的数据读了一遍,剩下的操作都是在sort_buffer临时文件中执行的。但这个算法有一个问题,就是如果查询要返回的字段很多的话,那么sort_buffer里面要放的字段数太多,这样内存里能够同时放下的行数很少,要分成很多个临时文件,排序的性能会很差
  • 所以如果单行很大,这个方法效率不够好。
  • 我们可以修改一个max_length_for_sort_data这个参数使其使用另外一种算法。max_length_for_sort_data,是 MySQL 中专门控制用于排序的行数据的长度的一个参数。它的意思是,如果单行的长度超过这个值,MySQL 就认为单行太大,要换一个算法。
  • citynameage 这三个字段的定义总长度是36,我把max_length_for_sort_data设置为 16,我们再来看看计算过程有什么改变。设置的 sql 语句如下:
1
SET max_length_for_sort_data = 16;
  • 新的算法放入 sort_buffer 的字段,只有要排序的列(即 name 字段)和主键 id。

  • 但这时,排序的结果就因为少了 city 和 age 字段的值,不能直接返回了,整个执行流程就变成如下所示的样子:

    1. 初始化sort_buffer,确定放入两个字段,即nameid
    2. 从索引 city 找到第一个满足city='苏州'条件的主键id,也就是图中的ID3
    3. 主键id索引取出整行,取 name、id 这两个字段,存入 sort_buffer 中。
    4. 从索引city取下一个记录的主键 id。
    5. 重复步骤 3、4 直到 city 的值不满足查询条件为止,对应的主键 id 也就是图中的IDX
    6. sort_buffer中的数据按照字段name做快速排序。
    7. 遍历排序结果,取前 1000 行,并按照 id 的值回到原表中取出 city、name 和 age 三个字段返回给客户端。
  • 这个执行流程的示意图如下,我把它称为rowid排序
    rowid排序

  • 对比全字段排序rowid排序多了一次回表查询,即是多了第7步的查询主键索引树。

全字段排序 VS rowid 排序

  • 如果 MySQL 实在是担心排序内存太小,会影响排序效率,才会采用 rowid 排序算法,这样排序过程中一次可以排序更多行,但是需要再回到原表去取数据。
  • 如果 MySQL 认为内存足够大,会优先选择全字段排序,把需要的字段都放到 sort_buffer 中,这样排序后就会直接从内存里面返回查询结果了,不用再回到原表去取数据。
  • 这也就体现了 MySQL 的一个设计思想:如果内存够,就要多利用内存,尽量减少磁盘访问
  • 对于 InnoDB 表来说,rowid 排序会要求回表多造成磁盘读,因此不会被优先选择。

如何避免排序

  • 其实,并不是所有的order by语句,都需要排序操作的。从上面分析的执行过程,我们可以看到,MySQL 之所以需要生成临时表,并且在临时表上做排序操作,其原因是原来的数据都是无序的
  • 如果能够保证从city这个索引上取出来的行,天然就是按照 name 递增排序的话,是不是就可以不用再排序了呢?
  • 因此想到了联合索引,创建(city,name)联合索引,sql 语句如下:
1
alter table user add index city_user(city, name);
  • 此时的索引树如下:
    city,name索引树
  • 在这个索引里面,我们依然可以用树搜索的方式定位到第一个满足city='苏州'的记录,并且额外确保了,接下来按顺序取“下一条记录”的遍历过程中,只要 city 的值是苏州,name 的值就一定是有序的。
  • 按照上图,整个查询的流程如下:
    1. 从索引(city,name)找到第一个满足 city=’苏州’条件的主键 id。
    2. 到主键 id 索引取出整行,取 name、city、age 三个字段的值,作为结果集的一部分直接返回。
    3. 从索引(city,name)取下一个记录主键 id。
    4. 重复步骤 2、3,直到查到第 1000 条记录,或者是不满足 city=’苏州’条件时循环结束。
  • 对应的流程图如下:
    city,name联合索引的执行流程
  • 可以看到,这个查询过程不需要临时表,也不需要排序。接下来,我们用 explain 的结果来印证一下。
  • 从图中可以看到,Extra字段中没有Using filesort了,也就是不需要排序了。而且由于(city,name)这个联合索引本身有序,所以这个查询也不用把 4000 行全都读一遍,只要找到满足条件的前 1000 条记录就可以退出了。也就是说,在我们这个例子里,只需要扫描 1000 次。
  • 难道仅仅这样就能满足了?此条查询语句是否能再优化呢?
  • 朋友们还记得覆盖索引吗?覆盖索引的好处就是能够避免再次回表查询,不了解的朋友们可以看一下陈某之前写的文章:Mysql 性能优化:如何使用覆盖索引?
  • 我们创建(city,name,age)联合索引,这样在执行上面的查询语句就能使用覆盖索引了,避免了回表查询了,sql 语句如下:
1
alter table user add index city_user_age(city, name, age);
  • 此时执行流程图如下:
    覆盖索引使用执行流程
  • 当然,覆盖索引能够提升效率,但是维护索引也是需要代价的,因此还需要权衡使用。

总结

  • 今天这篇文章,我和你介绍了 MySQL 里面order by语句的几种算法流程。
  • 在开发系统的时候,你总是不可避免地会使用到 order by 语句。心里要清楚每个语句的排序逻辑是怎么实现的,还要能够分析出在最坏情况下,每个语句的执行对系统资源的消耗,这样才能做到下笔如有神,不犯低级错误。

前言

  • 声明式事务是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方法中,如下:
    1
    2
    3
    4
    5
    6
    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,导致异常无法抛出,自然会导致事务失效。伪代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Transactional
    public void method(){
    try{
    //插入一条数据
    //更改一条数据
    }catch(Exception ex){
    return;
    }
    }

方法中调用同类的方法

  • 简单的说就是一个类中的A方法(未标注声明式事务)在内部调用了B方法(标注了声明式事务),这样会导致B方法中的事务失效。
  • 代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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配置文件或者其他配置文件中即可。

总结

  • 事务失效的原因很多,但是千万不要做到一知半解,只有深入理解了,才能在面试过程中对答如流。
  • 今天的文章就到此结束了,如果觉得陈某写得不错,有所收获的,关注在看来一波,你们的鼓励,将会是我写作的动力,谢谢支持!!!

导读

  • 工厂方法模式是所有设计模式中比较常用的一种模式,但是真正能搞懂用好的少之又少,Spring底层大量的使用该设计模式来进行封装,以致开发者阅读源代码的时候晕头转向。
  • 今天陈某分别从以下五个方面详细讲述一下工厂方法模式:
    1. 从什么是工厂方法模式
    2. 通用框架实现
    3. 工厂方法模式的优点
    4. 工厂方法模式的升级
    5. Spring底层如何使用工厂方法模式

什么是工厂方法模式?

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

通用框架实现

  • 工厂方法模式的变种有很多,陈某给出一个比较实用的通用框架。

  • 抽象产品类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public abstract class Product {
    /**
    * 公共逻辑方法
    */
    public void method1(){}

    /**
    * 抽象方法:由子类实现,根据业务逻辑定义多个
    */
    public abstract void method2();
    }
  • 具体产品类1,继承抽象产品类,如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class Product1 extends Product {
    /**
    * 实现抽象产品类的抽象方法
    */
    @Override
    public void method2() {

    }
    }
  • 具体产品类2,继承抽象产品类,如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class Product2 extends Product {

    /**
    * 实现抽象产品类的抽象方法
    */
    @Override
    public void method2() {

    }
    }
  • 抽象工厂类,必须定义一个工厂方法来自己实现具体的创建逻辑,如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public abstract class AbstractFactory {
    /**
    * 工厂方法,需要子类实现
    * @param cls
    * @param <T>
    * @return
    */
    public abstract <T extends Product> T create(Class<T> cls);
    }
  • 具体工厂类,使用了反射对具体产品的实例化,如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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;
    }
    }
  • 测试如下:

    1
    2
    3
    4
    5
    6
    7
    public static void main(String[] args) {
    //创建具体工厂类
    ConcreteFactory factory = new ConcreteFactory();
    //调用工厂方法获取产品类1的实例
    Product1 product1 = factory.create(Product1.class);
    System.out.println(product1);
    }
  • 以上是简单的一个通用框架,读者可以根据自己的业务在其上拓展。

工厂方法模式的优点

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

工厂方法模式的升级

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

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

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

  • 工厂方法模式在Spring底层被广泛的使用,陈某今天举个最常用的例子就是AbstractFactoryBean
  • 这个抽象工厂很熟悉了,这里不再讨论具体的作用。其实现了FactoryBean接口,这个接口中getObject()方法返回真正的Bean实例。
  • AbstractFactoryBean中的getObject()方法如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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,至于结论如何,具体看你从哪方面看了。

总结

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

导读

  • 模板模式在是Spring底层被广泛的应用,比如事务管理器的实现,JDBC模板的实现。
  • 今天就来谈谈什么是模板模式模板模式的优缺点模板模式的简单演示模板模式在Spring底层的实现

什么是模板模式

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

优点

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

缺点

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

简单演示

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

    1. 初始化游戏
    2. 开始游戏
    3. 结束游戏
  • 上述的三个步骤可以是模板类的抽象方法,由具体的子类实现,比如足球游戏。

  • 定义模板类,必须是一个抽象类,模板方法必须是final修饰。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public abstract class Game {
    //抽象方法
    abstract void initialize();
    abstract void startPlay();
    abstract void endPlay();

    //模板方法
    public final void play(){

    //初始化游戏
    initialize();

    //开始游戏
    startPlay();

    //结束游戏
    endPlay();
    }
    }
  • 定义实现类,足球游戏,继承模板类,实现其中的三个抽象方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class Football extends Game {

    @Override
    void endPlay() {
    System.out.println("足球游戏结束......");
    }

    @Override
    void initialize() {
    System.out.println("足球游戏初始化中......");
    }

    @Override
    void startPlay() {
    System.out.println("足球游侠开始了......");
    }
    }
  • 此时写一个测试方法,运行足球游戏,如下:

    1
    2
    3
    4
    5
    6
    7
    8
    public class TemplatePatternDemo {
    public static void main(String[] args) {
    //创建足球游戏实例
    Game game = new Football();
    //开始游戏
    game.play();
    }
    }
  • 输出结果如下:

    1
    2
    3
    足球游戏初始化中......
    足球游侠开始了......
    足球游戏结束......

Spring中的模板模式

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

模板抽象类

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

模板方法

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

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

抽象方法

  • 事务管理器抽象类中的抽象方法定义了多个,分别用于处理不同的业务逻辑,由子类实现其中具体的逻辑,如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //提交事务
    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,这些实现都用到了模板模式。
  • 如果觉得陈某的文章能够对你有所帮助,有所启发,关注分享一波,点个在看,谢谢支持!!!

导读

  • 在开发中一定会用到统计一张表的行数,比如一个交易系统,老板会让你每天生成一个报表,这些统计信息少不了sql中的count函数。
  • 但是随着记录越来越多,查询的速度会越来越慢,为什么会这样呢?Mysql内部到底是怎么处理的?
  • 今天这篇文章将从Mysql内部对于count函数是怎样处理的?

count的实现方式

  • 在Mysql中的不同的存储引擎对count函数有不同的实现方式。
  • MyISAM引擎把一个表的总行数存在了磁盘上,因此执行count(*)的时候会直接返回这个数,效率很高(没有where查询条件)。
  • InnoDB引擎并没有直接将总数存在磁盘上,在执行count(*)函数的时候需要一行一行的将数据读出来,然后累计总数。

为什么InnoDB不将总数存起来?

  • 说道InnoDB相信读者总会想到其支持事务的特性,事务具有隔离性,如果将总数存起来,怎么保证各个事务之间的总数的一致性呢?不明白的看下图:

  • 事务A事务B中的count(*)的执行结果是不同的,因此InnoDB引擎在每个事务中返回多少行是不确定的,只能一行一行的读出来用来判断总数。

如何提升count效率

  • InnoDB对于如何提升count(*)的查询效率,网上有多种解决办法,这里主要介绍三种,并分析可行性。

show table status

  • show table status这个命令能够很快的查询出数据库中每个表的行数,但是真的能够替代count(*)吗?
  • 答案是不能。原因很简单,这个命令统计出来的值是一个估值,因此是不准确的,官方文档说误差大概在40%-50%
  • 因此这种方法直接pass,不准确还用它干嘛。

缓存系统存储总数

  • 这种方法也是最容易想到的,增加一行就+1,删除一行就-1,并且缓存系统读取也是很快,既简单又方便的为什么不用?

  • 缓存系统和Mysql是两个系统,比如redisMysql这两个是典型的比较。两个系统最难的就是在高并发下无法保证数据的一致性。通过以下两图我们来理解一下:

  • 通过上面两张图,无论是redis计数+1还是insert into user先执行,最终都会导致数据在逻辑上的不一致。第一张图会出现redis计数少了,第二张图虽然计数正确了但是并没有查询出插入的那一行数据。

  • 在并发系统里面,我们是无法精确控制不同线程的执行时刻的,因为存在图中的这种操作序列,所以,我们说即使Redis正常工作,这个计数值还是逻辑上不精确的。

在数据库保存计数

  • 通过缓存系统保存的分析得知了使用缓存无法保证数据在逻辑上的一致性,因此我们想到了直接使用数据库来保存,有了事务的支持,也就保证了数据的一致性了。

  • 如何使用呢?很简单,直接将计数保存在一张表中(table_name,total)

  • 至于执行的逻辑只需要将缓存系统中redis计数+1改成total字段+1即可,如下图:

  • 由于在同一个事务中,保证了数据在逻辑上的一致性。

不同count的用法

  • count()是一个聚合函数,对于返回的结果集,一行行地判断,如果count函数的参数不是NULL,累计值就加1,否则不加。最后返回累计值。
  • count的用法有多种,分别是count(*)count(字段)count(1)count(主键id)。那么多种用法,到底有什么差别呢?当然,前提是没有where条件语句
  • count(id):InnoDB引擎会遍历整张表,把每一行的id值都取出来,返回给server层。server层拿到id后,判断是不可能为空的,就按行累加。
  • count(1):InnoDB引擎遍历整张表,但不取值。server层对于返回的每一行,放一个数字1进去,判断是不可能为空的,按行累加。
  • count(字段)
    • 如果这个“字段”是定义为not null的话,一行行地从记录里面读出这个字段,判断不能为null,按行累加;
    • 如果这个字段定义允许为null,那么执行的时候,判断到有可能是null,还要把值取出来再判断一下,不是null才累加。
  • count(*):不会把全部字段取出来,而是专门做了优化,不取值。count(*)肯定不是null,按行累加。
  • 所以结论很简单:按照效率排序的话,count(字段)<count(主键id)<count(1)count(*),所以建议读者,尽量使用count(*)
  • 注意:这里肯定有人会问,count(id)不是走的索引吗,为什么查询效率和其他的差不多呢?陈某在这里解释一下,虽然走的索引,但是还是要一行一行的扫描才能统计出来总数。

总结

  • MyISAM表虽然count(*)很快,但是不支持事务;
  • show table status命令虽然返回很快,但是不准确;
  • InnoDB直接count(*)会遍历全表(没有where条件),虽然结果准确,但会导致性能问题。
  • 缓存系统的存储计数虽然简单效率高,但是无法保证数据的一致性。
  • 数据库保存计数很简单,也能保证数据的一致性,建议使用。
  • 思考题,读者留言区讨论:在系统高并发的情况下,使用数据库保存计数,是先更新计数+1,还是先插入数据。即是先update total+=1还是先insert into

导读

  • 现在这个时代大家可能最关心的就是钱了,那么有没有想过你银行转账给你没有一次是转多的,要么失败,要么成功,为什么不能失误一下多转一笔呢?醒醒吧年轻人,别做梦了,做银行的能那么傻x吗?
  • 今天我们就来谈一谈为什么银行转账不能多给我转一笔?关乎到钱的问题,小伙伴们打起精神!!!
  • 要想要理解上述的疑惑,不得不提的一个概念就是幂等性,至于什么是幂等性,如何通过代码实现幂等性,下面将会详细讲述。

什么是幂等性

  • 所谓幂等性通俗的将就是一次请求和多次请求同一个资源产生相同的副作用。用数学语言表达就是f(x)=f(f(x))
  • 维基百科的幂等性定义如下:
1
2
幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。
在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,“setTrue()”函数就是一个幂等函数,无论多次执行,其结果都是一样的,更复杂的操作幂等保证是利用唯一交易号(流水号)实现.

为什么需要幂等性

  • 在系统高并发的环境下,很有可能因为网络,阻塞等等问题导致客户端或者调用方并不能及时的收到服务端的反馈甚至是调用超时的问题。总之,就是请求方调用了你的服务,但是没有收到任何的信息,完全懵逼的状态。比如订单的问题,可能会遇到如下的几个问题:
  1. 创建订单时,第一次调用服务超时,再次调用是否产生两笔订单?
  2. 订单创建成功去减库存时,第一次减库存超时,是否会多扣一次?
  3. 订单支付时,服务端扣钱成功,但是接口反馈超时,此时再次调用支付,是否会多扣一笔呢?
  • 作为消费者,前两种能接受,第三种情况就MMP了,哈哈哈!!!这种情况一般有如下两种解决方式
  1. 服务方提供一个查询操作是否成功的api,第一次超时之后,调用方调用查询接口,如果查到了就走成功的流程,失败了就走失败的流程。
  2. 另一种就是服务方需要使用幂等的方式保证一次和多次的请求结果一致。

HTTP的幂等性

  • GET:只是获取资源,对资源本身没有任何副作用,天然的幂等性。
  • HEAD:本质上和GET一样,获取头信息,主要是探活的作用,具有幂等性。
  • OPTIONS:获取当前URL所支持的方法,因此也是具有幂等性的。
  • DELETE:用于删除资源,有副作用,但是它应该满足幂等性,比如根据id删除某一个资源,调用方可以调用N次而不用担心引起的错误(根据业务需求而变)。
  • PUT:用于更新资源,有副作用,但是它应该满足幂等性,比如根据id更新数据,调用多次和N次的作用是相同的(根据业务需求而变)。
  • POST:用于添加资源,多次提交很可能产生副作用,比如订单提交,多次提交很可能产生多笔订单。

幂等性的实现方式

  • 对于客户端交互的接口,可以在前端拦截一部分,例如防止表单重复提交,按钮置灰,隐藏,不可点击等方式。但是前端进行拦截器显然是针对普通用户,懂点技术的都可以模拟请求调用接口,所以后端幂等性很重要。
  • 后端的幂等性如何实现?将会从以下几个方面介绍。

数据库去重表

  • 在往数据库中插入数据的时候,利用数据库唯一索引特性,保证数据唯一。比如订单的流水号,也可以是多个字段的组合。
  • 实现比较简单,读者可以自己实现看看,这里不再提供demo了。

状态机

  • 很多业务中多有多个状态,比如订单的状态有提交、待支付、已支付、取消、退款等等状态。后端可以根据不同的状态去保证幂等性,比如在退款的时候,一定要保证这笔订单是已支付的状态。
  • 业务中常常出现,读者可以自己实现看看,不再提供demo。

TOKEN机制

  • 针对客户端连续点击或者调用方的超时重试等情况,例如提交订单,此种操作就可以用Token的机制实现防止重复提交。
  • TOKEN机制如何实现?简单的说就是调用方在调用接口的时候先向后端请求一个全局ID(TOKEN),请求的时候携带这个全局ID一起请求,后端需要对这个全局ID校验来保证幂等操作,流程如下图:

  • 主要的流程步骤如下:
    • 客户端先发送获取token的请求,服务端会生成一个全局唯一的ID保存在redis中,同时把这个ID返回给客户端。
    • 客户端调用业务请求的时候必须携带这个token,一般放在请求头上。
    • 服务端会校验这个Token,如果校验成功,则执行业务。
    • 如果校验失败,则表示重复操作,直接返回指定的结果给客户端。
  • 通过以上的流程分析,唯一的重点就是这个全局唯一ID如何生成,在分布式服务中往往都会有一个生成全局ID的服务来保证ID的唯一性,但是工程量和实现难度比较大,UUID的数据量相对有些大,此处陈某选择的是雪花算法生成全局唯一ID,不了解雪花算法的读者下一篇文章会着重介绍。

代码实现

  • 陈某选择的环境是SpringBoot+Redis单机环境+注解+拦截器的方式实现,只是演示一下思想,具体的代码可以参照实现。
  • redis如何实现,获取Token接口将全局唯一Id存入Redis(一定要设置失效时间,根据业务需求),业务请求的时候直接从redis中删除,根据delete的返回值判断,返回true表示第一次请求,返回false表示重复请求。代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Service
public class TokenServiceImpl implements TokenService {
@Autowired
private StringRedisTemplate stringRedisTemplate;

@Override
public String getToken() {
//获取全局唯一id
long nextId = SnowflakeUtil.nextId();
//存入redis,设置10分钟失效
stringRedisTemplate.opsForValue().set(String.valueOf(nextId), UUID.randomUUID().toString(),10, TimeUnit.MINUTES);
return String.valueOf(nextId);
}

/**
* 删除记录,true表示第一次提交,false重复提交
*/
@Override
public Boolean checkToken(String token) {
return stringRedisTemplate.delete(token);
}
}
  • 注解的实现如下,标注在controller类上表示当前类上全部接口都做幂等,标注单个方法上,表示单个接口做幂等操作。
1
2
3
4
5
6
7
8
9
10
/**
* @Description 幂等操作的注解
* @Author CJB
* @Date 2020/3/25 10:19
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatLimiter {
}
  • 请求头的拦截器,用于提取请求头和校验请求头,如下:
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
/**
* @Description 获取请求头的信息,具体校验逻辑读者自己实现
* @Author CJB
* @Date 2020/3/25 11:09
*/
@Component
public class HeaderIntercept implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//获取token
String token = request.getHeader(HeaderConstant.TOKEN);
//校验逻辑
if (!validToken(token))
throw new TokenInvalidException("TOKEN失效");
//获取其他的参数.....
RequestHeader header = RequestHeader.builder()
.token(token)
.build();
//放入request中
request.setAttribute(HeaderConstant.HEADER_INFO,header);
return true;
}

/**
* 校验token,逻辑自己实现
* @param token
* @return
*/
private boolean validToken(String token){
return Boolean.TRUE;
}
}
  • 保证幂等性的拦截器,直接从redis中删除token,成功则第一次提交,不成功则重复提交。
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
@Component
public class RepeatIntercept implements HandlerInterceptor {

@Autowired
private TokenService tokenService;

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod){
//获取方法上的参数
RepeatLimiter repeatLimiter = AnnotationUtils.findAnnotation(((HandlerMethod) handler).getMethod(), RepeatLimiter.class);

if (Objects.isNull(repeatLimiter)){
//获取controller类上注解
repeatLimiter=AnnotationUtils.findAnnotation(((HandlerMethod) handler).getBean().getClass(),RepeatLimiter.class);
}

//使用注解,需要拦截验证
if (Objects.nonNull(repeatLimiter)){
//获取全局token,表单提交的唯一id
RequestHeader info = RequestContextUtils.getHeaderInfo();

//没有携带token,抛异常,这里的异常需要全局捕获
if (StringUtils.isEmpty(info.getToken()))
throw new RepeatException();

//校验token
Boolean flag = tokenService.checkToken(info.getToken());

//删除失败,表示
if (Boolean.FALSE.equals(flag))
//抛出重复提交的异常
throw new RepeatException();
}
}
return true;
}
}
  • 接口幂等实现,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RestController
@RequestMapping("/order")
public class OrderController {

@Autowired
private OrderService orderService;

/**
* 下单
* @param order
* @return
*/
@PostMapping
@RepeatLimiter //幂等性保证
public CommenResult add(@RequestBody Order order){
orderService.save(order);
return new CommenResult("200","下单成功");
}
}

演示

  • 发送getToken的请求获取Token

  • 携带Token下单第一次:

  • 第二次下单:

导读

  • 索引下推(index condition pushdown )简称ICP,在Mysql5.6的版本上推出,用于优化查询。
  • 在不使用ICP的情况下,在使用非主键索引(又叫普通索引或者二级索引)进行查询时,存储引擎通过索引检索到数据,然后返回给MySQL服务器,服务器然后判断数据是否符合条件 。
  • 在使用ICP的情况下,如果存在某些被索引的列的判断条件时,MySQL服务器将这一部分判断条件传递给存储引擎,然后由存储引擎通过判断索引是否符合MySQL服务器传递的条件,只有当索引符合条件时才会将数据检索出来返回给MySQL服务器 。
  • 索引条件下推优化可以减少存储引擎查询基础表的次数,也可以减少MySQL服务器从存储引擎接收数据的次数。
    阅读全文 »

导读

  • 相信读者看过很多MYSQL索引优化的文章,其中有很多优化的方法,比如最佳左前缀,覆盖索引等方法,但是你真正理解为什么要使用最佳左前缀,为什么使用覆盖索引会提升查询的效率吗?

  • 本篇文章将从MYSQL内部结构上讲一下为什么覆盖索引能够提升效率。

阅读全文 »

导读

  • Mysql在中小型企业中是个香饽饽,目前主流的数据库之一,几乎没有一个后端开发者不会使用的,但是作为一个老司机,仅仅会用真的不够。

  • 今天陈某透过一个简单的查询语句来讲述在Mysql内部的执行过程。

    1
    select * from table where id=10;
    阅读全文 »