blog

タイムゾーン問題のウェブ開発

タイムゾーンの問題は、国際化されたビジネスシナリオでは一般的です。この記事では、Web開発におけるタイムゾーンの問題を探ります。 タイムゾーンの概念については、誰もがある程度理解していると思います。地...

Oct 30, 2020 · 7 min. read
シェア

タイムゾーンの問題は、国際化されたビジネスシーンではよくあることです。本稿では、ウェブ開発におけるタイムゾーンの問題を探ります。

皆さんはタイムゾーンという概念をご存知だと思います。地球は24のタイムゾーンに分かれており、東京時間は8番目の東部ゾーン、アメリカの太平洋時間は8番目の西部ゾーンで、その差は16時間です。

例えば、サーバーとデータベースが東京にあり、アメリカのユーザーが東京時間で2020年7月1日8:00から7月1日18:00までの10時間分のデータをブラウザから照会したいとします。

ブラウザが太平洋時間であることをシミュレートするには、システム時間を太平洋時間に設定するだけです。そして、システム時間の変更は、JVMのデフォルトタイムゾーンに影響を与えるので、サーバープログラムを東京時間のままにするには、次のようにコードでタイムゾーンを指定する必要があります:

TimeZone.setDefault(TimeZone.getTimeZone("GMT+8"));

そして、データベースMySQLのタイムゾーンも東京時間に設定され、SQLは次のとおりです。

set global time_zone = '+8:00';
set time_zone = '+8:00';
flush privileges; 

以下、クエリをクリックして、最初に送信されたものを見てみましょう:

開始時刻と終了時刻の両方が、インターフェイスに表示されている時刻よりも8時間長いことがわかります。これは、ElementUIコンポーネントの日付時刻セレクタを使用しているためで、デフォルトのタイムゾーンは0なので、ブラウザのタイムゾーンに基づいて、選択された時刻をタイムゾーン0に変換します。最終的に送信されるのは、時刻+タイムゾーンの文字列表現です。

正常に送信されたデータへのフロントエンドは、バックエンドでデータを受信するために、次のようになります。バックエンドはSpringBootを使用し、コントローラのコードは次のとおりです。

@PostMapping("/time")
public List<Data> test(@RequestBody TimeDto dto) {
 Date startTime = dto.getStartTime();
 Date endTime = dto.getEndTime();
 System.out.println(startTime);
 System.out.println(endTime);
 // グリーンタイム
 String format = "yyyy-MM-dd HH:mm:ss";
 SimpleDateFormat sdfGreen = new SimpleDateFormat(format);
 sdfGreen.setTimeZone(TimeZone.getTimeZone("GMT+0"));
 System.out.println("GrimmTime:" + sdfGreen.format(startTime) + "  + sdfGreen.format(endTime));
 // 東京時間 (+8)
 SimpleDateFormat sdfTokyo = new SimpleDateFormat(format);
 sdfTokyo.setTimeZone(TimeZone.getTimeZone("GMT+8"));
 System.out.println("東京時間:" + sdfTokyo.format(startTime) + "  + sdfTokyo.format(endTime));
 // 太平洋時間
 SimpleDateFormat sdfPacific = new SimpleDateFormat(format);
 sdfPacific.setTimeZone(TimeZone.getTimeZone("GMT-8"));
 System.out.println("太平洋時間:" + sdfPacific.format(startTime) + "  + sdfPacific.format(endTime));
 List<Data> dataList = queryDate(dto);
 return dataList;
}
/**
Thu Jul 02 00:00:00 GMT+08:00 2020
Thu Jul 02 10:00:00 GMT+08:00 2020
グリーンタイム:2020-07-01 16:00:002020-07-02 02へ:00:00
東京時間:2020-07-02 00:00:002020-07-02 10へ:00:00
太平洋時間:2020-07-01 08:00:002020-07-01 18へ:00:00
**/

JVMのタイムゾーンは東8なので、デシリアライズ中に得られるDateオブジェクトも東8時間、つまり2日の0:00-2:10です。startTimeとendTimeを直接使用してクエリを実行すると、2日の東京時間0:00から10:00までのデータを取得することになります。

では、1日8:00から1日18:00までの東京時間のデータを照会するにはどうすればよいでしょうか。フロントエンドから送信された太平洋時刻はタイムゾーン変換されてバックエンドで受信されるので、フロントエンドで照会したい東京時刻を直接送信することが可能です。つまり、1日の8:00 - 1日の18:00です。el-date-pickerのvalue-format属性で、選択された時刻のフォーマットを "yyyy-MM-dd HH:mm:ss "と指定することで、送信される文字列にはタイムゾーン属性が付きません。

<el-date-picker
 v-model="dateTimeRange"
 type="datetimerange"
 range-separator=" 
 start-placeholder="開始日"
 end-placeholder="終了日"
 value-format="yyyy-MM-dd HH:mm:ss"
 >
</el-date-picker>

そして、このフォーマットの時刻をDateオブジェクトに変換するように修正されていない場合、バックエンドは以下のエラーを報告します。

JSON parse error: Cannot deserialize value of type `java.util.Date` from String "2020-07-01 08:00:00": not a valid representation (error: Failed to parse Date value '2020-07-01 08:00:00': Cannot parse date "2020-07-01 08:00:00": while it seems to fit format 'yyyy-MM-dd'T'HH:mm:ss.SSSZ', parsing fails (leniency? null)); nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `java.util.Date` from String "2020-07-01 08:00:00": not a valid representation (error: Failed to parse Date value '2020-07-01 08:00:00': Cannot parse date "2020-07-01 08:00:00": while it seems to fit format 'yyyy-MM-dd'T'HH:mm:ss.SSSZ', parsing fails (leniency? null))↵ at [Source: (PushbackInputStream); line: 1, column: 14] (through reference chain: com.chaycao.timezone.TimeDto["startTime"])

そのため、正しくデシリアライズするためには、jackjsonがデシリアライズするための追加情報を提供する必要があります。JsonFormatアノテーションを追加して、タイムゾーンと時間フォーマットを指定します。つまり、フロントエンドとバックエンドの転送で発生するタイムゾーンの問題は、時刻データのシリアライズとデシリアライズの方法に注意することで解決できます。

public class TimeDto {
 @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
 Date startTime;
 @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
 Date endTime;
 //...
}

データベースで発生する可能性のあるタイムゾーンの問題について、もう1つ見てみましょう。

MySQL のタイムゾーンを太平洋時間に変更します。

set global time_zone = '-8:00';
set time_zone = '-8:00';
flush privileges; 

クエリーの結果が変わるかどうかを確認するために、クエリーの手順は次のようになります:

private List<Data> queryDate(TimeDto dto) {
 DriverManagerDataSource dataSource = new DriverManagerDataSource();
 dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
 dataSource.setUrl("jdbc:mysql://localhost/test?useSSL=false&useUnicode=true&characterEncoding=UTF8&allowPublicKeyRetrieval=true&serverTimezone=Asia/Tokyo");
 dataSource.setUsername("root");
 dataSource.setPassword("caoniezi");
 Date startTime = dto.getStartTime();
 Date endTime = dto.getEndTime();
 JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
 String sql = "SELECT * FROM data WHERE create_time >= ? and create_time <= ?";
 List<Map<String, Object>> maps = jdbcTemplate.queryForList(
 sql,
 new Object[]{startTime, endTime});
 List<Data> dataList = new ArrayList<>();
 for (Map<String, Object> map : maps) {
 Data data = new Data();
 data.setId((Integer) map.get("id"));
 data.setContent((String) map.get("content"));
 data.setCreateTime((Date) map.get("create_time"));
 dataList.add(data);
 }
 return dataList;
}

クエリの結果は "D, E, F "のままなので、データベースのタイムゾーンの変更はこのクエリには影響しないようです。

これは、create_timeフィールドがdatetime型であり、タイムゾーンの概念を持たず、タイムゾーンの影響を受けないYYYYMMDDHHMMSS形式の整数を格納するためです。

タイムゾーンを東8に戻し、create_timeのタイプをtimestampに変更し、タイムゾーンを西8に変更すると、クエリー結果は "H, I, J "となります。クエリの結果は "H, I, J "です。

set global time_zone = '+8:00';
set time_zone = '+8:00';
flush privileges; 
ALTER TABLE `data` MODIFY COLUMN `create_time` TIMESTAMP DEFAULT NULL;
set global time_zone = '-8:00';
set time_zone = '-8:00';
flush privileges; 

これは、timestampがタイムゾーンの概念であり、時刻のエポックからの秒数を格納するためです。 タイプをtimestampに変更すると、create_timeの値も東8ゾーンから計算され、0タイムゾーンの時刻秒数が格納されます。西8で照会されると、16時間短縮されます。

では、WEST8のデータベースから欲しいデータを見つけるにはどうすればいいのでしょうか。

jdbc接続URLのserverTimezoneパラメータは、ドライバのMySQLのタイムゾーンを指定するためのもので、この操作で、MySQLのタイムゾーンは変更されましたが、serverTimezoneは変更されず、東8のままです。

jdbc:mysql://localhost/test?useSSL=false&useUnicode=true&characterEncoding=UTF8&allowPublicKeyRetrieval=true&serverTimezone=Asia/Tokyo

MySQLドライバは、指定されたserverTimezoneとJVM timezoneに基づいて変換を行います。 どちらもEast 8であるため、startTimeとendTimeの時間文字列は変更されませんが、MySQLのタイムゾーンがWest 8に変更されたため、クエリ結果はH、I、Jに該当します。

serverTimezone が指定されていない場合、ドライバは MySQL のタイムゾーンを serverTimezone として使用します。

しかし、これには問題があります。つまり、datetime型のデータをクエリするときにも変換が起こり、クエリの結果は30日の16:00から1日の2:00までのデータになってしまいます。では、どうすればdatetime型、timestamp型のデータが正しいことを確認できるのでしょうか。まず、serverTimezoneにAsia/Tokyoを指定する必要があります。そして、serverTimezoneとMySQLのtimezoneの不一致により、クエリのtimestampeデータにタイムゾーンの問題が発生するため、最終的な解決策は、MySQLのtimezoneを東8に修正することです。MySQLのタイムゾーン、serverTimezoneとJVMのタイムゾーンの一貫性を確保することで、時間データの読み取りと書き込みの正確性を保証します。

記事のコードはGithubにアップロードされているので、興味のある学生は自分で試すことができます:

Read next

高度に同時的な10億トラフィックシナリオで分散フロー制限を実現するにはどうすればいいのか?これを読んで、私は完全に理解した!

前に書く\nセオリー:「高同時性 10億トラフィック下で分散フロー制限を実現するには?これらの理論は必ずマスターしてください!\nアルゴリズム:「高同時性 10億レベルのトラフィックで分散フロー制限を実現するには?あなたがマスターしなければならないこれらのアルゴリズム!\nプロジェクトのソースコードはgithubに公開されています: github

Oct 30, 2020 · 10 min read