2025.6.20
时间设置的是23点59分59秒,数据库却存的是第二天00:00:00
2025-06-18 3,387 阅读6分钟
专栏:
java问题集
关注
.markdown-body{word-break:break-word;line-height:1.75;font-weight:400;font-size:16px;overflow-x:hidden;color:#252933}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4,.markdown-body h5,.markdown-body h6{line-height:1.5;margin-top:35px;margin-bottom:10px;padding-bottom:5px}.markdown-body h1{font-size:24px;line-height:38px;margin-bottom:5px}.markdown-body h2{font-size:22px;line-height:34px;padding-bottom:12px;border-bottom:1px solid #ececec}.markdown-body h3{font-size:20px;line-height:28px}.markdown-body h4{font-size:18px;line-height:26px}.markdown-body h5{font-size:17px;line-height:24px}.markdown-body h6{font-size:16px;line-height:24px}.markdown-body p{line-height:inherit;margin-top:22px;margin-bottom:22px}.markdown-body img{max-width:100%}.markdown-body hr{border:none;border-top:1px solid #ddd;margin-top:32px;margin-bottom:32px}.markdown-body code{word-break:break-word;border-radius:2px;overflow-x:auto;background-color:#fff5f5;color:#ff502c;font-size:.87em;padding:.065em .4em}.markdown-body code,.markdown-body pre{font-family:Menlo,Monaco,Consolas,Courier New,monospace}.markdown-body pre{overflow:auto;position:relative;line-height:1.75}.markdown-body pre>code{font-size:12px;padding:15px 12px;margin:0;word-break:normal;display:block;overflow-x:auto;color:#333;background:#f8f8f8}.markdown-body a{text-decoration:none;color:#0269c8;border-bottom:1px solid #d1e9ff}.markdown-body a:active,.markdown-body a:hover{color:#275b8c}.markdown-body table{display:inline-block!important;font-size:12px;width:auto;max-width:100%;overflow:auto;border:1px solid #f6f6f6}.markdown-body thead{background:#f6f6f6;color:#000;text-align:left}.markdown-body tr:nth-child(2n){background-color:#fcfcfc}.markdown-body td,.markdown-body th{padding:12px 7px;line-height:24px}.markdown-body td{min-width:120px}.markdown-body blockquote{color:#666;padding:1px 23px;margin:22px 0;border-left:4px solid #cbcbcb;background-color:#f8f8f8}.markdown-body blockquote:after{display:block;content:""}.markdown-body blockquote>p{margin:10px 0}.markdown-body ol,.markdown-body ul{padding-left:28px}.markdown-body ol li,.markdown-body ul li{margin-bottom:0;list-style:inherit}.markdown-body ol li .task-list-item,.markdown-body ul li .task-list-item{list-style:none}.markdown-body ol li .task-list-item ol,.markdown-body ol li .task-list-item ul,.markdown-body ul li .task-list-item ol,.markdown-body ul li .task-list-item ul{margin-top:0}.markdown-body ol ol,.markdown-body ol ul,.markdown-body ul ol,.markdown-body ul ul{margin-top:3px}.markdown-body ol li{padding-left:6px}.markdown-body .contains-task-list{padding-left:0}.markdown-body .task-list-item{list-style:none}@media (max-width:720px){.markdown-body h1{font-size:24px}.markdown-body h2{font-size:20px}.markdown-body h3{font-size:18px}}.markdown-body pre,.markdown-body pre>code.hljs{color:#333;background:#f8f8f8}.hljs-comment,.hljs-quote{color:#998;font-style:italic}.hljs-keyword,.hljs-selector-tag,.hljs-subst{color:#333;font-weight:700}.hljs-literal,.hljs-number,.hljs-tag .hljs-attr,.hljs-template-variable,.hljs-variable{color:teal}.hljs-doctag,.hljs-string{color:#d14}.hljs-section,.hljs-selector-id,.hljs-title{color:#900;font-weight:700}.hljs-subst{font-weight:400}.hljs-class .hljs-title,.hljs-type{color:#458;font-weight:700}.hljs-attribute,.hljs-name,.hljs-tag{color:navy;font-weight:400}.hljs-link,.hljs-regexp{color:#009926}.hljs-bullet,.hljs-symbol{color:#990073}.hljs-built_in,.hljs-builtin-name{color:#0086b3}.hljs-meta{color:#999;font-weight:700}.hljs-deletion{background:#fdd}.hljs-addition{background:#dfd}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}
问题描述
昨天下班的时候,运营反馈了一个问题,明明设置的是两天后解封,为什么提示却是三天后呢。
比如今天(6.16)被拉入黑名单了,用户报名会提示 “6.19号恢复报名”,但是现在却提示6月20号才能报名,经过排查发现,就是解封的时间被多加了 1s 中,本来应该是存2025-06-18 23:59:59
,但是数据库却是2025-06-19 00:00:00
。
看了数据库有接近一半的数据是正确的,有一半的数据是第二天0晨(百思不得其解啊🤣)
代码逻辑实现:
LocalDateTime currentTime = LocalDateTime.now(); LocalDateTime futureTime = currentTime.plus(2, ChronoUnit.DAYS); // 设置了为当天的最后一秒啊 LocalDateTime resultTime = futureTime.withHour(23).withMinute(59).withSecond(59); BlackAccount entity = new BlackAccount(); // 实体字段类型为Date,数据库是timestamp entity.setDeblockTime(Date.from(resultTime.atZone(ZoneId.systemDefault()).toInstant())); blackAccountService.save(entity);
❓看到上面的代码,有没有大佬已经发现问题了。确实上面的代码存在问题
解决过程
排查思路
一、排除代码问题
1.确认了项目中所有设置DeblockTime
的地方 只有1出。排除了代码逻辑问题,被其他代码覆盖问题;
二、问问AI:
LocalDateTime futureTime = currentTime.plus(oaConfigActivityBlacklist.getBlockDay(), ChronoUnit.DAYS); LocalDateTime resultTime = futureTime.withHour(23).withMinute(59).withSecond(59); Date date = Date.from(resultTime.atZone(ZoneId.systemDefault()).toInstant());
这种写法能保证最后的date 的时分秒为 23时59分59秒么?
ai的答案如下 :
1. 夏令时(DST)影响
-
场景:若时区在目标日期存在夏令时调整(如时钟回拨或前拨),可能导致时间偏差。
-
示例:
- 假设系统时区为
Europe/Paris
,2023 年 10 月 29 日凌晨 3 点时钟回拨到 2 点。 - 若
futureTime
是该日,设置23:59:59
后,转换为 UTC 时可能变为21:59:59Z
或22:59:59Z
,取决于时钟调整规则。
- 假设系统时区为
2. 时区转换问题
-
场景:若
currentTime
是其他时区的时间(如 UTC),直接转换可能导致时间偏移。 -
示例:
- 若
currentTime
是 UTC 时间2023-10-01T16:00:00Z
,系统时区为Asia/Shanghai
(UTC+8)。 - 添加 1 天后得到
2023-10-02T16:00:00Z
,设置为23:59:59
后转换为Date
,实际对应 UTC 时间2023-10-02T15:59:59Z
。
- 若
看了数据的时间分布,任何时间点 都存在
23:59:59
和00:00:00
的情况,就算创建时间相差几分钟的情况下,都存在这两种数据。
三、批量插入数据测试
看看能不能复现这个问题,会不会插入时候精度等其他问题:
for (int i = 0; i < 100; i++) { Thread.sleep(100); LocalDateTime currentTime = LocalDateTime.now(); LocalDateTime futureTime = currentTime.plus(2, ChronoUnit.DAYS); // 设置了为当天的最后一秒啊 LocalDateTime resultTime = futureTime.withHour(23).withMinute(59).withSecond(59); BlackAccount entity = new BlackAccount(); // 实体字段类型为Date,数据库是timestamp entity.setDeblockTime(Date.from(resultTime.atZone(ZoneId.systemDefault()).toInstant())); blackAccountService.save(entity); }
果然还真复现了,有一半的数据是2025-06-19 23:59:59
有一半的数据是2025-06-20 00:00:00
定位问题
通过demo的复现,可以确认是在存数据库的时候出了问题。 因为Date的精度是控制在毫秒,pgsql 中TimeStamp 的精度用的默认值,精确到秒,所以在插入的时候Date的毫秒部分大于等于500的时候就会加1秒处理。入库之后就变成了第二天的00:00:00
呢
解决方案
要么将java对象的时间精度和 数据库的精度保持一致,要么就将java对象多余的精度置为0,解决方案如下:
- 方案1:代码中清空秒后面的数据
修改前:futureTime.withHour(23).withMinute(59).withSecond(59);
修改后:futureTime.withHour(23).withMinute(59).withSecond(59).withNano(0);
- 方案2:调整数据库TimeStamp精度不小于java(date)对象的精度
修改前:
修改后:
知识扩展
1. Date 和 LocalDateTime
特性 | java.util.Date (Java 1.0) | java.time.LocalDateTime (Java 8+) |
---|---|---|
精度 | 毫秒级(1/1000 秒) | 纳秒级(1/1,000,000,000 秒) |
包路径 | java.util.Date | java.time.LocalDateTime |
可变性 | 可变(修改会影响原对象) | 不可变(所有操作返回新对象) |
时区感知 | 不存储时区,但内部时间戳基于 UTC | 无时区,仅表示本地日期和时间 |
2. mysql 中的timestamp 和 datetime
特性 | DATETIME | TIMESTAMP |
---|---|---|
存储范围 | 1000-01-01 00:00:00 到 9999-12-31 23:59:59 | 1970-01-01 00:00:01 UTC 到 2038-01-19 03:14:07 UTC |
精度 | 5.6.4 版本后支持 fractional seconds(如DATETIME(6))最高精度微妙,设置0的话就表示精确到秒 | 同上(如TIMESTAMP(6)) |
存储空间 | 8 字节 | 4 字节(时间戳范围小) |
时区感知 | 不存储时区信息,直接存储字面量 | 自动转换时区:存储时转换为 UTC,读取时转换为会话时区 |
默认值 | 无默认值(除非显式设置DEFAULT) | 支持DEFAULT CURRENT_TIMESTAMP和ON UPDATE CURRENT_TIMESTAMP |
自动更新 | 不支持 | 支持自动更新为当前时间(ON UPDATE) |
3.适用场景建议
- java 中尽量用
LocalDateTime
吧,毕竟LocalDateTime
主要就是用来取代Date
对象的,区别如下
场景类型 | java.util.Date(旧 API) | java.time.LocalDateTime(新 API) |
---|---|---|
简单本地时间记录 | 可使用,但 API 繁琐(需配合Calendar) | 推荐使用(无需时区,代码简洁) |
带时区的时间处理 | 不推荐(时区处理易混淆) | 推荐使用ZonedDateTime或OffsetDateTime |
多线程环境 | 不推荐(非线程安全) | 推荐(不可变设计,线程安全) |
数据库交互(JDBC 4.2+) | 需转换为java.sql.Timestamp | 直接支持(如pstmt.setObject(1, localDateTime)) |
时间计算与格式化 | 需依赖SimpleDateFormat(非线程安全) | 推荐(DateTimeFormatter线程安全) |
高精度需求(纳秒级) | 仅支持毫秒级 | 支持纳秒级(1/1,000,000,000 秒 |
- 数据库到底是用
timestamp
还是datetime
呢,跨国业务用timestamp 其他场景建议用datetime:
场景 | 推荐类型 | 原因 |
---|---|---|
存储历史事件时间(如订单创建时间) | DATETIME | 不依赖时区,固定记录用户输入的时间 |
记录服务器本地时间(如定时任务执行时间) | DATETIME | 无需时区转换,直接反映服务器时间 |
多时区应用(如跨国业务) | TIMESTAMP | 自动处理时区转换,确保数据一致性(如登录时间) |
需要自动更新时间戳 | TIMESTAMP | 支持ON UPDATE CURRENT_TIMESTAMP特性 |
存储范围超过 2038 年 | DATETIME | TIMESTAMP仅支持到 【2038】 年 |
微秒级精度需求 | DATETIME(6)或TIMESTAMP(6) | 根据是否需要时区转换选择 |
总结
本文主要讲述了在处理用户解封时间时,因 Java 代码中时间精度与数据库TIMESTAMP
类型精度不一致,导致约一半数据存储时间比预期多 1 秒的问题。通过排查与测试,定位问题并给出了 Java 对象时间精度和调整数据库精度两种解决方案,同时对比了 Java 和数据库中多种时间类型的特性及适用场景 。