生产问题(十三)谷歌Protobuf误修改系统全局时区

一、引言

        最近其他组出了个线上问题,导致用户的时间出现问题,影响用户出行,后来才发现是谷歌的Protobuf会更改系统全局时区。不过有一说一,感觉jdk的问题更大。

        Protobuf(Protocol Buffers)是一种轻量级的数据序列化格式,由Google开发。它可以用于将结构化数据序列化为二进制格式,以便在不同的系统之间进行数据交换和存储。Protobuf具有高效、紧凑和可扩展的特点,可以在多种编程语言中使用。它通常用于网络通信、数据存储和配置文件等方面。

二、时区结构

        一开始这个问题看的作者非常迷惑,因为报告写的是修改了机器时区,什么意思?调用了操作系统对应时区的api?不可能吧,这种代码在哪里都不会给他用的,review要被喷死,再看看。

        哦,原来是jvm时区的TimeZone里面的全局变量被改了。这个不能叫做机器时区,就是个全局的时区变量。

        在看问题之前先要看看时区的结构,不然不了解全局的时区变量在哪里。

1. TimeZone(时区):TimeZone 类用于表示不同地区的时区信息。它提供了获取和设置时区偏移量、获取时区名称等方法。通过 TimeZone 类,你可以将日期和时间转换为特定时区的表示形式。

2. Calendar(日历):Calendar 类是一个抽象类,用于处理日期和时间的计算和操作。它提供了获取和设置年、月、日、时、分、秒等日期和时间字段的方法。Calendar 类还提供了一些方便的方法,如计算两个日期之间的差异等。通过 Calendar 类,你可以执行日期和时间的各种操作。

3. GregorianCalendar(公历):GregorianCalendar 类是 Calendar 类的一个具体实现,用于处理公历日期和时间。它继承了 Calendar 类的功能,并提供了更多的方法和功能。GregorianCalendar 类支持闰年、月份天数等公历特性。

        一旦使用 TimeZone 类初始化了一个时区对象,该时区对象的偏移量和其他属性是固定的,不会随着时间的推移而变化。时区对象的属性在初始化时被确定,并且不会自动更新。
        不过时区对象的属性可以通过调用 TimeZone 类的方法来进行更改。例如setRawOffset() 方法来更改时区的原始偏移量,或者通过 setDSTSavings() 方法来更改夏令时的偏移量。

三、分析-构造函数

        这其实是错误的方向,想直接看Protobuf问题的可以直接跳到第四章,不过作者认为错误的方向也是有必要的,因为不管是一时的还是长期的错误方向,既然它能使经验丰富的开发人员产生错误的想法,他就一定是有价值的。

        就像很多学校研究,其实不一定能研究出什么对的,但是可以排除很多的错误的方向,并且在很多方面为别人提供避雷指引。

        出问题的代码

public sType() {
    this.sDate = new GregorianCalendar(1,0,1,0,0,0);
    this.sDate.setTimeZone(ZoneInfo.getTimeZone("UTC"));
}

        这是框架生成的一个构造方法,相对于在不为空的情况下给你时间一个默认值,默认值是0时区。

        按道理说这样只是一个局部变量,为什么会对TimeZone的全局变量造成影响呢?只能说明在这段代码里面,把一个局部变量和全局变量做了关联,使用同一个引用,然后局部变量被修改,映射到了全局变量上面。

        这里拿到timezone的全局变量

        全局变量传递

         这时候Calendar的局部变量与TimeZone的全局变量同一个引用

        这里传入了TimeZone的全局变量,然后给到了Calendar那个局部变量

        第二行setTimeZone把Calendar的局部变量改为0时区,但是这时候TimeZone的全局变量和Calendar局部变量引用相同,全局变量也被修改

        这里会把局部变量的引用直接换成入参

        TimeZone的这个全局变量和局部变量绑定了,所以改局部变量就会改全局变量

        有的同学看着这个应该就会有想法了,因为引用关联是会有影响,但是需要修改引用的值。直接替换引用对于第一个全局变量是不产生影响的

四、分析-Protobuf

        需要先写个demo,结合目前的情况,是因为类里面有个GregorianCalendar类型的字段,然后这个类被Protobuf序列化存到redis,又在其他时候被反序列化拿出来用。那么demo就很清晰了:

private static final DefaultIdStrategy ID_STRATEGY = ((DefaultIdStrategy)RuntimeEnv.ID_STRATEGY);

private static <T> byte[] serialPojoByClass(T value) {
byte[] result = null;

 Class<T> clazz = (Class<T>)value.getClass();
 Schema<T> schema = RuntimeSchema.getSchema(clazz, ID_STRATEGY);
 result = ProtobufIOUtil.toByteArray(value, schema, LinkedBuffer.allocate());

 return result;
}

public static class DateTest {
private GregorianCalendar testDate;

 public void setT(GregorianCalendar testDate) {
this.testDate = testDate;
 }
}

public static <T> T deserializePojo(byte[] bytes, Class<T> clazz)
throws IllegalAccessException, InstantiationException {

if (bytes == null || bytes.length == 0) {
return null;
 }

return deserializePojo(bytes, 0, bytes.length, clazz);
}

private static <T> T deserializePojoByClass(byte[] bytes, int offset, int length, Class<T> clazz)
throws IllegalAccessException, InstantiationException {
if (clazz.isArray()) {
return null;
 }

Schema<T> schema = RuntimeSchema.getSchema(clazz, ID_STRATEGY);
 T result = clazz.newInstance();
 ProtobufIOUtil.mergeFrom(bytes, offset, length, result, schema);
 return result;
}

public static <T> T deserializePojo(byte[] bytes, int offset, int length, Class<T> clazz)
throws InstantiationException, IllegalAccessException {

if (bytes == null || bytes.length == 0) {
return null;
 }

return deserializePojoByClass(bytes, offset, length, clazz);
}

public static void main(String[] args) throws IOException, InstantiationException, IllegalAccessException {
DateTest dateTest = new DateTest();
 GregorianCalendar t = new GregorianCalendar(1, 0, 1, 0, 0, 0);
 t.setTimeZone(ZoneInfo.getTimeZone("UTC"));
 dateTest.setT(t);
 byte[] s = serialPojoByClass(dateTest);
 System.out.println("序列化前:" + TimeZone.getDefault().getID());
 deserializePojo(s, DateTest.class);
 System.out.println("序列化后:" + TimeZone.getDefault().getID());

}

        先看效果再分析,很奇怪吧,我只是序列化再反序列化,怎么全局时区被改了。

        

        这时候把局部变量取出来了,局部变量引用着全局变量

        pb这里new了GregorianCalendar,和全局变量绑定

        这时候开始设置全局变量里面的每一个字段,字段是从序列化字符串拿出来

        取出了时区的id

        unsafe替换字段,时区彻底被改变

五、总结

        这个过程简单点说就是Protobuf会把对象里面的每一个字段都new出来,如果对象是字段继续new里面的对象,并且设置里面对象的每一个字段,所以最底层的时区id都会被改变,替换成序列化字符串里面的值。

        本来这是没什么问题的,糟糕的是GregorianCalendar里面的字段是直接和TimeZone的全局时区关联的,改他就是在改全局时区,所以后面就会导致系统时区是错的。

        这个锅其实应该是jdk和Protobuf一起背,Protobuf责任还要小一点,Protobuf会说我只是做序列化反序列化,你jdk怎么搞一个不安全的GregorianCalendar出来呢?jdk会说我有问题,但是这个你们提供工具就要考虑各种情况做兼容,如果用户自己定义了类相似的类呢?

        这个其实就非常像工作中到扯皮状态的两个组了,作为使用者都不好提issue。各位同学用的时候注意Protobuf的问题,也要注意GregorianCalendar这个类。

  • 30
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

胖当当技术

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值