记录一种系统用户跨时区动态展示和存储的方案,并解决SimpleDateFormat的线程安全问题

问题描述

当系统的面向用户为国外用户时,不同国家和地区的用户可能存在时区不同的问题。针对不同的时区,如何正确对用户展示当地时间,以及如何对时间进行统一有效存储,是本次学习解决的问题。

常用方案

首先是询问GPT得到的一些常用解决方案。

1. 统一时间标准 

  • 使用 UTC 时间
    • 在系统内部存储和传递时间时统一使用 UTC(Coordinated Universal Time)。
    • 在显示或输出给用户时,根据用户所在的时区将 UTC 转换为本地时间。

2. 用户时区管理

  • 用户设置时区
    • 允许用户在个人设置中指定其所在的时区。
    • 后端根据用户时区动态转换时间,确保显示正确。
  • 自动时区检测
    • 使用前端脚本(如 JavaScript 的 Intl.DateTimeFormat().resolvedOptions().timeZone)自动检测用户时区。
    • 后端根据检测到的时区调整时间。

动态时区调整方案

问题明确

首先,明确动态时区调整方案需要解决的问题。

1、前端操作的日期数据以String类型传递到后端时,由于用户来自多个国家日期所在的时区是多变的,后端需要一种手段来实现对日期的统一存储根据时区动态展示

2、SimpleDateFormat实例关于时间的序列化和反序列化方法parse与format是线程不安全的。

接下来让我们先解释下SimpleDateFormat的序列化方法:

parse():将字符串类型的时间,根据指定格式,解析转换成Date

format():将Date类型的时间,根据格式,转成对应格式的字符串

SimpleDateFormat序列化方法中的问题

线程安全问题

SimpleDateFormat类在其和parse方法中都会使用共享的 字段,如CalendarTimeZone,而这些字段在方法调用过程中会被修改。例如SimpleDateFormat内部使用了Calendar来解析和格式化日期。这个Calendar对象并不是线程本地的,每个线程在调用parseformat时都会共享同一个Calendar实例。

  • parse方法中,SimpleDateFormat通过Calendar处理日期字符串,它会修改Calendar实例的内部状态。

  • format方法中,SimpleDateFormat使用Calendar来格式化日期,也会修改Calendar实例的状态。

由于 Calendar 是共享的,如果多个线程同时调用 SimpleDateFormatparseformat 方法,线程间的竞争可能会导致数据错误或不一致的结果。

时区问题

先上结论:SimpleDateFormatparse()会自动地将时间转换为JVM默认时区的时间。

首先我们知道,Date类型的对象本身是无时区概念的,Date代表的就是一个时间戳的绝对值,这个变量的值为自1997-01-01 00:00:00(GMT)至Date对象记录时刻所经过的毫秒数。

但是,当Date涉及到一些格式化或者存储操作时,会被默认转化成JVM默认时区的日期,这个过程是有时区概念的。也就是说对于SimpleDateFormat的parse()和format()方法,内部默认的逻辑是:

根据SimpleDateFormat实例当中指定的时区,将字符串类型的时间解析为Date,或者将Date类型的时间转换成字符串类型。如果实例没有指定时区,默认为JVM所在时区。但是由于Date在格式化操作时会被JVM时区影响,所以此时如果打印或者存储Date,看到的时间都是基于JVM所在时区

动态时区调整的实现

 1、定义一个时区接口,用于获取当前请求中的时区。

规定日期格式、请求头当中时区对应的字段常量。定义一个Concurrenthashmap,用于缓存请求中用过的时区信息。提供一个获取请求当中时区的方法。

String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
String TIME_ZONE_HEADER = "X-Req-Zone-Id";
/**
* TimeZone::getTimeZone有锁
* 缓存TimeZone
*/
ConcurrentHashMap<String, TimeZone> TIME_ZONE_CACHE = new ConcurrentHashMap<>(32);
default TimeZone currentTimeZone() {
        TimeZone curZone = TimeZone.getDefault();
        //从当前请求中获取TimeZone
        RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
        ...
        //使用Concurrenthashmap缓存
        ...
        return curZone;
    }

2、实现该接口,同时继承SimpleDateFormat类(假设为A)。

在实现类中,首先实现一个类的单例,用于提升频繁处理日期时的性能。

定义一个存放SimpleDateFormat实例的Threadlocal变量,保证每个请求线程拥有自己独立的SimpleDateFormat实例,解决了之前提到的线程安全问题。

public static final A INSTANCE = new RequestTimeZoneDateFormat();
static final ThreadLocal<SimpleDateFormat> CACHE = ThreadLocal.withInitial(() -> new SimpleDateFormat(xxx));

3、重写format和parse方法

获取请求中的时区后,赋值给SimpleDateFormat实例,再调用SimpleDateFormat的format和parse。

    @Override
    public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition pos) {
        SimpleDateFormat simpleDateFormat = currentSimpleDateFormat();
        return simpleDateFormat.format(date, toAppendTo, pos);
    }

    @Override
    public Date parse(String source) throws ParseException {
        SimpleDateFormat simpleDateFormat = currentSimpleDateFormat();
        return simpleDateFormat.parse(source);
    }

    private SimpleDateFormat currentSimpleDateFormat() {
        SimpleDateFormat simpleDateFormat = CACHE.get();
        TimeZone curZone = this.currentTimeZone();
        simpleDateFormat.setTimeZone(curZone);
        return simpleDateFormat;
    }

总结:

  • 每次请求来临时,通过 ThreadLocal 获取独立的 SimpleDateFormat 实例。

  • 调用 parseformat 方法时,获取当前请求的时区。

  • 根据当前请求的时区设置 SimpleDateFormat 实例的时区。

  • 最后调用父类的 parseformat 方法进行实际的日期解析或格式化操作。

因此,实现了根据接口请求中的时区,对日期进行动态处理的操作。

参考文章

java中的Date和时区_java date 时区-优快云博客

SimpleDateFormat.parse()方法中的时区设置缺陷-优快云博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值