GVKun编程网logo

一起因MySQL时间戳精度引发的血案分析(时间戳 mysql)

8

对于想了解一起因MySQL时间戳精度引发的血案分析的读者,本文将是一篇不可错过的文章,我们将详细介绍时间戳mysql,并且为您提供关于07.空指针引发的血案、dwr引发的血案之四内存溢出、golang

对于想了解一起因MySQL时间戳精度引发的血案分析的读者,本文将是一篇不可错过的文章,我们将详细介绍时间戳 mysql,并且为您提供关于07.空指针引发的血案、dwr引发的血案之四 内存溢出、golang mysql 确诊之旅(2000万开房数据被曝光引发的血案)、golang mysql 诊断之旅(2000万开房数据被曝光引发的血案)的有价值信息。

本文目录一览:

一起因MySQL时间戳精度引发的血案分析(时间戳 mysql)

一起因MySQL时间戳精度引发的血案分析(时间戳 mysql)

这篇文章主要给大家介绍了一起因MysqL时间戳精度引发的血案的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用MysqL具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧

写在前面

最近工作中遇到两例MysqL时间戳相关的问题,一个是mysql-connector-java和msyql的精度不一致导致数据查不到;另一例是应用服务器时区错误导致数据查询不到。通过这篇文章,希望能够解答关于MysqL中时间戳的几个问题:

MysqL中的DATETIME精度为什么只支持到秒?

MysqL中的DATETIME类型跟时区有关吗?

MysqL设计表的时候,表示时间的字段改如何选择?

案例分析 DATETIME的精度问题

前段时间,将负责的应用的mysql-connector-java的版本从5.1.16升级到5.1.30,在做功能回归的时候发现,使用了类似上面的sql的用例的运行时数据会有遗漏,导致功能有问题。

考虑到我负责的应用中,有个功能需要用到类似下面这种sql,即使用时间戳作为查询的条件,查询在某个时间戳之后的所有数据。

经过排查发现:mysql-connector-java在5.1.23之前会将秒后面的精度丢弃再传给MysqL服务端,正好我们使用的MysqL版本中DATETIME的精度是秒;在我将mysql-connector-java升级到5.1.30后,从java应用通过mysql-connector-java将时间戳传到MysqL服务端的时候,就不会将毫秒数丢弃了,从mysql-connector-java的角度看是修复了一个BUG,但是对于我的应用来说却是触发了一个BUG。

如果你面对这个问题,你会怎么修复呢?

我们当时想了三种方案:

将mybatis的Mapper接口中的时间戳参数的类型,从java.util.Date改成java.sql.Date;

在传入Mapper接口之前,将传入的时间戳按秒取正,代码如下

在查询之前,将传入的时间戳减1秒;

经过验证,方案1会,java.util.Date转过去的java.sql.Date对象会将日期之后的精度全部丢掉,从而导致查询出更多不必要的数据;方案3是可以的,就是可能会查出多一两条数据;方案2也是可以的,相当于从代码上对mysql-connector-java的特性做了补偿。最终我选择的是方案2。

案例复现

利用homebrew安装MysqL,版本是8.0.15,装好后建一个表,用来存放用户信息,sql如下:

使用spirngboot + mybatis作为开发框架,定义一个用户实体,代码如下所示:

定义该实体对应的Mapper,代码如下:

设置连接MysqL相关的配置,代码如下:

编写测试代码,先插入一条数据,然后用时间戳作为查询条件去查询,代码如下:

运行单测,如我们的设想,确实是没有查询出数据来,结果如下:

然后修改代码,利用上面的代码将查询的时间戳按秒取正,代码如下:

再次运行单测,如我们的设想,这次可以查询出数据来了。

不过,这里有个小插曲,我在最开始设计表的时候,使用的sql语句是下面这样的,

聪明如你一定发现了,这里的datetime已经支持小数点后更小的时间精度了,最多支持6位即最多可以支持到微妙级别。这个特性是什么时候引入的呢,我去查阅了[MysqL的官方文档][9],发现这个特性是在MysqL 5.6.4之后开始支持的。

知识点总结

经过了前面的实际案例分析和案例复现,想必读者已经对MysqL中DATETIME这个类型有了一定的认识,接下来跟我一起看下,我们从这个案例中可以总结出哪些经验。

mysql-connector-java的版本和MysqL的版本需要配套使用,例如5.6.4之前的版本,就最好不要使用mysql-connector-java的5.1.23之后的版本,否则就可能会遇到我们这次遇到的问题。

MysqL中用来表示时间的字段类型有:DATE、DATETIME、TIMESTAMP,它们之间有相同点,各自也有自己的特性,我总结了一个表格,如下所示:

DATETIME

类型在MysqL中是以“YYYYMMDDHHMMSS”格式的整数存放的,与时区无关,使用8个字节的空间;

TIMESTAMP类型可以保存的时间范围要小很多,显示的值依赖时区,MysqL的服务器、操作系统以及客户端连接都有时区的设置。

一般情况下推荐使用DATETIME作为时间戳字段,不推荐使用bigint类型来存储时间。

在开发中,应该尽量避免使用时间戳作为查询条件,如果必须要用,则需要充分考虑MysqL的精度和查询参数的精度等问题。

参考资料

https://dev.MysqL.com/doc/refman/8.0/en/datetime.html

《高性能MysqL》

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对小编的支持。

07.空指针引发的血案

07.空指针引发的血案

1. 前言

《手册》的第 7 页和 25 页有两段关于空指针的描述:

【强制】Object 的 equals 方法容易抛空指针异常,应使用常量或确定有值的对象来调用 equals。

【推荐】防止 NPE,是程序员的基本修养,注意 NPE 产生的场景:

  1. 返回类型为基本数据类型,return 包装数据类型的对象时,自动拆箱有可能产生 NPE。

反例:public int f () { return Integer 对象}, 如果为 null,自动解箱抛 NPE。

  1. 数据库的查询结果可能为 null。
  2. 集合里的元素即使 isNotEmpty,取出的数据元素也可能为 null。
  3. 远程调用返回对象时,一律要求进行空指针判断,防止 NPE。
  4. 对于 Session 中获取的数据,建议进行 NPE 检查,避免空指针。
  5. 级联调用 obj.getA ().getB ().getC (); 一连串调用,易产生 NPE。

《手册》对空指针常见的原因和基本的避免空指针异常的方式给了介绍,非常有参考价值。

那么我们思考以下几个问题:

  • 如何学习NullPointerException(简称为 NPE)?
  • 哪些用法可能造 NPE 相关的 BUG?
  • 在业务开发中作为接口提供者和使用者如何更有效地避免空指针呢?

2. 了解空指针

2.1 源码注释

前面介绍过源码是学习的一个重要途径,我们一起看看NullPointerException的源码:

/**
 * Thrown when an application attempts to use {@code null} in a
 * case where an object is required. These include:
 * <ul>
 * <li>Calling the instance method of a {@code null} object.
 * <li>Accessing or modifying the field of a {@code null} object.
 * <li>Taking the length of {@code null} as if it were an array.
 * <li>Accessing or modifying the slots of {@code null} as if it
 *     were an array.
 * <li>Throwing {@code null} as if it were a {@code Throwable}
 *     value.
 * </ul>
 * <p>
 * Applications should throw instances of this class to indicate
 * other illegal uses of the {@code null} object.
 *
 * {@code NullPointerException} objects may be constructed by the
 * virtual machine as if {@linkplain Throwable#Throwable(String,
 * Throwable, boolean, boolean) suppression were disabled and/or the
 * stack trace was not writable}.
 *
 * @author  unascribed
 * @since   JDK1.0
 */
public
class NullPointerException extends RuntimeException {
    private static final long serialVersionUID = 5162710183389028792L;

    /**
     * Constructs a {@code NullPointerException} with no detail message.
     */
    public NullPointerException() {
        super();
    }

    /**
     * Constructs a {@code NullPointerException} with the specified
     * detail message.
     *
     * @param   s   the detail message.
     */
    public NullPointerException(String s) {
        super(s);
    }
}

源码注释给出了非常详尽地解释:

空指针发生的原因是应用需要一个对象时却传入了null,包含以下几种情况:

  1. 调用 null 对象的实例方法。
  2. 访问或者修改 null 对象的属性。
  3. 获取值为 null 的数组的长度。
  4. 访问或者修改值为 null 的二维数组的列时。
  5. 把 null 当做 Throwable 对象抛出时。

实际编写代码时,产生空指针的原因都是这些情况或者这些情况的变种。

《手册》中的另外一处描述

“集合里的元素即使 isNotEmpty,取出的数据元素也可能为null。”

和第 4 条非常类似。

如《手册》中的:

“级联调用 obj.getA ().getB ().getC (); 一连串调用,易产生 NPE。”

和第 1 条很类似,因为每一层都可能得到null

当遇到《手册》中和源码注释中所描述的这些场景时,要注意预防空指针。

另外通过读源码注释我们还得到了 “意外发现”,JVM 也可能会通过Throwable#Throwable(String, Throwable, boolean, boolean)构造函数来构造NullPointerException对象。

2.2 继承体系

通过源码可以看到 NPE 继承自RuntimeException我们可以通过 IDEA 的 “Java Class Diagram” 来查看类的继承体系。

image.png
可以清晰地看到 NPE 继承自RuntimeException,另外我们选取NoSuchFieldExceptionNoSuchFieldErrorNoClassDefFoundError,可以看到Throwable的子类型包括ErrorException, 其中 NPE 又是Exception的子类。

那么为什么ExceptionError有什么区别?Excption又分为哪些类型呢?

我们可以分别去java.lang.Exceptionjava.lang.Error的源码注释中寻找答案。

通过Exception的源码注释我们了解到,Exception分为两类一种是非受检异常(uncheked exceptions)即java.lang.RuntimeException以及其子类;而受检异常(checked exceptions)的抛出需要再普通函数或构造方法上通过throws声明。

通过java.lang.Error的源码注释我们了解到,Error代表严重的问题,不应该被程序try-catch。编译时异常检测时,Error也被视为不可检异常(uncheked exceptions)。

大家可以在 IDEA 中分别查看ExceptionError的子类,了解自己开发中常遇到的异常都属于哪个分类。

我们还可以通过《JLS》第 11 章Exceptions对异常进行学习。

其中在异常的类型这里,讲到:

不可检异常(unchecked exception)包括运行时异常和 error 类。

可检异常(checked exception)不属于不可检异常的所有异常都是可检异常。除 RuntimeException 和其子类,以及 Error 类以及其子类外的其他 Throwable 的子类。

image.png
还有更多关于异常的详细描述,,包括异常的原因、异步异常、异常的编译时检查等,大家可以自己进一步学习。

3. 空指针引发的血案

3.1 最常见的错误姿势

 @Test
    public void test() {
        Assertions.assertThrows(NullPointerException.class, () -> {
            List<UserDTO> users = new ArrayList<>();
            users.add(new UserDTO(1L, 3));
            users.add(new UserDTO(2L, null));
            users.add(new UserDTO(3L, 3));
            send(users);
        });

    }

    // 第 1 处
    private void send(List<UserDTO> users) {
        for (UserDTO userDto : users) {
            doSend(userDto);
        }
    }

    private static final Integer SOME_TYPE = 2;

    private void doSend(UserDTO userDTO) {
        String target = "default";
        // 第 2 处
        if (!userDTO.getType().equals(SOME_TYPE)) {
            target = getTarget(userDTO.getType());
        }
        System.out.println(String.format("userNo:%s, 发送到%s成功", userDTO, target));
    }

    private String getTarget(Integer type) {
        return type + "号基地";
    }

在第 1 处,如果集合为null则会抛空指针;

在第 2 处,如果type属性为null则会抛空指针异常,导致后续都发送失败。

大家看这个例子觉得很简单,看到输入的参数有null本能地就会考虑空指针问题,但是自己写代码时你并不知道上游是否会有null

3. 2 无结果仍返回对象

实际开发中有些同学会有一些非常 “个性” 的写法。

为了避免空指针或避免检查到 null 参数抛异常,直接返回一个空参构造函数创建的对象。

类似下面的做法:

/**
 * 根据订单编号查询订单
 *
 * @param orderNo 订单编号
 * @return 订单
 */
public Order getByOrderNo(String orderNo) {

    if (StringUtils.isEmpty(orderNo)) {
        return new Order();
    }
    // 查询order
    return doGetByOrderNo(orderNo);
}

由于常见的单个数据的查询接口,参数检查不符时会抛异常或者返回null。 极少有上述的写法,因此调用方的惯例是判断结果不为null就使用其中的属性。

这个哥们这么写之后,上层判断返回值不为null, 上层就放心大胆得调用实例函数,导致线上报空指针,就造成了线上 BUG。

3.3 新增 @NonNull 属性反序列化的 BUG

假如有一个订单更新的 RPC 接口,该接口有一个OrderUpdateParam参数,之前有两个属性一个是id一个是name。在某个需求时,新增了一个 extra 属性,且该字段一定不能为null

采用 lombok 的@NonNull注解来避免空指针:

import lombok.Data;
import lombok.NonNull;

import java.io.Serializable;

@Data
public class OrderUpdateParam implements Serializable {
    private static final long serialVersionUID = 3240762365557530541L;

    private Long id;

    private String name;

     // 其它属性
  
    // 新增的属性
    @NonNull
    private String extra;
}

上线后导致没有使用最新 jar 包的服务对该接口的 RPC 调用报错。

我们来分析一下原因,在 IDEA 的 target - classes 目录下找到 DEMO 编译后的 class 文件,IDEA 会自动帮我们反编译:

public class OrderUpdateParam implements Serializable {
    private static final long serialVersionUID = 3240762365557530541L;
    private Long id;
    private String name;
    @NonNull
    private String extra;

    public OrderUpdateParam(@NonNull final String extra) {
        if (extra == null) {
            throw new NullPointerException("extra is marked non-null but is null");
        } else {
            this.extra = extra;
        }
    }

    @NonNull
    public String getExtra() {
        return this.extra;
    }
    public void setExtra(@NonNull final String extra) {
        if (extra == null) {
            throw new NullPointerException("extra is marked non-null but is null");
        } else {
            this.extra = extra;
        }
    }
  // 其他代码

}

我们还可以使用反编译工具:JD-GUI对编译后的 class 文件进行反编译,查看源码。

由于调用方调用的是不含extra属性的 jar 包,并且序列化编号是一致的,反序列化时会抛出 NPE。

Caused by: java.lang.NullPointerException: extra

​        at com.xxx.OrderUpdateParam.<init>(OrderUpdateParam.java:21)

RPC 参数新增 lombok 的@NonNull注解时,要考虑调用方是否及时更新 jar 包,避免出现空指针。

3.4 自动拆箱导致空指针

前面章节讲到了对象转换,如果我们下面的GoodCreateDTO是我们自己服务的对象, 而GoodCreateParam是我们调用服务的参数对象。

@Data
public class GoodCreateDTO {
    private String title;

    private Long price;

    private Long count;
}

@Data
public class GoodCreateParam implements Serializable {

    private static final long serialVersionUID = -560222124628416274L;
    private String title;

    private long price;

    private long count;
}

其中GoodCreateDTOcount属性在我们系统中是非必传参数,本系统可能为null

如果我们没有拉取源码的习惯,直接通过前面的转换工具类去转换。

我们潜意识会认为外部接口的对象类型也都是包装类型,这时候很容易因为转换出现 NPE 而导致线上 BUG。

public class GoodCreateConverter {

    public static GoodCreateParam convertToParam(GoodCreateDTO goodCreateDTO) {
        if (goodCreateDTO == null) {
            return null;
        }
        GoodCreateParam goodCreateParam = new GoodCreateParam();
        goodCreateParam.setTitle(goodCreateDTO.getTitle());
        goodCreateParam.setPrice(goodCreateDTO.getPrice());
        goodCreateParam.setCount(goodCreateDTO.getCount());
        return goodCreateParam;
    }
}

当转换器执行到goodCreateParam.setCount(goodCreateDTO.getCount());会自动拆箱会报空指针。

GoodCreateDTOcount属性为null时,自动拆箱将报空指针。

再看一个花样踩坑的例子

我们作为使用方调用如下的二方服务接口:

public Boolean someRemoteCall();

然后自以为对方肯定会返回TRUEFALSE,然后直接拿来作为判断条件或者转为基本类型,如果返回的是null,则会报空指针异常:

if (someRemoteCall()) {
           // 业务代码
 }

大家看示例的时候可能认为这种情况很简单,自己开发的时候肯定会注意,但是往往事实并非如此。

希望大家可以掌握常见的可能发生空指针场景,在开发是注意预防。

3.5 分批调用合并结果时空指针

大家再看下面这个经典的例子。

因为某些批量查询的二方接口在数据较大时容易超时,因此可以分为小批次调用。

下面封装一个将List数据拆分成每size个一批数据,去调用functionRPC 接口,然后将结果合并。

  public static <T, V> List<V> partitionCallList(List<T> dataList, int size, Function<List<T>, List<V>> function) {

        if (CollectionUtils.isEmpty(dataList)) {
            return new ArrayList<>(0);
        }
        Preconditions.checkArgument(size > 0, "size 必须大于0");

        return Lists.partition(dataList, size)
                .stream()
                .map(function)
                .reduce(new ArrayList<>(),
                        (resultList1, resultList2) -> {
                            resultList1.addAll(resultList2);
                            return resultList1;
                        });


    }

看着挺对,没啥问题,其实则不然。

设想一下,如果某一个批次请求无数据,不是返回空集合而是 null,会怎样?

很不幸,又一个空指针异常向你飞来 …

此时要根据具体业务场景来判断如何处理这里可能产生的空指针异常

如果在某个场景中,返回值为 null 是一定不允许的行为,可以在 function 函数中对结果进行检查,如果结果为 null,可抛异常。

如果是允许的,在调用 map 后,可以过滤 null :

// 省略前面代码
.map(function)
.filter(Objects::nonNull)
// 省略后续代码

4. 预防空指针的一些方法

NPE造成的线上 BUG 还有很多种形式,如何预防空指针很重要。

下面将介绍几种预防 NPE 的一些常见方法:

image.png

4.1 接口提供者角度

4.1.1 返回空集合

如果参数不符合要求直接返回空集合,底层的函数也使用一致的方式:

public List<Order> getByOrderName(String name) {
    if (StringUtils.isNotEmpty(name)) {
        return doGetByOrderName(name);
    }
    return Collections.emptyList();
}

4.1.2 使用 Optional

Optional是 Java 8 引入的特性,返回一个Optional则明确告诉使用者结果可能为空:

public Optional<Order> getByOrderId(Long orderId) {
    return Optional.ofNullable(doGetByOrderId(orderId));
}

如果大家感兴趣可以进入Optional的源码,结合前面介绍的codota工具进行深入学习,也可以结合《Java 8 实战》的相关章节进行学习。

4.1.3 使用空对象设计模式

该设计模式为了解决 NPE 产生原因的第 1 条 “调用null对象的实例方法”。

在编写业务代码时为了避免NPE经常需要先判空再执行实例方法:

public void doSomeOperation(Operation operation) {
    int a = 5;
    int b = 6;
    if (operation != null) {
        operation.execute(a, b);
    }
}

《设计模式之禅》(第二版)554 页在拓展篇讲述了 “空对象模式”。

可以构造一个NullXXX类拓展自某个接口, 这样这个接口需要为null时,直接返回该对象即可:

public class NullOperation implements Operation {

    @Override
    public void execute(int a, int b) {
        // do nothing
    }
}

这样上面的判空操作就不再有必要, 因为我们在需要出现null的地方都统一返回NullOperation,而且对应的对象方法都是有的:

public void doSomeOperation(Operation operation) {
    int a = 5;
    int b = 6;
    operation.execute(a, b);
}

4.2 接口使用者角度

讲完了接口的编写者该怎么做,我们讲讲接口的使用者该如何避免NPE

4.2.1 null 检查

正如《代码简洁之道》第 7.8 节 “别传 null 值” 中所要表达的意义:

可以进行参数检查,对不满足的条件抛出异常。

直接在使用前对不能为null的和不满足业务要求的条件进行检查,是一种最简单最常见的做法。

通过防御性参数检测,可以极大降低出错的概率,提高程序的健壮性:

    @Override
    public void updateOrder(OrderUpdateParam orderUpdateParam) {
        checkUpdateParam(orderUpdateParam);
        doUpdate(orderUpdateParam);
    }

    private void checkUpdateParam(OrderUpdateParam orderUpdateParam) {
        if (orderUpdateParam == null) {
            throw new IllegalArgumentException("参数不能为空");
        }
        Long id = orderUpdateParam.getId();
        String name = orderUpdateParam.getName();
        if (id == null) {
            throw new IllegalArgumentException("id不能为空");
        }
        if (name == null) {
            throw new IllegalArgumentException("name不能为空");
        }
    }

JDK 和各种开源框架中可以找到很多这种模式,java.util.concurrent.ThreadPoolExecutor#execute就是采用这种模式。

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
     // 其他代码
     }

以及org.springframework.context.support.AbstractApplicationContext#assertBeanFactoryActive

protected void assertBeanFactoryActive() {
   if (!this.active.get()) {
      if (this.closed.get()) {
         throw new IllegalStateException(getDisplayName() + " has been closed already");
      }
      else {
         throw new IllegalStateException(getDisplayName() + " has not been refreshed yet");
      }
   }
}

4.2.2 使用 Objects

可以使用 Java 7 引入的 Objects 类,来简化判空抛出空指针的代码。

使用方法如下:

private void checkUpdateParam2(OrderUpdateParam orderUpdateParam) {
    Objects.requireNonNull(orderUpdateParam);
    Objects.requireNonNull(orderUpdateParam.getId());
    Objects.requireNonNull(orderUpdateParam.getName());
}

原理很简单,我们看下源码;

public static <T> T requireNonNull(T obj) {
    if (obj == null)
        throw new NullPointerException();
    return obj;
}

4.2.3 使用 commons 包

我们可以使用 commons-lang3 或者 commons-collections4 等常用的工具类辅助我们判空。

4.2.3.1 使用字符串工具类:org.apache.commons.lang3.StringUtils
public void doSomething(String param) {
    if (StringUtils.isNotEmpty(param)) {
        // 使用param参数
    }
}

4.2.3.2 使用校验工具类:org.apache.commons.lang3.Validate
public static void doSomething(Object param) {
    Validate.notNull(param,"param must not null");
}
public static void doSomething2(List<String> parms) {
    Validate.notEmpty(parms);
}

该校验工具类支持多种类型的校验,支持自定义提示文本等。

前面已经介绍了读源码是最好的学习方式之一,这里我们看下底层的源码:

public static <T extends Collection<?>> T notEmpty(final T collection, final String message, final Object... values) {
    if (collection == null) {
        throw new NullPointerException(String.format(message, values));
    }
    if (collection.isEmpty()) {
        throw new IllegalArgumentException(String.format(message, values));
    }
    return collection;
}

该如果集合对象为 null 则会抛空NullPointerException如果集合为空则抛出IllegalArgumentException

通过源码我们还可以了解到更多的校验函数。

4.2.4 使用集合工具类:org.apache.commons.collections4.CollectionUtils

public void doSomething(List<String> params) {
    if (CollectionUtils.isNotEmpty(params)) {
        // 使用params
    }
}

4.2.5 使用 guava 包

可以使用 guava 包的com.google.common.base.Preconditions前置条件检测类。

同样看源码,源码给出了一个范例。原始代码如下:

public static double sqrt(double value) {
    if (value < 0) {
        throw new IllegalArgumentException("input is negative: " + value);
    }
    // calculate square root
}

使用Preconditions后,代码可以简化为:

 public static double sqrt(double value) {
   checkArgument(value >= 0, "input is negative: %s", value);
   // calculate square root
 }
 

Spring 源码里很多地方可以找到类似的用法,下面是其中一个例子:

org.springframework.context.annotation.AnnotationConfigApplicationContext#register

public void register(Class<?>... annotatedClasses) {
    Assert.notEmpty(annotatedClasses, "At least one annotated class must be specified");
    this.reader.register(annotatedClasses);
}

org.springframework.util.Assert#notEmpty(java.lang.Object[], java.lang.String)

public static void notEmpty(Object[] array, String message) {
    if (ObjectUtils.isEmpty(array)) {
        throw new IllegalArgumentException(message);
    }
}

虽然使用的具体工具类不一样,核心的思想都是一致的。

4.2.6 自动化 API

4.2.6.1 使用 lombok 的@Nonnull注解
 public void doSomething5(@NonNull String param) {
      // 使用param
      proccess(param);
 }

查看编译后的代码:

 public void doSomething5(@NonNull String param) {
      if (param == null) {
          throw new NullPointerException("param is marked non-null but is null");
      } else {
          this.proccess(param);
      }
  }

4.2.6.2 使用 IntelliJ IDEA 提供的 @NotNull 和 @Nullable 注解

maven 依赖如下:

<!-- https://mvnrepository.com/artifact/org.jetbrains/annotations -->
<dependency>
    <groupId>org.jetbrains</groupId>
    <artifactId>annotations</artifactId>
    <version>17.0.0</version>
</dependency>

@NotNull 在参数上的用法和上面的例子非常相似。

public static void doSomething(@NotNull String param) {
    // 使用param
    proccess(param);
}

我们可以去该注解的源码org.jetbrains.annotations.NotNull#exception里查看更多细节,大家也可以使用 IDEA 插件或者前面介绍的 JD-GUI 来查看编译后的 class 文件,去了解 @NotNull 注解的作用。

5. 总结

本节主要讲述空指针的含义,空指针常见的中枪姿势,以及如何避免空指针异常。下一节将为你揭秘 当 switch 遇到空指针,又会发生什么奇妙的事情。

参考资料


  1. 阿里巴巴与 Java 社区开发者.《 Java 开发手册 1.5.0:华山版》.2019:7,25↩︎
  2. James Gosling, Bill Joy, Guy Steele, Gilad Bracha, Alex Buckley.《Java Language Specification: Java SE 8 Edition》. 2015↩︎

dwr引发的血案之四 内存溢出

dwr引发的血案之四 内存溢出

上一篇我们讲到dwr的hello world,但是在真实的应用的场景不是如此简单的,在真实的场景中我们需要做如下的事情,用户登录系统,然后给其他人发送信息,还有不同组的人不能通信,这时我们就需要辨别出不同组别,不同组别不能进行相互通信。所以我们要进行修改我们的代码,增加登录以及类别的判断。
在上一篇的基础上添加LoginInfo类,代码如下:
public class LoginInfo {
 private int groupType;
 private String userName;
 public int getGroupType() {
  return groupType;
 }
 public void setGroupType(int groupType) {
  this.groupType = groupType;
 }
 public String getUserName() {
  return userName;
 }
 public void setUserName(String userName) {
  this.userName = userName;
 }
 public LoginInfo(int groupType,String userName) {
  super();
  this.groupType = groupType;
  this.userName = userName;
 }
 public LoginInfo() {
  super();
 }
 
}
将index.jsp改成登录页,原来的index.jsp改成chat.jsp,DwrControler跳转到chat.jsp,index.jsp的代码如下
<%@ page language="java" pageEncoding="UTF-8" contentType="text/html; charset=UTF-8"%>
<%
String path = request.getcontextpath();
String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/";
%>
<html>
<head>
<base href="<%=basePath%>">
</head>
<body>
<h2>登陆</h2>
 <form action="DwrControler" method="post">
  <table>
   <tr>
    <td>用户名</td>
    <td><input type="text" name="userName" /></td>
   </tr>
   <tr>
    <td>选择会议室</td>
    <td><select name="groupType">
     <option value="1">会议室一</option>
     <option value="2">会议室二</option>
    </select></td>
   </tr>
   <tr>
    <td colspan="2"><input type="submit" value="登陆" /></td>
   </tr>
  </table>
 </form>
 
</body>
</html>
页面效果如下:
然后chat.jsp修改为如下
<%@ page language="java" pageEncoding="UTF-8" isELIgnored="false" contentType="text/html; charset=UTF-8"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<body>

<h2>
欢迎${param.userName }来到
<c:if test="${param.groupType==1}">
会议室一
</c:if>
<c:if test="${param.groupType==2}">
会议室二
</c:if>
</h2>
<textarea id="text" rows="20" cols="50"></textarea>
<input type="text" id="message" />
<input type="button" id="send" value="发送消息"/>
<input type="hidden" id="userName" value="${param.userName }" />
<input type="hidden" id="groupType" value="${param.groupType }" />
</body>

<script type='text/javascript'
src='dwr/engine.js'></script>
<script type='text/javascript'
src='dwr/util.js'></script>
<script type='text/javascript'
src='dwr/interface/dwrService.js'></script>
<script type='text/javascript'
src='scripts/dwr.js?1'></script>
</html>

在dwr.js中修改代码,增加初始的时候写入session的功能
dwr.engine.setActiveReverseAjax(true); // 启用dwr反向ajax
dwr.engine.setNotifyServerOnPageUnload(true);// 刷新页面后销毁当前scriptsession
dwr.engine.setErrorHandler(function() {
    //alert("错误");
}); // 自定义错误处理方式
window.onload=function(){
 
 //chat.jsp初始化完成登陆dwr写入session
 dwrService.login(document.getElementById('userName').value,document.getElementById('groupType').value);
 
 //绑定点击事件
 document.getElementById("send").onclick=function(){
  dwrService.sendMessage(document.getElementById("message").value);
  document.getElementById("message").value="";
 };
};
function getMessage(message){
 document.getElementById("text").value=document.getElementById("text").value+"\n"+message;
}
最后修改dwrService的功能,添加登录的方法,修改发送的方法。代码如下:
package com.ww.service;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.directwebremoting.browser;
import org.directwebremoting.ScriptSession;
import org.directwebremoting.ScriptSessionFilter;
import org.directwebremoting.ScriptSessions;
import org.directwebremoting.WebContext;
import org.directwebremoting.WebContextFactory;
import com.ww.model.LoginInfo;
public class DwrService {
 public DwrService() {
  System.out.println("DwrService Init");
 }
 
 
 public void login(String userName,int groupType){
  LoginInfo info=new LoginInfo(groupType,userName);
  WebContext wctx = WebContextFactory.get();
  ScriptSession ss=wctx.getScriptSession();
  ss.setAttribute("logininfo",info);
 }
 public String sendMessage(final String message) {
  WebContext wctx = WebContextFactory.get();
  ScriptSession ss=wctx.getScriptSession();
  final LoginInfo info=(LoginInfo) ss.getAttribute("logininfo");
  if(info==null){
   return "对不起,你的信息有点问题,请重新登陆";
  }
  browser.withPageFiltered(wctx.getcontextpath() + "/DwrControler",new ScriptSessionFilter() {
     public boolean match(ScriptSession session) {
      LoginInfo in=(LoginInfo) session.getAttribute("logininfo");
      if(in==null){
       //没有登录信息,则不发送信息过去
       return false;
      }
      System.out.println(info.getGroupType()+":"+in.getGroupType());
      if(info.getGroupType()!=in.getGroupType()){
       //不是同一会议室的不发送信息
       return false;
      }
      return true;
     }
    },new Runnable() {
     public void run() {
      SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
      ScriptSessions.addFunctionCall("getMessage",info.getUserName()+"          "+sdf.format(new Date())+message );
     }
    });
  return "";
 }
}
最后实现的效果如下:


在不同的会议室只能看到自己本会议室的消息,一个简单的业务场景demo就这样搭建起来了,但是在真实的业务的场景比这复杂还有很多其他模块组成,启动后就会出现各种各样的问题。其中有个一问题就是
org.apache.jasper.JasperException: javax.servlet.servletexception: java.lang.OutOfMemoryError: PermGen space
	org.apache.jasper.servlet.JspServletWrapper.handleJspException(JspServletWrapper.java:522)
	org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:398)
	org.apache.jasper.servlet.JspServlet.serviceJspFile(JspServlet.java:337)
	org.apache.jasper.servlet.JspServlet.service(JspServlet.java:266)
	javax.servlet.http.HttpServlet.service(HttpServlet.java:803)
	com.ww.DwrControler.doPost(DwrControler.java:33)
	javax.servlet.http.HttpServlet.service(HttpServlet.java:710)
	javax.servlet.http.HttpServlet.service(HttpServlet.java:803)
    
首先是百度上查找outofmemory,说是内存不足,为此专门配置了最大堆内存,但是没有作用。只要人数一上去以后还是会outofmemory,后来使用了jvisualVM查看了下,

发现permGen内存满了,后来查找配置了 -XX:MaxPermSize=256m这样后,在dwr发送消息就不会溢出了。permGen在java中是永久代的意思,放置类信息、常量、静态变量等,然后再我们的dwr如果用户过多就会引发permGen oufofmemroy。因为session中设置的数据过多导致的。所以一个是增大我们的permGen大小,还有就是减小session中数据的大小,不需要的数据不存储。

golang mysql 确诊之旅(2000万开房数据被曝光引发的血案)

golang mysql 确诊之旅(2000万开房数据被曝光引发的血案)

golang mysql 诊断之旅(2000万开房数据被曝光引发的血案) 最近由于某某漏洞原因,2000万开房数据被曝光,数据是csv式,打开慢的要死,于是想把这5000w的开房数据导入mysql,然后用go写个简单的查询工具。 悲剧开始了: 第一步,下载 mysql模块,go get githu

golang mysql 诊断之旅(2000万开房数据被曝光引发的血案)

最近由于某某漏洞原因,2000万开房数据被曝光,数据是csv格式,打开慢的要死,于是想把这5000w的开房数据导入mysql,然后用go写个简单的查询工具。


悲剧开始了:

第一步,下载 mysql模块,go get github.com/go-sql-driver/mysql,

第二步,写个小例子测试下

package main

import (
	"database/sql" //这包一定要引用
	"encoding/json"
	"fmt"                              //这个前面一章讲过
	_ "github.com/go-sql-driver/mysql" //这就是刚才下载的包
)

// 定义一个结构体, 需要大写开头哦, 字段名也需要大写开头哦, 否则json模块会识别不了
// 结构体成员仅大写开头外界才能访问
type User struct {
	User     string `json:"user"`
	Password string `json:"password"`
	Host     string `json:"host"`
}

// 一如既往的main方法
func main() {
	// 格式有点怪, @tcp 是指网络协议(难道支持udp?), 然后是域名和端口
	db, e := sql.Open("mysql", "root:@tcp(192.168.7.15:3306)/mysql?charset=utf8")
	if e != nil { //如果连接出错,e将不是nil的
		print("ERROR?")
		return
	}

	defer db.Close()

	// 提醒一句, 运行到这里, 并不代表数据库连接是完全OK的, 因为发送第一条SQL才会校验密码 汗~!
	rows, e := db.Query("select user,password,host from mysql.user")
	if e != nil {
		fmt.Printf("query error!!%v\n", e)
		return
	}
	if rows == nil {
		print("Rows is nil")
		return
	}
	fmt.Println("DB rows.Next")
	for rows.Next() { //跟java的ResultSet一样,需要先next读取
		user := new(User)
		// rows貌似只支持Scan方法 继续汗~! 当然,可以通过GetColumns()来得到字段顺序
		row_err := rows.Scan(&amp;user.User, &amp;user.Password, &amp;user.Host)
		if row_err != nil {
			print("Row error!!")
			return
		}
		b, _ := json.Marshal(user)
		fmt.Println(string(b)) // 这里没有判断错误, 呵呵, 一般都不会有错吧
	}
	fmt.Println("Done")
}
登录后复制

立即学习“go语言免费学习笔记(深入)”;

点击下载“修复打印机驱动工具”;

结果一直报错:

立即学习“go语言免费学习笔记(深入)”;

点击下载“修复打印机驱动工具”;

	panic: runtime error: index out of range
	
	goroutine 1 [running]:
	github.com/go-sql-driver/mysql.readLengthEncodedInteger(0x10fb0037, 0x1, 0xfc9, 0x0, 0x0, ...)
		E:/go/src/github.com/go-sql-driver/mysql/utils.go:406 +0x3e8
	github.com/go-sql-driver/mysql.skipLengthEnodedString(0x10fb0037, 0x1, 0xfc9, 0x2, 0x0, ...)
		E:/go/src/github.com/go-sql-driver/mysql/utils.go:366 +0x38
	github.com/go-sql-driver/mysql.(*mysqlConn).readColumns(0x10f88230, 0x1, 0x10f86500, 0x1, 0x1, ...)
		E:/go/src/github.com/go-sql-driver/mysql/packets.go:482 +0x389
	github.com/go-sql-driver/mysql.(*mysqlConn).getSystemVar(0x10f88230, 0x530b88, 0x12, 0x0, 0x0, ...)
		E:/go/src/github.com/go-sql-driver/mysql/connection.go:228 +0x118
	github.com/go-sql-driver/mysql.(*mysqlDriver).Open(0x5f0bf4, 0x547aa8, 0x2f, 0x1, 0x10f9f900, ...)
		E:/go/src/github.com/go-sql-driver/mysql/driver.go:70 +0x2de
	database/sql.(*DB).conn(0x10f85e40, 0x10f50228, 0xff014c, 0x5)
		C:/Users/ADMINI~1/AppData/Local/Temp/2/bindist465310315/go/src/pkg/database/sql/sql.go:484 +0x15e
	database/sql.(*DB).query(0x10f85e40, 0x527b68, 0x8, 0x0, 0x0, ...)
		C:/Users/ADMINI~1/AppData/Local/Temp/2/bindist465310315/go/src/pkg/database/sql/sql.go:708 +0x58
	database/sql.(*DB).Query(0x10f85e40, 0x527b68, 0x8, 0x0, 0x0, ...)
		C:/Users/ADMINI~1/AppData/Local/Temp/2/bindist465310315/go/src/pkg/database/sql/sql.go:699 +0x6b
	main.main()
		E:/go/src/testmysql/testmysql.go:54 +0x89
	
	goroutine 3 [syscall]:
	syscall.Syscall6(0x7c80a7bd, 0x5, 0xf70, 0x10f86420, 0x10f50280, ...)
		C:/Users/ADMINI~1/AppData/Local/Temp/2/bindist465310315/go/src/pkg/runtime/zsyscall_windows_windows_386.c:97 +0x49
	syscall.GetQueuedCompletionStatus(0xf70, 0x10f86420, 0x10f50280, 0x10f50278, 0xffffffff, ...)
		C:/Users/ADMINI~1/AppData/Local/Temp/2/bindist465310315/go/src/pkg/syscall/zsyscall_windows_386.go:507 +0x7e
	net.(*resultSrv).Run(0x10f50260)
		C:/Users/ADMINI~1/AppData/Local/Temp/2/bindist465310315/go/src/pkg/net/fd_windows.go:150 +0x11a
	created by net.startServer
		C:/Users/ADMINI~1/AppData/Local/Temp/2/bindist465310315/go/src/pkg/net/fd_windows.go:285 +0xde
	
	goroutine 4 [select]:
	net.(*ioSrv).ProcessRemoteIO(0x10f50268)
		C:/Users/ADMINI~1/AppData/Local/Temp/2/bindist465310315/go/src/pkg/net/fd_windows.go:183 +0x171
	created by net.startServer
		C:/Users/ADMINI~1/AppData/Local/Temp/2/bindist465310315/go/src/pkg/net/fd_windows.go:293 +0x163
	exit status 2
登录后复制

刚开始怀疑windows没有装mysql驱动,可是兴趣来了想分析下堆栈信息,正好学点新东西,

立即学习“go语言免费学习笔记(深入)”;

点击下载“修复打印机驱动工具”;

log大法,跟踪readLengthEncodedInteger,并加入如下调试代码,打印b[]byte的内存信息,发现b[0] = 0xfe,但是后面却没有数据了,所以造成了数组b的index溢出,完整代码如下:

立即学习“go语言免费学习笔记(深入)”;

点击下载“修复打印机驱动工具”;

func readLengthEncodedInteger(b []byte) (num uint64, isNull bool, n int) {
	fmt.Println(b)
	fmt.Printf("0x%02x\n", b[0])
	switch b[0] {

	// 251: NULL
	case 0xfb:
		n = 1
		isNull = true
		return

	// 252: value of following 2
	case 0xfc:
		num = uint64(b[1]) | uint64(b[2])
<br>
继续跟踪发现:package.go的readColumns中有一个很奇怪的退出,但是没有这个0xfe的处理,而且奇怪的是for循环只有这里可以正常return。。。
<p></p><p><span>立即学习</span>“<a href="https://pan.quark.cn/s/00968c3c2c15"rel="nofollow" target="_blank">go语言免费学习笔记(深入)</a>”;</p><p><span>点击下载</span>“<a href="https://teacher.php.cn/jump/126"rel="nofollow" target="_blank">修复打印机驱动工具</a>”;</p>
<p></p><p><span>立即学习</span>“<a href="https://pan.quark.cn/s/00968c3c2c15"rel="nofollow" target="_blank">go语言免费学习笔记(深入)</a>”;</p><p><span>点击下载</span>“<a href="https://teacher.php.cn/jump/126"rel="nofollow" target="_blank">修复打印机驱动工具</a>”;</p>
<pre name="code">func (mc *mysqlConn) readColumns(count int) (columns []mysqlField, err error) {
	var data []byte
	var i, pos, n int
	var name []byte

	columns = make([]mysqlField, count)
	fmt.Println("count:")
	fmt.Println(count)
	for {
		data, err = mc.readPacket()
		if err != nil {
			return
		}

		// EOF Packet
		if data[0] == iEOF &amp;&amp; len(data) == 5 {
			if i != count {
				err = fmt.Errorf("ColumnsCount mismatch n:%d len:%d", count, len(columns))
			}
			return
		}
登录后复制

立即学习“go语言免费学习笔记(深入)”;

点击下载“修复打印机驱动工具”;


不太甘心,google读mysql的protocol http://dev.mysql.com/doc/internals/en/com-field-list-response.html

发现重要说明,这个Column的包后面有一个EOF_Packet,也就是上面的0xfe,于是怀疑这是go mysql driver的一个bug

立即学习“go语言免费学习笔记(深入)”;

点击下载“修复打印机驱动工具”;

15.6.5.1. COM_FIELD_LIST response
The response to a COM_FIELD_LIST can either be a

a ERR_Packet or

one or more Column Definition packets and a closing EOF_Packet
登录后复制


立即学习“go语言免费学习笔记(深入)”;

点击下载“修复打印机驱动工具”;

于是到go-sql-driver的官网 https://github.com/go-sql-driver/mysql/blob/master/packets.go,想pull->提交这个bug的patch,结果。。。

我擦!!这个issue已经在github上修改了,如下:

立即学习“go语言免费学习笔记(深入)”;

点击下载“修复打印机驱动工具”;

           // EOF Packet
                if data[0] == iEOF &amp;&amp; (len(data) == 5 || len(data) == 1) {
                        if i == count {
                                return columns, nil
                        }
                        return nil, fmt.Errorf("ColumnsCount mismatch n:%d len:%d", count, len(columns))
                }
登录后复制

顿时崩溃啊,猜想go get下来的代码是旧版本的,看README.md

立即学习“go语言免费学习笔记(深入)”;

点击下载“修复打印机驱动工具”;

**Current tagged Release:** June 03, 2013 (Version 1.0.1)

再看github上

**Current tagged Release:** May 14, 2013 (Version 1.0)

疑问来了,虽然github库上的release版本还是旧的,但是go get下来的新版本存在这个问题!!!???


解决方法:唯一的方式就是取一个github上的版本git clone https://github.com/go-sql-driver/mysql.git,不要用go get的版本。


最后,哪位大神解释下 go get和git clone下来的版本为什么不一样??


补充:

1.@ASTA谢: 因为你之前安装过github.com/go-sql-driver/mysql,所以你go get的时候不会更新,你必须使用go get -u更新,而git clone是最新版本的更新(2013-10-24 16:23:57),这个go get -u 是更新,但不是问题所在,@jimmykuu:默认是去获取tag为go1的代码,果然如jimmykuu所说:我查看tag为go1的代码:https://github.com/go-sql-driver/mysql/tree/go1,和go get下来的相同,而且golang上有说明。。。"When checking out or updating a package, get looks for a branch or tag that matches the locally installed version of Go. The most important rule is that if the local installation is running version "go1", get searches for a branch or tag named "go1". If no such version exists it retrieves the most recent version of the package."

所以我的结论就是go-sql-driver中的go1 tag充满bug,不负责任!

2. 刚才看了下,之所以其他人没有碰到这个问题,是因为我正好用的古老版本mysql4.1 ,补丁在这fix crash when connect to mysql4.1:https://github.com/go-sql-driver/mysql/commit/4a178617b97609ebd4d4a0ae5791225540c1bb26#diff-2357b8494bbd2f27c09e61fc8ef5f092 


     }
     
         // EOF Packet
    -    if data[0] == iEOF && len(data) == 5 {
    +    if data[0] == iEOF && (len(data) == 5 || len(data) == 1) {
           if i != count {
             err = fmt.Errorf("ColumnsCount mismatch n:%d len:%d", count, len(columns))
           }

3.用git clone的新版本替换后,切记删掉pkg下生成的mysql.a文件!!


golang mysql 诊断之旅(2000万开房数据被曝光引发的血案)

golang mysql 诊断之旅(2000万开房数据被曝光引发的血案)

最近由于某某漏洞原因,2000万开房数据被曝光,数据是csv格式,打开慢的要死,于是想把这2000w的开房数据导入MysqL,然后用go写个简单的查询工具。


悲剧开始了:

第一步,下载 MysqL模块,go get github.com/go-sql-driver/MysqL,

第二步,写个小例子测试下

package main

import (
	"database/sql" //这包一定要引用
	"encoding/json"
	"fmt"                              //这个前面一章讲过
	_ "github.com/go-sql-driver/MysqL" //这就是刚才下载的包
)

// 定义一个结构体,需要大写开头哦,字段名也需要大写开头哦,否则json模块会识别不了
// 结构体成员仅大写开头外界才能访问
type User struct {
	User     string `json:"user"`
	Password string `json:"password"`
	Host     string `json:"host"`
}

// 一如既往的main方法
func main() {
	// 格式有点怪,@tcp 是指网络协议(难道支持udp?),然后是域名和端口
	db,e := sql.Open("MysqL","root:@tcp(192.168.7.15:3306)/MysqL?charset=utf8")
	if e != nil { //如果连接出错,e将不是nil的
		print("ERROR?")
		return
	}

	defer db.Close()

	// 提醒一句,运行到这里,并不代表数据库连接是完全OK的,因为发送第一条sql才会校验密码 汗~!
	rows,e := db.Query("select user,password,host from MysqL.user")
	if e != nil {
		fmt.Printf("query error!!%v\n",e)
		return
	}
	if rows == nil {
		print("Rows is nil")
		return
	}
	fmt.Println("DB rows.Next")
	for rows.Next() { //跟java的ResultSet一样,需要先next读取
		user := new(User)
		// rows貌似只支持Scan方法 继续汗~! 当然,可以通过GetColumns()来得到字段顺序
		row_err := rows.Scan(&user.User,&user.Password,&user.Host)
		if row_err != nil {
			print("Row error!!")
			return
		}
		b,_ := json.Marshal(user)
		fmt.Println(string(b)) // 这里没有判断错误,呵呵,一般都不会有错吧
	}
	fmt.Println("Done")
}

结果一直报错:

	panic: runtime error: index out of range
	
	goroutine 1 [running]:
	github.com/go-sql-driver/MysqL.readLengthEncodedInteger(0x10fb0037,0x1,0xfc9,0x0,...)
		E:/go/src/github.com/go-sql-driver/MysqL/utils.go:406 +0x3e8
	github.com/go-sql-driver/MysqL.skipLengthEnodedString(0x10fb0037,0x2,...)
		E:/go/src/github.com/go-sql-driver/MysqL/utils.go:366 +0x38
	github.com/go-sql-driver/MysqL.(*MysqLConn).readColumns(0x10f88230,0x10f86500,...)
		E:/go/src/github.com/go-sql-driver/MysqL/packets.go:482 +0x389
	github.com/go-sql-driver/MysqL.(*MysqLConn).getSystemVar(0x10f88230,0x530b88,0x12,...)
		E:/go/src/github.com/go-sql-driver/MysqL/connection.go:228 +0x118
	github.com/go-sql-driver/MysqL.(*MysqLDriver).Open(0x5f0bf4,0x547aa8,0x2f,0x10f9f900,...)
		E:/go/src/github.com/go-sql-driver/MysqL/driver.go:70 +0x2de
	database/sql.(*DB).conn(0x10f85e40,0x10f50228,0xff014c,0x5)
		C:/Users/ADMINI~1/AppData/Local/Temp/2/bindist465310315/go/src/pkg/database/sql/sql.go:484 +0x15e
	database/sql.(*DB).query(0x10f85e40,0x527b68,0x8,...)
		C:/Users/ADMINI~1/AppData/Local/Temp/2/bindist465310315/go/src/pkg/database/sql/sql.go:708 +0x58
	database/sql.(*DB).Query(0x10f85e40,...)
		C:/Users/ADMINI~1/AppData/Local/Temp/2/bindist465310315/go/src/pkg/database/sql/sql.go:699 +0x6b
	main.main()
		E:/go/src/testMysqL/testMysqL.go:54 +0x89
	
	goroutine 3 [syscall]:
	syscall.Syscall6(0x7c80a7bd,0x5,0xf70,0x10f86420,0x10f50280,...)
		C:/Users/ADMINI~1/AppData/Local/Temp/2/bindist465310315/go/src/pkg/runtime/zsyscall_windows_windows_386.c:97 +0x49
	syscall.GetQueuedCompletionStatus(0xf70,0x10f50278,0xffffffff,...)
		C:/Users/ADMINI~1/AppData/Local/Temp/2/bindist465310315/go/src/pkg/syscall/zsyscall_windows_386.go:507 +0x7e
	net.(*resultSrv).Run(0x10f50260)
		C:/Users/ADMINI~1/AppData/Local/Temp/2/bindist465310315/go/src/pkg/net/fd_windows.go:150 +0x11a
	created by net.startServer
		C:/Users/ADMINI~1/AppData/Local/Temp/2/bindist465310315/go/src/pkg/net/fd_windows.go:285 +0xde
	
	goroutine 4 [select]:
	net.(*ioSrv).ProcessRemoteIO(0x10f50268)
		C:/Users/ADMINI~1/AppData/Local/Temp/2/bindist465310315/go/src/pkg/net/fd_windows.go:183 +0x171
	created by net.startServer
		C:/Users/ADMINI~1/AppData/Local/Temp/2/bindist465310315/go/src/pkg/net/fd_windows.go:293 +0x163
	exit status 2

刚开始怀疑windows没有装MysqL驱动,可是兴趣来了想分析下堆栈信息,正好学点新东西,

log大法,跟踪readLengthEncodedInteger,并加入如下调试代码,打印b[]byte的内存信息,发现b[0] = 0xfe,但是后面却没有数据了,所以造成了数组b的index溢出,完整代码如下:

func readLengthEncodedInteger(b []byte) (num uint64,isNull bool,n int) {
	fmt.Println(b)
	fmt.Printf("0x%02x\n",b[0])
	switch b[0] {

	// 251: NULL
	case 0xfb:
		n = 1
		isNull = true
		return

	// 252: value of following 2
	case 0xfc:
		num = uint64(b[1]) | uint64(b[2])<<8
		n = 3
		return

	// 253: value of following 3
	case 0xfd:
		num = uint64(b[1]) | uint64(b[2])<<8 | uint64(b[3])<<16
		n = 4
		return

	// 254: value of following 8
	case 0xfe:
		num = uint64(b[1]) | uint64(b[2])<<8 | uint64(b[3])<<16 |
			uint64(b[4])<<24 | uint64(b[5])<<32 | uint64(b[6])<<40 |
			uint64(b[7])<<48 | uint64(b[8])<<54
		n = 9
		return
	}

	// 0-250: value of first byte
	num = uint64(b[0])
	n = 1
	return
}

继续跟踪发现:package.go的readColumns中有一个很奇怪的退出,但是没有这个0xfe的处理,而且奇怪的是for循环只有这里可以正常return。。。
func (mc *MysqLConn) readColumns(count int) (columns []MysqLField,err error) {
	var data []byte
	var i,pos,n int
	var name []byte

	columns = make([]MysqLField,count)
	fmt.Println("count:")
	fmt.Println(count)
	for {
		data,err = mc.readPacket()
		if err != nil {
			return
		}

		// EOF Packet
		if data[0] == iEOF && len(data) == 5 {
			if i != count {
				err = fmt.Errorf("ColumnsCount mismatch n:%d len:%d",count,len(columns))
			}
			return
		}


不太甘心,google读MysqL的protocol http://dev.mysql.com/doc/internals/en/com-field-list-response.html

发现重要说明,这个Column的包后面有一个EOF_Packet,也就是上面的0xfe,于是怀疑这是go MysqL driver的一个bug

15.6.5.1. COM_FIELD_LIST response
The response to a COM_FIELD_LIST can either be a

a ERR_Packet or

one or more Column DeFinition packets and a closing EOF_Packet


于是到go-sql-driver的官网https://github.com/go-sql-driver/mysql/blob/master/packets.go,想pull->提交这个bug的patch,结果。。。

我擦!!这个issue已经在github上修改了,如下:

           // EOF Packet
                if data[0] == iEOF && (len(data) == 5 || len(data) == 1) {
                        if i == count {
                                return columns,nil
                        }
                        return nil,fmt.Errorf("ColumnsCount mismatch n:%d len:%d",len(columns))
                }

顿时崩溃啊,猜想go get下来的代码是旧版本的,看README.md

**Current tagged Release:** June 03,2013 (Version 1.0.1)

再看github上

**Current tagged Release:** May 14,2013 (Version 1.0)

疑问来了,虽然github库上的release版本还是旧的,但是go get下来的新版本存在这个问题!!!???


解决方案:唯一的方式就是取一个github上的版本git clonehttps://github.com/go-sql-driver/MysqL.git,不要用go get的版本。


最后,哪位大神解释下 go get和git clone下来的版本为什么不一样??


补充:

1.@ASTA谢: 因为你之前安装过github.com/go-sql-driver/MysqL,所以你go get的时候不会更新,你必须使用go get -u更新,而git clone是最新版本的更新(2013-10-24 16:23:57),这个go get -u 是更新,但不是问题所在,@jimmykuu:默认是去获取tag为go1的代码,果然如jimmykuu所说:我查看tag为go1的代码:https://github.com/go-sql-driver/mysql/tree/go1,和go get下来的相同,而且golang上有说明。。。"When checking out or updating a package,get looks for a branch or tag that matches the locally installed version of Go. The most important rule is that if the local installation is running version "go1",get searches for a branch or tag named "go1". If no such version exists it retrieves the most recent version of the package."

所以我的结论就是go-sql-driver中的go1 tag充满bug,不负责任!

2. 刚才看了下,之所以其他人没有碰到这个问题,是因为我正好用的古老版本MysqL4.1 ,补丁在这fix crash when connect to MysqL4.1:https://github.com/go-sql-driver/mysql/commit/4a178617b97609ebd4d4a0ae5791225540c1bb26#diff-2357b8494bbd2f27c09e61fc8ef5f092


}
//EOFPacket
-ifdata[0]==iEOF&&len(data)==5{
+ifdata[0]==iEOF&&(len(data)==5||len(data)==1){
ifi!=count{
err=fmt.Errorf("ColumnsCountmismatchn:%dlen:%d",len(columns))
}

3.用git clone的新版本替换后,切记删掉pkg下生成的MysqL.a文件!!

今天关于一起因MySQL时间戳精度引发的血案分析时间戳 mysql的讲解已经结束,谢谢您的阅读,如果想了解更多关于07.空指针引发的血案、dwr引发的血案之四 内存溢出、golang mysql 确诊之旅(2000万开房数据被曝光引发的血案)、golang mysql 诊断之旅(2000万开房数据被曝光引发的血案)的相关知识,请在本站搜索。

本文标签: