4

Mysql8.0 jdbc驱动时区问题

 2 years ago
source link: https://ppj19891020.github.io/2021/03/25/20210325_Mysql8-0-jdbc%E9%A9%B1%E5%8A%A8%E6%97%B6%E5%8C%BA%E9%97%AE%E9%A2%98/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Mysql8.0 jdbc驱动时区问题

Posted on

2021-03-25

我们公司在升级Apollo1.8.1版本之后,发Item表的创建时间和更新时间字段与portal上展示差了13个小时,现象如下:

20210325114050.png

Item表结构:

20210325114147.png

  1. 刚开始我们想到肯定是数据库的时区与Adminservice服务器时间的时区不一致导致的。

    Adminservice所在的服务器时区是CST中国标准时间:

    20210325113208.png

    Mysql服务器的时区如下:

    20210325113703.png

    发现服务器和mysql服务器的时区是一致的,在加上和运维沟通后他们最近也没有升级服务器相关,应该不是这里的问题。

  2. 排查jpa关于时间戳的转换

    查看关于jpa时间戳的准换还是用了最基本的方式去转换时间戳,不涉及时区的转换,代码如下

    org.hibernate.type.TimestampType

    public class TimestampType
    extends AbstractSingleColumnStandardBasicType<Date>
    implements VersionType<Date>, LiteralType<Date> {
    public static final TimestampType INSTANCE = new TimestampType();

    public TimestampType() {
    super( TimestampTypeDescriptor.INSTANCE, JdbcTimestampTypeDescriptor.INSTANCE );
    }

    @Override
    public String getName() {
    return "timestamp";
    }

    @Override
    public String[] getRegistrationKeys() {
    return new String[] { getName(), Timestamp.class.getName(), java.util.Date.class.getName() };
    }

    @Override
    public Date next(Date current, SharedSessionContractImplementor session) {
    return seed( session );
    }

    @Override
    public Date seed(SharedSessionContractImplementor session) {
    return new Timestamp( System.currentTimeMillis() );
    }

    @Override
    public Comparator<Date> getComparator() {
    return getJavaTypeDescriptor().getComparator();
    }

    @Override
    public String objectToSQLString(Date value, Dialect dialect) throws Exception {
    final Timestamp ts = Timestamp.class.isInstance( value )
    ? ( Timestamp ) value
    : new Timestamp( value.getTime() );
    // TODO : use JDBC date literal escape syntax? -> {d 'date-string'} in yyyy-mm-dd hh:mm:ss[.f...] format
    return StringType.INSTANCE.objectToSQLString( ts.toString(), dialect );
    }

    @Override
    public Date fromStringValue(String xml) throws HibernateException {
    return fromString( xml );
    }
    }
  3. mysql jdbc连接排查

    由于之前的apollo1.5.2版本和1.8.1版本,升级了myql驱动,由之前的5.1.46升级到8.0.16,因此怀疑是不是mysql驱动导致的时区问题。然后看到网上确实还真是这个原因,参考文档https://blog.csdn.net/weixin_41787459/article/details/105790044

    查看驱动关于timezone的源码如下

    com.mysql.cj.jdbc.ConnectionImpl

    private void initializePropsFromServer() throws SQLException {
    //......
    this.session.getProtocol().initServerSession();
    //......
    }

    com.mysql.cj.protocol.a.NativeProtocol

    public void initServerSession() {
    configureTimezone();
    //......
    }

    public void configureTimezone() {
    String configuredTimeZoneOnServer = this.serverSession.getServerVariable("time_zone");

    if ("SYSTEM".equalsIgnoreCase(configuredTimeZoneOnServer)) {
    configuredTimeZoneOnServer = this.serverSession.getServerVariable("system_time_zone");
    }

    String canonicalTimezone = getPropertySet().getStringProperty(PropertyKey.serverTimezone).getValue();

    if (configuredTimeZoneOnServer != null) {
    // user can override this with driver properties, so don't detect if that's the case
    if (canonicalTimezone == null || StringUtils.isEmptyOrWhitespaceOnly(canonicalTimezone)) {
    try {
    canonicalTimezone = TimeUtil.getCanonicalTimezone(configuredTimeZoneOnServer, getExceptionInterceptor());
    } catch (IllegalArgumentException iae) {
    throw ExceptionFactory.createException(WrongArgumentException.class, iae.getMessage(), getExceptionInterceptor());
    }
    }
    }

    if (canonicalTimezone != null && canonicalTimezone.length() > 0) {
    this.serverSession.setServerTimeZone(TimeZone.getTimeZone(canonicalTimezone));

    //
    // The Calendar class has the behavior of mapping unknown timezones to 'GMT' instead of throwing an exception, so we must check for this...
    //
    if (!canonicalTimezone.equalsIgnoreCase("GMT") && this.serverSession.getServerTimeZone().getID().equals("GMT")) {
    throw ExceptionFactory.createException(WrongArgumentException.class, Messages.getString("Connection.9", new Object[] { canonicalTimezone }),
    getExceptionInterceptor());
    }
    }

    this.serverSession.setDefaultTimeZone(this.serverSession.getServerTimeZone());
    }

    追踪代码可知,当 MySQL 的 time_zone 值为 SYSTEM 时,会取 system_time_zone 值作为协调时区。

    重点在这里!若 String configuredTimeZoneOnServer 得到的是 CST 那么 Java 会误以为这是 CST -0500 ,因此 TimeZone.getTimeZone(canonicalTimezone) 会给出错误的时区信息。

    本机默认时区是 Asia/Shanghai +0800 ,误认为服务器时区为 CST -0500 ,实际上服务器是 CST +0800

    Timestamp 被转换为会话时区的时间字符串了。问题到此已然明晰:

    JDBC 误认为会话时区在 CST-5
    JBDC 把 Timestamp+0 转为 CST-5 的 String-5
    MySQL 认为会话时区在 CST+8,将 String-5 转为 Timestamp-13
    最终结果相差 13 个小时!如果处在冬令时还会相差 14 个小时。

  1. jdbc配置连接指定时区(推荐)

    #原有配置
    #jdbc:mysql://localhost:3306/test?characterEncoding=UTF-8

    #指定为东八区(北京时间)
    jdbc:mysql://localhost:3306/test?characterEncoding=UTF-8&serverTimezone=GMT%2B8

    #或指定为上海时间
    #jdbc:mysql://localhost:3306/test?characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
  2. 修改数据库时区(不推荐使用,可能影响其他程序)

    #查询数据库时区
    show variables like '%time_zone'

    #在my.cnf文件里指定时区,添加下行代码
    default-time_zone = '+8:00

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK