2023年3月

这是一份机票预订系统的软件测试报告,老师评分98分,希望能帮助各位园友。

image-20230329115842585

一、 测试任务、目标

本次课程设计将对航班订票系统进行系统测试,验证系统是否满足登录注册以及订票退票等功能要求,同时测试系统的性能是否达标。

二、 进行测试工作量估计

根据机票预定系统的需求规格说明书,可以看出这是一个规模比较小的系统,可以采用手工测试和自动化测试相结合的方式进行测试。可以采用场景法、成效价类划分法、边界类法和常见错误法来编写测试用例。本系统有8个功能点要测试,比较复杂的功能点是新增订票、查询订票和修改订票三个功能点,每个功能点大约需求10个测试用例,其它均为2-5个测试用例,初步估计有50个测试用例,约有3人/天的工作量,执行测试则有6人/天的工作量。

三、 人员资源和资源分配

人员资源分配:

小组成员每人各负责一部分进行开发测试:

测试员(一人)

软件工程需求分析(一人)

系统设计员(一人)

项目经理(一人)

架构师(一人)

资源分配:

初级设计不需要用到过多资金,只需保证人手一台电脑设备,以供成员使用即可。

四、明确任务的时间和进度安排

机票预订系统设计周期预计为一个月,一个月内完成模型的初步设计。其中具体时间安排如下:

img

其中设计进度为期一个月,其中为确保工期按时完成,每个星期都会有一次阶段性验收。

五、风险估计和应急计划;

风险估计:

1.系统可侧性差

2.测试环境不符合测试要求

3.测试人手不够,时间不足

4.测试所需硬件、软件需求不满足

应急计划:

1.加强测试环境的管理和改进

2.在项目前期加强对人员的管理和培训,测试人员要尽早熟悉产品,做好人员分配工作。

3.测试前做好测试所需硬件配置,软件安装更新等工作。

六、测试失败/通过的标准

在功能测试中,系统中各功能能正常执行;在性能测试中,各类测试指标包括测试中应该达到的某些性能指标,这些性能指标均是来自应用系统设计开发时遵循的业务需求,当某个测试的某一类指标已经超出了业务需求的要求范围,则测试已经达到目的,即可终止压力测试。

应用软件级别的测试指标:

测试用例执行通过率,通过的测试用例个数/测例总数。该值>=95,

(1) 事务的执行情况

事务的平均响应时间(期望值:<15s)

事务的最大响应时间(期望值:<30s)

平均每秒处理数量(分别记录单位时间内成功、失败和停止的数量)

不同并发用户数的状况下的上述记录值

(2)测试结果分析情况

测试指标:

吞吐量:单位时间内网络传输数据量

七、测试范围

1.功能测试

​ 注册和登录用户信息

​ 订票办理

​ 退票办理

​ 查询购票信息

  1. 性能测试

​ 本次测试是针对系统的性能特征和系统的性能调优而进行的,主要需要获得如下的性能测试指标。

​ 1、系统的响应能力:即在各种负载压力情况下,系统的响应时间,也就是从客户端交易发起,到服务器端交易应答返回所需要的时间,包括网络传输时间和服务器处理时间。

​ 2、应用系统的吞吐率:即应用系统在单位时间内完成的交易量,也就是在单位时间内,应用系统针对不同的负载压力,所能完成的交易数量。

​ 3、应用系统的负载能力:即系统所能容忍的最大用户数量,也就是在正常的响应时间中,系统能够支持的最多的客户端的数量。

​ 3.UI界面测试

​ 1、导航、链接、 Cookie 、页面结构包括菜单、背景、颜色、字体、按钮名称、 TITLE 、提示信息的一致性等。

​ 2、友好性、可操作性(易用性)

​ 4.安全性测试

​ 1、程序安全性

​ 2、网络安全性

  1. 数据库测试

​ ① 系统数据机密性

② 系统数据的完整性;

③ 系统数据可管理性;

④ 系统数据的独立性;

⑤ 系统数据可备份和恢复能力

八、测试策略

1.单元测试

人工静态检查:

主要是保证代码逻辑的正确性。包括检查机票预定系统中算法的逻辑正确性、模块接口的正确性、检查输入参数有没有作正确性检查、调用其他方接口的正确性、异常错误处理、保证表达式、SQL语句的正确性、检查常量或全局变量使用的正确性、程序风格的一致性、规范性、检查代码注释是否完整。

动态执行跟踪:

检查实际的运行结果和预期结果是否一致。保证机票预定系统的代码覆盖率,尽量做到代码的全覆盖。常见单元测试覆盖标准:语句覆盖、分支覆盖、条件覆盖、分支-条件覆盖、条件组合覆盖、路径覆盖。

2.集成测试

机票预定系统经过单元测试的模块一个接一个地组合,然后测试组合单元的功能。通常,集成测试是在单元测试之后进行的。一旦创建并测试了所有单个单元,我们便开始组合那些经过测试的模块并开始执行集成测试。这里的主要目标是测试单元/模块之间的接口。以下是一些进行集成测试的简单步骤:准备测试整合计划、确定集成测试方法的类型、相应地设计测试用例,测试场景和测试脚本、一起部署所选模块并运行集成测试、跟踪缺陷并记录测试结果、重复上述步骤,直到测试完成整个系统。

3.系统测试

机票预定系统的系统测试是针对整个产品系统进行的测试,目的是验证系统是否满足了需求规格的定义,找出与需求规格不符或与之矛盾的地方,从而提出更加完善的方案。系统测试发现问题之后要经过调试找出错误原因和位置,然后进行改正,是基于系统整体需求说明书的黑盒类测试,应覆盖系统所有联合的部件。对象不仅仅包括需测试的软件,还要包含软件所依赖的硬件、外设甚至包括某些数据、某些支持软件及其接口等。

img

4.验收测试

在此阶段下,机票预定系统已通过三个测试级别(单元测试,集成测试,系统测试)但仍可能有一些小错误,当最终用户在实际场景中使用系统时,可以识别这些错误。验收测试是对先前完成的所有测试过程的挤压。需求分析:测试团队分析需求文档以找出所开发软件的目标。通过使用需求文档,流程图,系统需求规范,业务用例,业务需求文档和项目章程完成测试计划。测试计划创建:概述测试过程的整个策略。测试用例设计:能够涵盖大多数验收测试场景。测试用例执行:包括使用适当的输入值执行测试用例。测试团队从最终用户收集输入值,然后测试用例和最终用户执行所有测试用例,以确保软件在实际场景中正常工作。确认目标:成功完成所有测试过程后,测试团队确认软件应用程序没有错误,可以将其交付给客户端。

九、测试用例的设计与规划

所属产品 所属模块 相关需求 用例标题 前置条件 步骤 预期 用例类型 用例状态
机票预定系统 登录 正确登录 登录 使用已经注册的账号和密码 登录成功 功能测试 正常
机票预定系统 登录 账号不存在,无法登录 无法登录1 没有注册的用户名进行登录 登录失败,给出提示信息 功能测试 正常
机票预定系统 登录 密码不正确,登录失败 无法登录2 输入错误的密码进行登录 登录失败,给出提示信息 功能测试 正常
机票预定系统 查询机票 输入不存在的地点进行查询 查询航班1 已经登录 输入不存在的地点,点击查询 查询失败,给出提示信息 功能测试 正常
机票预定系统 查询机票 输入正确的地点进行查询 查询航班2 已经登录 输入正确的地点,点击查询 查询成功,显示航班信息 功能测试 正常
机票预定系统 订票 选择已经售完的机票,给出提示信息 订票失败1 已经登录,查询到对应的机票 点击已经售完的机票 无法订票,给出提示信息 功能测试 正常
机票预定系统 订票 选择有余票的机票,没有选择支付方式 订票失败2 已经登录,查询到对应的机票信息 点击未售完的机票,点击支付按钮 提示选择支付方式 功能测试 正常
机票预定系统 订票 选择有余票的机票,选择支付方式,但是余额不足 订票失败3 已经登录,查询到对应的机票信息 点击未售完的机票,选择支付方式,点击支付按钮 支付失败,给出提示信息 功能测试 正常
机票预定系统 订票 选择有余票的机票,选择支付方式,余额充足 订票成功 已经登录,查询到对应的机票信息 点击未售完的机票,选择支付方式,点击支付按钮 支付成功 功能测试 正常
机票预定系统 退票 退票时间在航班规定退票时间点之后,退票失败 退票失败 已经登录,购买相应的机票 选择购买的机票,点击退票按钮 退票失败,给出提示信息 功能测试 正常
机票预定系统 退票 退票时间在航班规定退票时间点之前,退票成功 退票成功 已经登录,购买相应的机票 选择购买的机票,点击退票按钮 退票成功,机票的金额返还给用户 功能测试 正常
机票预定系统 用户充值 用户往系统充值一定数量的金额,没有选择支付方式 充值失败1 已经登录 进入个人中心,点击充值按钮,直接点击支付 充值失败,给出提示信息 功能测试 正常
机票预定系统 用户充值 用户往系统充值一定数量的金额,选择支付方式,但是金额不足 充值失败2 已经登录 进入个人中心,点击充值按钮,选择支付方式,点击支付 充值失败,给出提示信息 功能测试 正常
机票预定系统 用户充值 用户往系统充值一定数量的金额,选择支付方式,金额充足 充值成功 已经登录 进入个人中心,点击充值按钮,选择支付方式,点击支付 充值成功 功能测试 正常
机票预定系统 用户投诉 用户点击投诉按钮,投诉理由不合理 投诉失败 已经登录 点击投诉按钮,填写投诉理由,点击提交按钮 投诉失败 功能测试 正常
机票预定系统 用户投诉 用户点击投诉按钮,投诉理由合理 投诉成功 已经登录 点击投诉按钮,填写投诉理由,点击提交按钮 投诉成功 功能测试 正常
机票预定系统 机票信息 从航空公司的数据库获取机票信息,显示在系统里 显示机票信息 1. 创建GET请求 2. 设置请求Param 3. 设置断言 4. 运行请求 显示成功 接口测试 正常
机票预定系统 购买的机票信息 从数据库获取用户已经购买的机票,显示在系统里 显示购买机票信息 已经登录和购买机票 1. 创建GET请求 2. 设置请求Param 3. 设置断言 4. 运行请求 显示成功 接口测试 正常

十、测试环境和测试条件的规划

操作系统:window10

数据库:mySQL8.0

服务器:tomcat1.0

CPU:8核

内存:16G

硬盘:1T

网络:百兆宽带

测试前置条件:将网站进行本地搭建,数据完整,功能正常

十一、 测试工具的设计和选择

Jmeter:不依赖于界面,如果服务正常启动,传递参数明确就可以添加测试用例,执行测试。

测试脚本不需要编程,熟悉http请求,熟悉业务流程,就可以根据页面中input对象来编写测试用例。

测试脚本维护方便,可以将测试脚本复制,并且可以将某一部分单独保存。

可以跳过页面限制,向后台程序添加非法数据,这样可以测试后台程序的健壮性。

利用badboy录制测试脚本,可以快速的形成测试脚本。

Jmeter断言可以验证代码中是否有需要得到的值。

使用参数化以及Jmeter提供的函数功能,可以快速完成测试数据的添加修改等。

selenium:Selenium是一个用于Web应用程序自动化测试工具。Selenium测试直接运行在浏览器中,就像真正的用户在操作一样。而且它开源、多浏览器、多平台、api齐全(自带很多方法)、浏览器内运行

loadrunner:LoadRunner 是一种预测系统行为和性能的工业标准级负载测试工具。通过以模拟上千万用户实施并发负载及实时性能监测的方式来确认和查找问题,LoadRunner 能够对整个企业架构进行测试。通过使用LoadRunner , 企业能最大限度地缩短测试时间, 优化性能和加速应用系统的发布周期。目前企业的网络应用环境都必须支持大量用户,网络体系架构中含各类应用环境且由不同供应商提供软件和硬件产品。难以预知的用户负载和愈来愈复杂的应用环境使公司时时担心会发生用户响应速度过慢, 系统崩溃等问题。这些都不可避免地导致公司收益的损失。

前言

在常规的应用系统开发中,很少会涉及到需要对数据进行分库或者分表的操作,多数情况下,我们习惯使用ORM带来的便利,且使用连接查询是一种高效率的开发方式,就算涉及到分表的场景,很多时候也都可以使用ORM自带的分表规则来解决问题。

比如在电商场景中,用户和订单是属于重点增量的数据,通常情况下,或者按用户编号取模或者按订单编号取模进行分表,按便利性来区分,可以使用按用户编号分表解决后续跨表分页查询问题,这也是推荐的方式之一。

据说淘宝采用的是双写订单,即客户和商家各自一套冗余数据库,再指向订单表,这样做可以规避资源抢夺的问题。

分表后查询的多种方法

全局表查询

顾名思义,全局查询就是将分表后的数据主键再集中存储到一张表中,由于全局表只存储很简单的编号信息,查询效率相对较高,但是在数据持续增长的情况下,压力也越来越大。

禁止跳页查询

禁止跳页查询在移动互联网中广泛被应用,这种方法的原理是在查询中摒弃舍弃传统的Page,转而使用一个timestamp时间戳来代码页码,下一页的查询总是在上一页的最后一条记录的时间戳之后,当客户端拉取不到任何数据的时候,即可停止分页。

这种方法带的一个问题就是不允许进行跳转分页,并且会带来冗余查询的问题,比如需要查询多张表后才得到PageSize需要的数据量,只能按部就班的往下查询,不能进行并行查询。特别致命的是,此方法还将带来重复数据的问题。对数据精度要求不高的场景可以采用。

按日期的二次查询法

按日期的二次查询法号称可以解决分页带来的性能和精度问题,具体原理为,先将分页跳过的数据量平均分布到所有表中,如 Page=10,PageSize=50,如果有5个分表,则SQL语句:page=page/5,LIMIT 2,10;分别对5张表进行查询,得到5个结果集,此时,5个结果集里面分别有10条数据,其中下标0和rn-1的结果分别是当前结果集中的最小和最大时间戳(maxTimestamp),通过比较5张表的返回记录得到一个最小的时间戳 minTimestamp,再将这个最小的时间戳带入SQL条件进行二次查询,SQL代码

SELECT * FROM TABLE_NAME WHERE Timestamp BETWEEN @minTimestamp AND @maxTimestamp ORDER BY Timestamp

通过上面的代码,可以从数据库中得到一个完全的结果集,然后在内存中将5个结果集合并排序,取分页数据即可。看起来无懈可击,完美解决了上面两种分页查询引起的问题。实际上我个人认为,这里面还是有一些需要注意的地方,比如由于分表规则的问题导致第一次查询的表比较多(可能几千张表),又或者在二次查询中,某个区间的数据比较大,最后就是在内存中合并结果集也会造成性能问题。
这种查询方法还是解决了精度的问题,也部分解决了性能问题,特别是在取模分表的场景,数据随机性比较大的情况下,还是非常有用的。

大数据集成法

当数据量达到一定程度的时候,可以考虑上ELK或者其它大数据套件,可以很好的解决分页带的影响。

NewSql法

如果有条件,可以迁移数据库到NewSql类型的数据库上,NewSql数据库属于分布式数据库,既有关系数据库的优点又可以无限扩表,通常还支持关系数据库间的无障碍迁移,比如国产的TiDB数据库等。

有序的二次查询法

有序的二次查询法是基于上面的按日期的二次查询法发展而来,这种方法目前还处于测试阶段,具体做法是将数据按天进行分表,这样就可以确保数据块是连续的,以查询最近17天的分页数据为例,先查询出所有表的总行数,这里使用 COUNT(*) ,Mysql 会优化为information_schema.
TABLES
.
TABLE_ROWS
索引查询提高查询效率,不用担心性能问题,下面列出详细的测试步骤。

建立分页实体

public class PageEntity
{
    /// <summary>
    /// 跳过的记录数
    /// </summary>
    public long Skip { get; set; }
    /// <summary>
    /// 选取的记录数
    /// </summary>
    public long Take { get; set; }
    /// <summary>
    /// 总行数
    /// </summary>
    public long Total { get; set; }
    /// <summary>
    /// 表名
    /// </summary>
    public string TableName { get; set; }
}

定义分页算法类

public class PageDataService
{
    ...
}

初始化表

在 PageDataService 类中使用内存表模拟数据库表,主要模拟数据分页的情况,所以每个表的数据量都很小,方便人肉计算和跳页

private readonly static List<PageEntity> entitys = new List<PageEntity>()
{
    new PageEntity{ Total=12,TableName="230301" },
    new PageEntity{ Total=3,TableName="230302" },
    new PageEntity{ Total=4,TableName="230303" },
    new PageEntity{ Total=1,TableName="230304" },
    new PageEntity{ Total=1,TableName="230305" },
    new PageEntity{ Total=7,TableName="230306" },
    new PageEntity{ Total=2,TableName="230307" },
    new PageEntity{ Total=11,TableName="230308" },
    new PageEntity{ Total=41,TableName="230309" },
    new PageEntity{ Total=25,TableName="230310" },
    new PageEntity{ Total=33,TableName="230311" },
    new PageEntity{ Total=8,TableName="230312" },
    new PageEntity{ Total=3,TableName="230313" },
    new PageEntity{ Total=0,TableName="230314" },
    new PageEntity{ Total=17,TableName="230315" },
    new PageEntity{ Total=88,TableName="230316" },
    new PageEntity{ Total=2,TableName="230317" }
};

分页算法

public static List<PageEntity> Pagination(int page, int pageSize)
{
    long preBlock = 0;
    int currentPage = page;
    long currentPageSize = pageSize;   
    List<PageEntity> results = new List<PageEntity>();

    foreach (var item in entitys)
    {
        if (item.Total == 0)
            continue;
        var skip = ((currentPage - 1) * currentPageSize) + preBlock;
        var remainder = item.Total - skip;
        if (remainder > 0)
        {
            item.Skip = skip;
            item.Take = currentPageSize;
            if (remainder >= currentPageSize)
            {
                results.Add(item);
                break;
            }
            else
            {
                currentPageSize = currentPageSize - remainder;
                item.Take = remainder;
                currentPage = 1;
                preBlock = 0;
                results.Add(item);
            }
        }
        else
        {
            preBlock = Math.Abs(remainder);
            currentPage = 1;
        }
    }

    // 输出测试结果
    if (results.Count > 0)
    {
        Console.ForegroundColor = ConsoleColor.Red;
        Console.WriteLine("本次查询,Page:{0},PageSize:{1}", page, pageSize);
        Console.ForegroundColor = ConsoleColor.Gray;
        foreach (var item in results)
        {
            Console.WriteLine("表:{0},总行数:{1},OFFSET:{2},LIMIT:{3}", item.TableName, item.Total, item.Skip, item.Take);
        }
        Console.WriteLine();
    }
    else
    {
        Console.ForegroundColor = ConsoleColor.Red;
        Console.WriteLine("分页下无数据:{0},{1}", page, pageSize);
        Console.ForegroundColor = ConsoleColor.Gray;
    }

    return results;
}

在上面的分页算法中,定义了4个私有变量,分别是
preBlock:存跨表数据块长度
currentPage:当前表分页
currentPageSize:当前表分页长度,也是当前表接 preBlock 所需要的查询长度
results:查询表结果,存需要进行二次查询的表结构

接下来,就对最近 17 张表进行模拟轮询计算,把数据块连接起来,首先是计算 skip 的长度,这里使用当前表分页加跨表块

var skip = ((currentPage - 1) * currentPageSize) + preBlock 

得到真实的 skip,然后用当前表 Total - skip 得到下一表的接续长度

 var remainder = item.Total - skip;

再通过判断接续长度 remainder 大于 0,如果小于0则设定 preBlock 和 currentPage 进入下一表结构,如果大于 0 则进一步判断其是否可以覆盖 currentPageSize,如果可以覆盖则记录当前表并跳出循环,否则 重置 currentPageSize 和其它条件后进入下一个表结构。

if (remainder > 0)
{
    item.Skip = skip;
    item.Take = currentPageSize;
    if (remainder >= currentPageSize)
    {
        results.Add(item);
        break;
    }
    else
    {
        currentPageSize = currentPageSize - remainder;
        item.Take = remainder;
        currentPage = 1;
        preBlock = 0;
        results.Add(item);
    }
}
else
{
    preBlock = Math.Abs(remainder);
    currentPage = 1;
}

测试分页结果

构建一些测试数据进行分页,看接续是否已经闭合

public class Program
{
    public static void Main(string[] args)
    {
        PageDataService.Pagination(1, 40);
        PageDataService.Pagination(2, 40);
        PageDataService.Pagination(3, 40);
        PageDataService.Pagination(4, 40);
        PageDataService.Pagination(5, 40);
        PageDataService.Pagination(6, 40);
        PageDataService.Pagination(7, 40);
        PageDataService.Pagination(8, 40);
        PageDataService.Pagination(9, 40);
        PageDataService.Pagination(113, 10);

        Console.ReadKey();
    }
}

输出测试结果

通过输出的测试结果,可以看到,数据块是连续的,且已经得到了每次需要查询的表结构数据,在实际应用中,只需要对这个结果执行并行查询然后在内存中归并排序就可以了。

并行查询和排序

public static void Query()
{
    var entitys = PageDataService.Pagination(1, 40);
    List<UserEntity> datas = new List<UserEntity>();
    Parallel.ForEach(entitys, entity =>
    {
        var sql = $"SELECT * FROM TABLE_{entity.TableName} ORDER BY Timestamp LIMIT {entity.Skip},{entity.Take}";
        var results = Mysql.Query<UserEntity>(sql);
        datas.AddRange(results);
    });

    // 排序
    datas = datas.OrderByDescending(x => x.Timestamp).ToList();
}

到这里,就完成了有序的二次查询法的算法过程。这种分页算法存在一定的局限性,比如必须是连续的数据块,按一定时间区间进行分表才可使用,大区间查询时的分页,第一次查询会比较慢,比如查询区间为3年内的按天分表分页数据,将会导致第一次查询开启 3*365 个数据库连接,当然,这取决于你第一次查询采用的是并行查询还是轮询,还是有优化空间的。

结束语

本文共列出了多种分库分表方式下的查询问题,大部分 ORM 只解决了分表插入的问题,对于分页查询,实际上也是没有很好的解决方案,原因在于分页查询和业务的分割有着紧密的联系,很多时候不能简单的将业务问题认为是中间件的问题。有序的二次查询法作为一次探索,期望能解决部分业务带来的分页问题。

本文收集了170多个windows11上的快捷键,其中有少部分是windows11新添加的。大部分的win10快捷键也适用于win11。这些快捷键涵盖了系统设置、命令行程序执行、Snap布局切换、对话框快速处理等诸多方面,这里收录的是这些分类中最常用的快捷键。

编写博文的过程中,我已验证了其中90%的快捷键,验证无效的均标记了出来。未验证的,也会在文中注明。

你也可以点击
官方快捷键网页
查看微软官方网站发布的完整快捷键清单。


  • 文中所有单字母快捷键,均使用大写表示,但实际执行时是小写

  • 尽管鼠标操作很高效,但有时候键盘操作更高效,同时还可以偶尔装一把,显得Geek一些

作者:京东零售 董方酉

引言

应用健康度是反馈应用健康程度的指标,它将系统指标分类为基础资源、容器、应用、报警配置、链路这几项,收集了一系列系统应用的指标,并对指标进行打分。

应用健康度的每一项指标显示着系统在某一方面可能存在的隐患和安全问题;因此提高应用健康度对于系统监控具有重要意义。知其然需知其所以然,了解应用健康度中的指标背后的隐患,对于我们了解和提升系统安全性很有帮助。

笔者作为后端研发工程师,同时在推动组内应用健康度提高的同时,基于遇到的问题现象,结合应用健康度进行剖析,将逐一总结一系列应用健康度隐患剖析;

第一篇,我们来剖析下容易被人忽视的数据库时区设置项可能导致的隐患。

一、应用健康度检查项

数据库连接池配置中,通过解析源代码获取,支持DBCP1.X,DBCP2.X,Ali Durid,HikariCP四种连接池;配置监测有以下几项

风险示例如下图所示:

connectTimeout 、SocketTimeout 和 时区 三个指标是连接池数据源的 url 中解析得到, 如:mysql://xxx.jd.com:3358/jdddddb_0?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8&connectTimeout=1000&socketTimeout=3000&serverTimezone=Asia/Shanghai

其中,时区设置容易被人忽视;忽略设置会带来什么样的隐患呢?

二、遇到的问题

1、现象

在2023年3月12日(3月的第二个周日),系统UMP监控报警,提示如下

2、问题原因

Mysql 驱动:mysql-connector-java 升级到8版本后。将数据库时间解析到java时间,需要获取数据库的时区。如果数据库连接中指定时区,则会用该时区,否则可能会使用系统时区

可通过select @@time_zone语句查询,如果返回SYSTEM,则说明数据库没有设置时区,使用select @@system_time_zone 语句可查询得出系统默认时区,为CST。

CST时区为美国中部时间,由于美国有夏令时和非夏令时

CST非夏令时对应 UTC-06:00,夏令时对应 UTC-05:00 。

美国的夏令时,从每年3月第2个星期天凌晨开始,到每年11月第1个星期天凌晨结束。

以2023年为例:

夏令时开始时间调整前:2023年03月12日星期日 02:00:00,时间向前拨一小时.

调整后:2023年03月12日星期日 03:00:00

夏令时结束时间调整前:2023年11月05日星期日 02:00:00,时间往回拨一小时.

调整后:2023年11月05日星期日 01:00:00

这意味这:CST没有2023-03-12 02:00:00~2023-03-12 03:00:00 这个区间的时间。会有两个 2023-11-05 01:00:00~2023-11-05 02:00:00区间的时间。

因此,在获取信息时会抛出“SQLException: HOUR_OF_DAY: 2 -> 3”异常。

3、修改方案

数据库连接地址中设置数据时区:serverTimezone=Asia/Shanghai

三、时间相关的其他隐患

1、据研究实验反馈,设置时区为默认时可能有性能问题,往往需要指定时区。

2、使用timestamp类型时需注意时间偏差:

timestamp类型的时间范围between '1970-01-01 00:00:01' and '2038-01-19 03:14:07',超出这个范围则值记录为'0000-00-00 00:00:00',该类型的一个重要特点就是保存的时间与时区密切相关,UTC(Universal Time Coordinated)标准,指的是经度0度上的标准时间,我国日常生活中时区以首都北京所处的东半球第8区为基准,统一使用东8区时间(俗称北京时间),比UTC要早8个小时,时区设置也遵照此标准,因此对应过来timestamp的时间范围则应校准为'1970-01-01 08:00:01' and '2038-01-19 11:14:07',也就是说东八区的1970-1-1 08:00:01等同于UTC1970-1-1 00:00:01。

3、尽量使用dateTime格式而非timestamp:

有一些情况需要注意不要使用 timestamp 存储时间:

• 生日:生日肯定会有早于1970年的,会超出 timestamp 的范围

• 有效期截止时间:timestamp 的最大时间是2038年,如果用来存类似身份证的有效期截止时间,营业执照的截止时间等就不合适。

• 业务生存时间:互联网时代发展快,业务时间很可能在2038年还在继续运营。

四、数据库连接设置的其他隐患

1、连接数设置

(1) 介绍

数据库连接池在初始化时将创建一定数量的数据库连接放到连接池中,这些数据库连接的数量是由最小数据库连接数制约。无论这些数据库连接是否被使用,连接池都将一直保证至少拥有这么多的连接数量。连接池的最大数据库连接数量限定了这个连接池能占有的最大连接数,当应用程序向连接池请求的连接数超过最大连接数量时,这些请求将被加入到等待队列中。

由此看来,当数据库最大连接数设置不够大时,则会出现某些报表或需要查询数据库的请求失败,由于连接数不够不能被处理,从而报错。当出现大量并发的报表请求,且连接池的最大连接数不够用时,一些用户的请求就无法处理,这样也就从另一个层面影响了整个项目处理吞吐量的能力,限制了项目的性能和效率。

(2)设置原则

既能保证项目正常使用时对数据库连接数的要求,又能保护DBS的安全和稳定。

(3)查询方式:

查询最大连接数命令:show variables like'%max_connections%';

查询当前数据库已建立连接数:show status like 'Threads_connected';

(4)建议:

MYSQL官网给出了一个设置最大连接数的建议比例:

Max_used_connections / max_connections * 100% ≈ 85%




即已使用的连接数占总上限的85%左右。

2、超时时间设置

(1)介绍

一次完整的请求包括三个阶段:1、建立连接 2、数据传输 3、断开连接

connect timeout:如果与服务器(这里指数据库)请求建立连接的时间超过ConnectionTimeOut,就会抛 ConnectionTimeOutException,即服务器连接超时,没有在规定的时间内建立连接。 在数据库连接设置中,connectTimeout表示等待和MySQL数据库建立socket链接的超时时间,默认值0,表示不设置超时,单位毫秒。

socket timeout:如果与服务器连接成功,就开始数据传输了。如果服务器处理数据用时过长,超过了SocketTimeOut,就会抛出SocketTimeOutExceptin,即服务器响应超时,服务器没有在规定的时间内返回给客户端数据。在数据库连接设置中,socketTimeout表示客户端和MySQL数据库建立socket后,读写socket时的等待的超时时间,linux系统默认的socketTimeout为30分钟。

(2)隐患

访问数据库超时间太长,访问数据量大或者扫描的数据量太大,导致数据库长时间无响应。链接被占用无法释放,会导致线程池被占满。因此,为了能够及时释放占用链接,其他业务对数据库访问不受影响,所以要合理设置数据库访问超时时间。

JDBC的socket timeout在数据库被突然停掉或是发生网络错误(由于设备故障等原因)时十分重要。由于TCP/IP的结构原因,socket没有办法探测到网络错误,因此应用也无法主动发现数据库连接断开。如果没有设置socket timeout的话,应用在数据库返回结果前会无期限地等下去,这种连接被称为dead connection。

为了避免dead connections,socket必须要有超时配置。socket timeout可以通过JDBC设置,socket timeout能够避免应用在发生网络错误时产生无休止等待的情况,缩短服务失效的时间。

(3)建议

一般情况,建议配置connectTimeout=60000,单位毫秒。建议配置socketTimeout=60000,单位毫秒。具体配置因系统而异。

总结

容易被忽视的数据库连接应用健康度检查项,背后有着时区、超时时间、连接数设置不当可能带来的隐患;根据应用实际情况并遵循设置原则进行合理设置,满足应用健康度检查项,才能防患于未然。

Redis 是一种高性能的缓存和 key-value 存储系统,常被用来实现分布式 Session 的方案。在这种方案中,用户的登录信息存储在 Redis 中,而不是存储在本地的 cookie 或 session 中。

当用户在集群中的不同节点之间切换时,通过读取 Redis 中的登录信息,各个节点可以实现登录态的同步。这种方式能够解决传统基于 cookie 和 session 的方案中,不同节点之间登录状态不同步的问题。此外,由于 Redis 的高可用和高性能,使得在分布式环境下,访问登录信息时速度更快,同时能够更好地应对高并发请求的情况。

配置简单,只需在yml中增加以下配置。

  #session失效时间
session:
  timeout: 86400
  store-type: redis

相比于传统的基于 cookie 和 session 的方案,使用 Redis 实现分布式 Session 有以下区别:

  1. 数据存储位置:普通的cookie session将数据存储在客户端浏览器中,而Redis分布式Session将数据存储在Redis服务器中。
  2. 可扩展性:Redis分布式Session可以方便地扩展应用程序,使得多个应用程序可以共享用户的Session数据,而普通的cookie session需要在每个应用程序上维护一份Session数据,难以扩展。
  3. 高性能:Redis 具有高性能的缓存机制,能够快速读写登录信息,保证在高并发场景下的用户体验。
  4. 可靠性:Redis分布式Session使用了Redis的高可用特性,可以提供更高的可靠性,而普通的cookie session在浏览器关闭或者清除Cookie的情况下会丢失Session数据。
  5. 跨平台性:由于 Redis 是基于网络的存储,能够跨平台地进行存储和读取登录信息,提高了应用的可移植性和扩展性。
  6. 安全性:Redis分布式Session可以使用更复杂的Session ID 算法,从而提高Session的安全性,而普通的cookie session则使用简单的Cookie值作为Session ID,容易受到伪造和攻击。

哈希算法:可以使用SHA-256或SHA-512等哈希算法生成唯一的Session ID。这些算法可以保证唯一性和不可预测性,并且比简单的随机数更难以猜测和伪造。另外,可以将一些与用户相关的信息,如用户ID、IP地址、时间戳等,作为哈希算法的输入,进一步增加Session ID的复杂度。

在Java中,我们可以使用哈希算法实现更复杂的Session ID算法。下面是一个使用SHA-256哈希算法生成Session ID的示例代码:

javaCopy code
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;

public class SessionIdGenerator {

    public static String generateSessionId() {
        try {
            // 使用SHA-256算法生成MessageDigest实例
            MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");

            // 生成16个随机字节作为Session ID的种子
            SecureRandom secureRandom = new SecureRandom();
            byte[] seed = secureRandom.generateSeed(16);

            // 将种子更新到MessageDigest中
            messageDigest.update(seed);

            // 对MessageDigest的摘要结果进行哈希计算
            byte[] digest = messageDigest.digest();

            // 将哈希结果转换为十六进制字符串作为Session ID
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < digest.length; i++) {
                sb.append(String.format("%02x", digest[i]));
            }
            return sb.toString();
        } catch (NoSuchAlgorithmException e) {
            // 当指定的算法不存在时,抛出异常
            throw new RuntimeException("SHA-256 algorithm not found");
        }
    }
}

在这个示例代码中,我们使用SHA-256算法生成MessageDigest实例,并将16个随机字节作为Session ID的种子更新到MessageDigest中。然后对MessageDigest的摘要结果进行哈希计算,将哈希结果转换为十六进制字符串作为Session ID返回。

你可以在需要使用Session ID的地方调用generateSessionId()方法来生成Session ID。例如,在Java Web应用程序中,可以在用户登录时生成Session ID并将其存储在HttpSession中。