• [技术干货] CBM文件的命名格式
    CBM 文件保存在data 目录的pg_cbm 文件夹下,命名方式为:pg_xlog_seqnum_startlsn_endlsn.cbm。seqnum文件序号表示这是第几个cbm文件,当一个cbm文件的大小超过100M时,将会切换到下一个cbm文件,并将seqnum加1。 startlsn为本cbm文件内容对应xlog记录的起始lsn; endlsn为本cbm文件切换时最后一次解析的截止lsn,若一个cbm文件还没有切换,那么endlsn为0。pg_cbm_tracked_location 说明:用于查询cbm已经解析到的lsn位置 入参:无 返回值:cbm已经解析到的lsn位置 b. pg_cbm_get_merged_file 说明:用于将指定lsn范围之内的cbm文件合并成一个cbm文件 入参:startlsn,指定的起始lsn;endlsn,指定的结束lsn 返回值:合并完的cbm文件名 c. pg_cbm_get_changed_block 说明:用于将指定lsn范围之内的cbm文件合并一个表,并返回表的各行记录 入参:startlsn,指定的起始lsn;endlsn,指定的结束lsn 返回值:合并完的表的记录,表的结构如下
  • [技术干货] Checkpoint 操作
    当系统运行时间较长的时候,由于操作较多,日志文件的数量也较多。如果每次利用日志进行恢复操作都会耗费大量的时间,为了节约时间同时减少不必要的恢复操作,引入了checkpoint的概念。checkpoint表示在此操作之前,相关数据已经被保存到永久存储中,即使系统故障,这部分数据也不会丢失,因此恢复的时候只要从checkpoint操作之后根据日志执行恢复操作就可以了。checkpoint本身也是一条xlog记录,该记录包含了redo点的位置,因此,每次恢复数据时,先从xloh记录里找到最近的一次checkpoint记录,并根据该记录找到相应的redo点位置,这就是执行本次恢复的起始点位置。checkpoint操作记录了redo点的位置。基于上述功能,由于数据的所有变化都被记录在了xlog中,GaussDB(DWS)数据库内核通过增加常驻的CBM writer线程,持续不断地对新增的xlog进行解析,识别并记录哪些数据数据页面被修改。进程启动时即开启CBM writer功能 在startup线程刚启动时,其根据已经解析出来的CBM文件,来决定CBM writer开始解析的起始LSN位置。每次执行到checkpoint末尾时,会设置CBM writer线程的latch。CBM writer线程等待latch被设置,然后进行一轮日志解析。 通过动态reload GUC参数,开启CBM writer功能 由于是动态开启的CBM writer功能,因此startup线程没有初始化CBM解析的起始位置。打开enable_cbm_writer开关的同时,会将CBM强制初始化的标志置为true。当CBM writer线程启动之后,其第一次解析中,会强制初始化获得解析的起始lsn。
  • [技术干货] 差分增量备份
    如果一次全量备份后的多次增量备份,指定的prior-backup-key均为上一次备份(可能是全量备份也可能增量备份)的backup-key,即此次备份是基于上一次备份集来进行的,那么这些增量备份就是差分增量备份,差分增量备份均是基于最近一次备份进行的。增量备份只需要备份上一备份节点到当前时间发生变化的数据文件,为了实现备份数据的完整性与一致性,正确识别并备份增量数据文件是至关重要的,作为增量备份的核心GaussDB(DWS)数据库内核的事务日志功能与cbm设计可以帮助Roach工具快速准确识别增量期间数据文件的变换信息,为快速准确完成增量备份提供了有力保障。为了保证数据的一致性和完整性,在对数据进行相关操作之前都会将具体的操作记录下来,持久化到可靠存储中,然后再进行具体的数据操作,这就是所谓的WAL(Write Ahead Logging),记录的相关操作称为XLOG日志,每一条日志记录都由LSN进行唯一标识。这样做的好处是事务的记录被提前记录并保存起来,在因一些外部原因(比如断电、操作系统失败等)导致操作失败后,我们可以通过保存的事务日志将这些操作重新执行一遍,保证数据不会丢失。
  • [技术干货] 增量备份核心设计
    增量备份指备份基于上一个备份集到当前阶段的数据变化文件,相比于全量备份所用的时间更少,能够节约大量的空间资源,因此一直是客户的常规备份手段。为了实现数据的完整性与一致性,正确识别并备份增量数据文件是至关重要的,Gauss(DWS)数据库内核的事务日志功能与CBM设计可以帮助Roach工具快速准确识别增量期间数据文件的变换信息,从而快速实现增量备份的任务。GaussDB(DWS)数仓的备份恢复工具Roach支持集群级增量备份。全量备份会将源数据完整备份,而增量备份仅将上次备份后所作的更改进行备份,这里的上次备份可以使全量备份,也可以是全量备份后的增量备份。需要注意的是,增量备份的基础始终是全量备份,如果一次全量备份之后进行了全量恢复,则不能再基于该全量备份进行增量备份,必须重新进行全量备份然后基于新的全量备份进行增量备份。 增量备份分为两种:累积增量备份和差分增量备份。如果一次全量备份后的多次增量备份,指定的prior-backup-key始终为全量备份的backup-key,即所有的增量备份都是基于全量备份来进行的,那么这些增量备份就是累积增量备份,累积增量备份均是基于最近一次全量备份进行的。
  • [技术干货] 使用通信中的视图进行分析
    组合使用pgxc_stat_activity 、 pgxc_thread_wait_status 、pgxc_comm_send_stream、pgxc_comm_recv_stream视图定位问题 在pgxc_stat_activity 视图中找到问题查询的query_id; 根据query_id查询pgxc_thread_wait_status 视图,看到 CN 在等DN4的数据,而DN4在与DN42建连;查询pgxc_comm_send_stream看到DN4发起的与DN2的逻辑连接中有一个处于READY半连接状态;查询pgxc_comm_recv_stream看到DN2已经接受了DN4发起的4个逻辑连接,问题出在DN4逻辑连接应答上。DN间接收数据分两个动作 LibcommRecvPoll,等待数据到达; LibcommRecv,数据到达后接收数据。 Poll Time:等待数据到达所花费的时间; Send time:发送数据并接收对端quota所花费的时间。在GaussDB(DWS)通信运维中,很多情况是由于一些参数配置的不合理导致集群效率低、资源过载、连接异常等问题。对于通信库和操作系统两方面都提供了大量可供用户自定义的参数、视图、脚本,熟练掌握一些重要工具有助于快速定位现网通信故障问题。 
  • [技术干货] 数据收发过程中报错 
    CN 连接DN报错, 主要关注后面的detail信息 Already too many client:DN线程数超过max_connections上限; Connection timed out:TCP建连超时,网络不通或者网络质量差; Connection refused:TCP建连被拒绝,防火墙配置或者端口号错误; timeout expired:连接已经建立,但连接信息同步时对端响应超时。 数据收发过程中报错 cancel/terminate日志:外部信号中止查询; reset by peer/broken pipe:连接对端线程主动关闭连接; Stream closed by remote:对端关闭连接或发生重启,需查看对端日志; Peer address changed:对端DN存在主备切换。 DN 间通信性能差 pv_total_memory_detail中通信内存过大:Libcomm通信库降级运行; gsar监控有严重丢包重传:网络质量差,丢包过多导致连接断开; top查看CPU高或者ksoftirq进程CPU高:排查异常CPU占用。
  • [技术干货] 集群通信常见问题
    CN 连接DN报错, 主要关注后面的detail信息 Already too many client:DN线程数超过max_connections上限; Connection timed out:TCP建连超时,网络不通或者网络质量差; Connection refused:TCP建连被拒绝,防火墙配置或者端口号错误; timeout expired:连接已经建立,但连接信息同步时对端响应超时。 数据收发过程中报错 cancel/terminate日志:外部信号中止查询; reset by peer/broken pipe:连接对端线程主动关闭连接; Stream closed by remote:对端关闭连接或发生重启,需查看对端日志; Peer address changed:对端DN存在主备切换。 DN 间通信性能差 pv_total_memory_detail中通信内存过大:Libcomm通信库降级运行; gsar监控有严重丢包重传:网络质量差,丢包过多导致连接断开; top查看CPU高或者ksoftirq进程CPU高:排查异常CPU占用。
  • [技术干货] CN多流
    对于256节点的集群来说,并发场景导致CN和DN之间存在大量连接,每个连接占用一个端口,则CN的端口号很容易受限。为解决此问题,设计了CN多流,即 CN与DN之间采用逻辑连接。comm_cn_dn_logic_conn参数默认值是off,在集群规模或并发达到一定程度时,需要将其开启为on,避免CN与DN之间由于端口号受限而无法建连。TCP 代理通信库采用 pull 模式进行流量控制,避免消息堵塞。两个DN 分别有一个buffer,当一条通道发送端数据量过大时,很容易造成buffer 填满,阻塞了其他通道的发送。此时,对于每条通道设置一个quota,接收端根据buffer剩余空间的大小发送给发送端一个合理quota值,发送端根据quota大小发送数据。 comm_quota_size 表示每个通道单次不间断发送数据量配额,默认值 1MB。当通道发送数据量达到配额时,发送端等待接收端重新发送配额,进而继续发送数据,从而实现流控功能。其取值为0时,表示不使用quota,在一些大流量等场景中,查询之间可能会有影响。在1GE网卡环境中,受网卡能力限制,应该调小该参数,推荐20KB~40KB。如果环境内存充足,参数comm_usable_memory设置较大,可以适当调大,从而提升性能。
  • [技术干货] TCP代理通信库
    视图展示所有DN的通信库状态。其中字段包括节点名称、节点通信库接收速率,单位为byte/s、节点通信库发送速率,单位为byte/s、节点通信库接收速率,单位为Kbyte/s、节点通信库发送速率,单位为Kbyte/s、cmailbox的buffer大小、libcomm进程通信内存的大小、libpq 进程通信内存的大小、postmaster 线程实时使用率、gs_sender_flow_controller 线程实时使用率、gs_receiver_flow_controller 线程实时使用率、多个gs_receivers_loop线程中最高的实时使用率、当前使用的逻辑连接总数。表示TCP代理通信库支持的最大DN数,最小值为1,最大值为8192。当DN数小于256时,默认值为256;否则,为大于等于DN数的2的N次方。在集群扩容、缩容场景下,要注意此参数的变更。 表示TCP代理通信库支持的最大并发Stream数量,默认值为1024,最 大 为 60000,此参数要保证大于并发数每并发平均Stream算子数(smp的平方),否则会报错Cannot get stream index, maybe comm_max_stream is not enough。此外,在设置此参数时需要考虑占用内存问题,其大小为256byte * comm_max_stream * comm_max_datanode ,可见在内存、comm_max_datanode 和comm_max_stream三者之间需要一个动态的规划。
  • [技术干货] 通信流
    视图展示所有DN上的通信库接收流状态。其中字段包括节点名称、使用此通信流的线程ID、连接对端节点名称、连接对端节点ID、通信对端DN在本DN内的标识编号、通信流在物理连接中的标识编号、通信流所使用的tpc通信socket、通信流当前的状态、通信流对应的debug_query_id 编号、通信流所执行查询的 plan_node_id 编号、通信流所执行查询send端的smpid编号、通信流所执行查询recv端的smpid编号、通信流接收的数据总量、通信流当前生命周期使用时长、通信流的平均接收速率、通信流当前的通信配额值、通信流当前缓存的数据大小。展示所有DN上的通信库发送流状态。其中字段包括节点名称、使用此通信流的线程ID、连接对端节点名称、连接对端节点ID、通信对端DN在本DN内的标识编号、通信流在物理连接中的标识编号、通信流所使用的tpc通信socket、通信流当前的状态、通信流对应的debug_query_id 编号、通信流所执行查询的 plan_node_id 编号、通信流所执行查询send端的smpid编号、通信流所执行查询recv端的smpid编号、通信流接收的数据总量、通信流当前生命周期使用时长、通信流的平均接收速率、通信流当前的通信配额值和通信流等待quota值产生的额外时间开销。
  • [技术干货] producer 线程
    并发比较高时 producer 线程会一直往队列里push,如果此时对端 cunsumer1 线程正在处理别的数据导致接收 buffer1 满了的话,producer2 和 producer3 无法往网络上填充更多的数据,发送阶段就会阻塞,而此时可能consumer2和consumer3正在空闲状态,等待这个接收数据,但是因为发送端阻塞而接收不到,这种场景会严重影响性能。这个模型我们称之为push模型。因此我们需要通过另外一种流控机制来解决这个问题,我们称之为pull模型。 push模型:发送端不感知接收端状态。一直往无锁队列中push,直到push阻塞; poll 模型:发送端感知接收端状态。发送端一开始不会发送数据,当接收端里的buffer 池内存满足一定条件时,通知对应的发送端,并告知可以接收的数据量,发送端可以按照对端可以接收的数据量进行发送。 通过 poll 模型的实现,在本线程阻塞的情况下,其他的线程不会阻塞,以确保物理连接中数据永远不会阻塞,保证连接的通畅性。该视图中的字段包括节点名称、连接对端节点的节点名称、连接对端IP的对端地址、当前物理连接使用的 Stream 逻辑连接数量、当前物理连接一分钟内探测到的最小时延、当前物理连接一分钟内探测道德平均值和当前物理连接一分钟内探测到的最大时延。
  • [技术干货] Libcomm 通信库
    GaussDB(DWS)为解决建联过多的问题,实现了 Libcomm 通信库(即逻辑连接通信库),在一个物理长连接上模拟n个逻辑连接,使得所有并发的数据跑在一个物理连接上。比如DN1需要给DN2发送数据,并发数1000,在原有逻辑下,DN1需要建立与DN2连接的1000个线程与之进行交互,消耗了大量的连接、内存、fd资源,而改造Libcomm通信库之后,DN1 与 DN2 仅需建立一个真正的物理连接,在这个物理连接上可以建立很多个逻辑链接,这样可以使得1000个并发就可以用同一个物理连接进行数据交互。 那么GaussDB(DWS)的逻辑连接是怎么实现的呢?首先我们从连接数据流入手,挖掘其实现逻辑。物理连接支持TCP、RDMA等协议连接,以TCP为例,其物理连接数据流可以分为两部分,即数据包头+数据。数据包头为固定长度,其中包含逻辑连接号和数据块长度,用来区别逻辑连接,并接收每个连接各自对应的数据。send queue:producer 发送线程将要发送的数据先push到一个无锁队列中,push完成之后,producer线程就可以继续做自己的事情了; send proxy thread:通信存在一个发送端代理线程,会统一将无锁队列中的数据,通过物理连接发送到对端。
  • [问题求助] 求助,求助帖: 云服务实例gaussdb集中式, 建表是不是不能指定列存以及不能创建分布式键
    求助,gaussdb集中式是不是不能创建 分布式键的表吗?CREATE TABLE tb_t3(c1 int,c2 int) DISTRIBUTE BY RANGE(c1)(    SLICE s1 VALUES LESS THAN (100),    SLICE s2 VALUES LESS THAN (200),    SLICE s3 VALUES LESS THAN (MAXVALUE) );
  • [技术干货] 健康体检项目之运营数据统计
    Apache POI 是用Java编写的免费开源的跨平台的Java API,Apache POI提供API给Java程序对Microsoft Office格式档案读和写的功能,其中使用最多的就是使用POI操作Excel文件。POI为“Poor Obfuscation Implementation”的首字母缩写,意为“简洁版的模糊实现”。一、运营数据统计1.1 需求分析通过运营数据统计可以展示出体检机构的运营情况,包括会员数据、预约到诊数据、热门套餐等信息。本章节就是要通过一个表格的形式来展示这些运营数据。效果如下图:  1.2 完善页面运营数据统计对应的页面为/pages/report_business.html。1.2.1 定义模型数据定义数据模型,通过VUE的数据绑定展示数据<script> var vue = new Vue({ el: '#app', data:{ reportData:{ reportDate:null, todayNewMember :0, totalMember :0, thisWeekNewMember :0, thisMonthNewMember :0, todayOrderNumber :0, todayVisitsNumber :0, thisWeekOrderNumber :0, thisWeekVisitsNumber :0, thisMonthOrderNumber :0, thisMonthVisitsNumber :0, hotSetmeal :[] } } }) </script><div class="box" style="height: 900px"> <div class="excelTitle" > <el-button @click="exportExcel">导出Excel</el-button>运营数据统计 </div> <div class="excelTime">日期:{{reportData.reportDate}}</div> <table class="exceTable" cellspacing="0" cellpadding="0"> <tr> <td colspan="4" class="headBody">会员数据统计</td> </tr> <tr> <td width='20%' class="tabletrBg">新增会员数</td> <td width='30%'>{{reportData.todayNewMember}}</td> <td width='20%' class="tabletrBg">总会员数</td> <td width='30%'>{{reportData.totalMember}}</td> </tr> <tr> <td class="tabletrBg">本周新增会员数</td> <td>{{reportData.thisWeekNewMember}}</td> <td class="tabletrBg">本月新增会员数</td> <td>{{reportData.thisMonthNewMember}}</td> </tr> <tr> <td colspan="4" class="headBody">预约到诊数据统计</td> </tr> <tr> <td class="tabletrBg">今日预约数</td> <td>{{reportData.todayOrderNumber}}</td> <td class="tabletrBg">今日到诊数</td> <td>{{reportData.todayVisitsNumber}}</td> </tr> <tr> <td class="tabletrBg">本周预约数</td> <td>{{reportData.thisWeekOrderNumber}}</td> <td class="tabletrBg">本周到诊数</td> <td>{{reportData.thisWeekVisitsNumber}}</td> </tr> <tr> <td class="tabletrBg">本月预约数</td> <td>{{reportData.thisMonthOrderNumber}}</td> <td class="tabletrBg">本月到诊数</td> <td>{{reportData.thisMonthVisitsNumber}}</td> </tr> <tr> <td colspan="4" class="headBody">热门套餐</td> </tr> <tr class="tabletrBg textCenter"> <td>套餐名称</td> <td>预约数量</td> <td>占比</td> <td>备注</td> </tr> <tr v-for="s in reportData.hotSetmeal"> <td>{{s.name}}</td> <td>{{s.setmeal_count}}</td> <td>{{s.proportion}}</td> <td></td> </tr> </table> </div>1.2.2 发送请求获取动态数据在VUE的钩子函数中发送ajax请求获取动态数据,通过VUE的数据绑定将数据展示到页面<script> var vue = new Vue({ el: '#app', data:{ reportData:{ reportDate:null, todayNewMember :0, totalMember :0, thisWeekNewMember :0, thisMonthNewMember :0, todayOrderNumber :0, todayVisitsNumber :0, thisWeekOrderNumber :0, thisWeekVisitsNumber :0, thisMonthOrderNumber :0, thisMonthVisitsNumber :0, hotSetmeal :[] } }, created() { //发送ajax请求获取动态数据 axios.get("/report/getBusinessReportData.do").then((res)=>{ this.reportData = res.data.data; }); } }) </script>根据页面对数据格式的要求,我们发送ajax请求,服务端需要返回如下格式的数据:{ "data":{ "todayVisitsNumber":0, "reportDate":"2019-04-25", "todayNewMember":0, "thisWeekVisitsNumber":0, "thisMonthNewMember":2, "thisWeekNewMember":0, "totalMember":10, "thisMonthOrderNumber":2, "thisMonthVisitsNumber":0, "todayOrderNumber":0, "thisWeekOrderNumber":0, "hotSetmeal":[ {"proportion":0.4545,"name":"粉红珍爱(女)升级TM12项筛查体检套餐","setmeal_count":5}, {"proportion":0.1818,"name":"阳光爸妈升级肿瘤12项筛查体检套餐","setmeal_count":2}, {"proportion":0.1818,"name":"珍爱高端升级肿瘤12项筛查","setmeal_count":2}, {"proportion":0.0909,"name":"孕前检查套餐","setmeal_count":1} ], }, "flag":true, "message":"获取运营统计数据成功" }1.3 后台代码1.3.1 Controller在ReportController中提供getBusinessReportData方法@Reference private ReportService reportService; ​ /** * 获取运营统计数据 * @return */ @RequestMapping("/getBusinessReportData") public Result getBusinessReportData(){ try { Map<String, Object> result = reportService.getBusinessReport(); return new Result(true,MessageConstant.GET_BUSINESS_REPORT_SUCCESS,result); } catch (Exception e) { e.printStackTrace(); return new Result(true,MessageConstant.GET_BUSINESS_REPORT_FAIL); } }1.3.2 服务接口在health_interface工程中创建ReportService服务接口并声明getBusinessReport方法package com.yunhe.service; import java.util.Map; public interface ReportService { /** * 获得运营统计数据 * Map数据格式: * todayNewMember -> number * totalMember -> number * thisWeekNewMember -> number * thisMonthNewMember -> number * todayOrderNumber -> number * todayVisitsNumber -> number * thisWeekOrderNumber -> number * thisWeekVisitsNumber -> number * thisMonthOrderNumber -> number * thisMonthVisitsNumber -> number * hotSetmeals -> List<Setmeal> */ public Map<String,Object> getBusinessReport() throws Exception; }1.3.3 服务实现类在health_service_provider工程中创建服务实现类ReportServiceImpl并实现ReportService接口package com.yunhe.service; import com.alibaba.dubbo.config.annotation.Service; import com.yunhe.dao.MemberDao; import com.yunhe.dao.OrderDao; import com.yunhe.utils.DateUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; /** * 统计报表服务 */ @Service(interfaceClass = ReportService.class) @Transactional public class ReportServiceImpl implements ReportService { @Autowired private MemberDao memberDao; @Autowired private OrderDao orderDao; /** * 获得运营统计数据 * Map数据格式: * todayNewMember -> number * totalMember -> number * thisWeekNewMember -> number * thisMonthNewMember -> number * todayOrderNumber -> number * todayVisitsNumber -> number * thisWeekOrderNumber -> number * thisWeekVisitsNumber -> number * thisMonthOrderNumber -> number * thisMonthVisitsNumber -> number * hotSetmeal -> List<Setmeal> */ public Map<String, Object> getBusinessReport() throws Exception{ //获得当前日期 String today = DateUtils.parseDate2String(DateUtils.getToday()); //获得本周一的日期 String thisWeekMonday = DateUtils.parseDate2String(DateUtils.getThisWeekMonday()); //获得本月第一天的日期 String firstDay4ThisMonth = DateUtils.parseDate2String(DateUtils.getFirstDay4ThisMonth()); //今日新增会员数 Integer todayNewMember = memberDao.findMemberCountByDate(today); //总会员数 Integer totalMember = memberDao.findMemberTotalCount(); //本周新增会员数 Integer thisWeekNewMember = memberDao.findMemberCountAfterDate(thisWeekMonday); //本月新增会员数 Integer thisMonthNewMember = memberDao.findMemberCountAfterDate(firstDay4ThisMonth); //今日预约数 Integer todayOrderNumber = orderDao.findOrderCountByDate(today); //本周预约数 Integer thisWeekOrderNumber = orderDao.findOrderCountAfterDate(thisWeekMonday); //本月预约数 Integer thisMonthOrderNumber = orderDao.findOrderCountAfterDate(firstDay4ThisMonth); //今日到诊数 Integer todayVisitsNumber = orderDao.findVisitsCountByDate(today); //本周到诊数 Integer thisWeekVisitsNumber = orderDao.findVisitsCountAfterDate(thisWeekMonday); //本月到诊数 Integer thisMonthVisitsNumber = orderDao.findVisitsCountAfterDate(firstDay4ThisMonth); //热门套餐(取前4) List<Map> hotSetmeal = orderDao.findHotSetmeal(); Map<String,Object> result = new HashMap<>(); result.put("reportDate",today); result.put("todayNewMember",todayNewMember); result.put("totalMember",totalMember); result.put("thisWeekNewMember",thisWeekNewMember); result.put("thisMonthNewMember",thisMonthNewMember); result.put("todayOrderNumber",todayOrderNumber); result.put("thisWeekOrderNumber",thisWeekOrderNumber); result.put("thisMonthOrderNumber",thisMonthOrderNumber); result.put("todayVisitsNumber",todayVisitsNumber); result.put("thisWeekVisitsNumber",thisWeekVisitsNumber); result.put("thisMonthVisitsNumber",thisMonthVisitsNumber); result.put("hotSetmeal",hotSetmeal); return result; } }1.3.4 Dao接口在OrderDao和MemberDao中声明相关统计查询方法package com.yunhe.dao; import com.yunhe.pojo.Order; import java.util.List; import java.util.Map; public interface OrderDao { public void add(Order order); public List<Order> findByCondition(Order order); public Map findById4Detail(Integer id); public Integer findOrderCountByDate(String date); public Integer findOrderCountAfterDate(String date); public Integer findVisitsCountByDate(String date); public Integer findVisitsCountAfterDate(String date); public List<Map> findHotSetmeal(); }package com.yunhe.dao; import com.github.pagehelper.Page; import com.yunhe.pojo.Member; import java.util.List; public interface MemberDao { public List<Member> findAll(); public Page<Member> selectByCondition(String queryString); public void add(Member member); public void deleteById(Integer id); public Member findById(Integer id); public Member findByTelephone(String telephone); public void edit(Member member); public Integer findMemberCountBeforeDate(String date); public Integer findMemberCountByDate(String date); public Integer findMemberCountAfterDate(String date); public Integer findMemberTotalCount(); }1.3.5 Mapper映射文件在OrderDao.xml和MemberDao.xml中定义SQL语句OrderDao.xml:<!--根据日期统计预约数--> <select id="findOrderCountByDate" parameterType="string" resultType="int"> select count(id) from t_order where orderDate = #{value} </select> ​ <!--根据日期统计预约数,统计指定日期之后的预约数--> <select id="findOrderCountAfterDate" parameterType="string" resultType="int"> select count(id) from t_order where orderDate &gt;= #{value} </select> ​ <!--根据日期统计到诊数--> <select id="findVisitsCountByDate" parameterType="string" resultType="int"> select count(id) from t_order where orderDate = #{value} and orderStatus = '已到诊' </select> ​ <!--根据日期统计到诊数,统计指定日期之后的到诊数--> <select id="findVisitsCountAfterDate" parameterType="string" resultType="int"> select count(id) from t_order where orderDate &gt;= #{value} and orderStatus = '已到诊' </select> ​ <!--热门套餐,查询前4条--> <select id="findHotSetmeal" resultType="map"> select s.name, count(o.id) setmeal_count , count(o.id)/(select count(id) from t_order) proportion from t_order o inner join t_setmeal s on s.id = o.setmeal_id group by o.setmeal_id order by setmeal_count desc limit 0,4 </select>MemberDao.xml:<!--根据日期统计会员数,统计指定日期之前的会员数--> <select id="findMemberCountBeforeDate" parameterType="string" resultType="int"> select count(id) from t_member where regTime &lt;= #{value} </select> ​ <!--根据日期统计会员数--> <select id="findMemberCountByDate" parameterType="string" resultType="int"> select count(id) from t_member where regTime = #{value} </select> ​ <!--根据日期统计会员数,统计指定日期之后的会员数--> <select id="findMemberCountAfterDate" parameterType="string" resultType="int"> select count(id) from t_member where regTime &gt;= #{value} </select> ​ <!--总会员数--> <select id="findMemberTotalCount" resultType="int"> select count(id) from t_member </select>二、. 运营数据统计报表导出2.1 需求分析运营数据统计报表导出就是将统计数据写入到Excel并提供给客户端浏览器进行下载,以便体检机构管理人员对运营数据的查看和存档。2.2 提供模板文件本节我们需要将运营统计数据通过POI写入到Excel文件,对应的Excel效果如下:  通过上面的Excel效果可以看到,表格比较复杂,涉及到合并单元格、字体、字号、字体加粗、对齐方式等的设置。如果我们通过POI编程的方式来设置这些效果代码会非常繁琐。在企业实际开发中,对于这种比较复杂的表格导出一般我们会提前设计一个Excel模板文件,在这个模板文件中提前将表格的结构和样式设置好,我们的程序只需要读取这个文件并在文件中的相应位置写入具体的值就可以了。在本章节资料中已经提供了一个名为report_template.xlsx的模板文件,需要将这个文件复制到health_backend工程的template目录中2.3 完善页面在report_business.html页面提供导出按钮并绑定事件<div class="excelTitle" > <el-button @click="exportExcel">导出Excel</el-button>运营数据统计 </div>methods:{ //导出Excel报表 exportExcel(){ window.location.href = '/report/exportBusinessReport.do'; } }2.4 后台代码在ReportController中提供exportBusinessReport方法,基于POI将数据写入到Excel中并通过输出流下载到客户端。/** * 导出Excel报表 * @return */ @RequestMapping("/exportBusinessReport") public Result exportBusinessReport(HttpServletRequest request, HttpServletResponse response){ try{ //远程调用报表服务获取报表数据 Map<String, Object> result = reportService.getBusinessReport(); //取出返回结果数据,准备将报表数据写入到Excel文件中 String reportDate = (String) result.get("reportDate"); Integer todayNewMember = (Integer) result.get("todayNewMember"); Integer totalMember = (Integer) result.get("totalMember"); Integer thisWeekNewMember = (Integer) result.get("thisWeekNewMember"); Integer thisMonthNewMember = (Integer) result.get("thisMonthNewMember"); Integer todayOrderNumber = (Integer) result.get("todayOrderNumber"); Integer thisWeekOrderNumber = (Integer) result.get("thisWeekOrderNumber"); Integer thisMonthOrderNumber = (Integer) result.get("thisMonthOrderNumber"); Integer todayVisitsNumber = (Integer) result.get("todayVisitsNumber"); Integer thisWeekVisitsNumber = (Integer) result.get("thisWeekVisitsNumber"); Integer thisMonthVisitsNumber = (Integer) result.get("thisMonthVisitsNumber"); List<Map> hotSetmeal = (List<Map>) result.get("hotSetmeal"); //获得Excel模板文件绝对路径 String temlateRealPath = request.getSession().getServletContext().getRealPath("template") + File.separator + "report_template.xlsx"; //读取模板文件创建Excel表格对象 XSSFWorkbook workbook = new XSSFWorkbook(new FileInputStream(new File(temlateRealPath))); XSSFSheet sheet = workbook.getSheetAt(0); XSSFRow row = sheet.getRow(2); row.getCell(5).setCellValue(reportDate);//日期 ​ row = sheet.getRow(4); row.getCell(5).setCellValue(todayNewMember);//新增会员数(本日) row.getCell(7).setCellValue(totalMember);//总会员数 ​ row = sheet.getRow(5); row.getCell(5).setCellValue(thisWeekNewMember);//本周新增会员数 row.getCell(7).setCellValue(thisMonthNewMember);//本月新增会员数 ​ row = sheet.getRow(7); row.getCell(5).setCellValue(todayOrderNumber);//今日预约数 row.getCell(7).setCellValue(todayVisitsNumber);//今日到诊数 ​ row = sheet.getRow(8); row.getCell(5).setCellValue(thisWeekOrderNumber);//本周预约数 row.getCell(7).setCellValue(thisWeekVisitsNumber);//本周到诊数 ​ row = sheet.getRow(9); row.getCell(5).setCellValue(thisMonthOrderNumber);//本月预约数 row.getCell(7).setCellValue(thisMonthVisitsNumber);//本月到诊数 ​ int rowNum = 12; for(Map map : hotSetmeal){//热门套餐 String name = (String) map.get("name"); Long setmeal_count = (Long) map.get("setmeal_count"); BigDecimal proportion = (BigDecimal) map.get("proportion"); row = sheet.getRow(rowNum ++); row.getCell(4).setCellValue(name);//套餐名称 row.getCell(5).setCellValue(setmeal_count);//预约数量 row.getCell(6).setCellValue(proportion.doubleValue());//占比 } ​ //通过输出流进行文件下载 ServletOutputStream out = response.getOutputStream(); response.setContentType("application/vnd.ms-excel"); response.setHeader("content-Disposition", "attachment;filename=report.xlsx"); workbook.write(out); out.flush(); out.close(); workbook.close(); return null; }catch (Exception e){ return new Result(false, MessageConstant.GET_BUSINESS_REPORT_FAIL,null); } } 
  • 数据库的Isolation特性
    数据库的隔离性(Isolation)核心是通过控制事务间数据的可见性、加锁策略或多版本数据管理(MVCC),减少不同事务对同一数据的干扰,不同隔离级别通过 “放宽或收紧” 这种控制力度,实现对脏读、不可重复读、幻读的不同防护效果。以 InnoDB 为例,其默认隔离级别 “可重复读(Repeatable Read)” 通过MVCC(多版本并发控制)+ 行锁 + 间隙锁的组合,避免脏读和不可重复读,但因 “当前读” 与 “快照读” 的机制差异,仍可能出现幻读。一、隔离性与隔离级别的核心逻辑:控制事务可见性隔离性的本质是解决 “多个事务同时读写同一数据时的干扰问题”,SQL 标准定义了 4 个隔离级别,从低到高对 “事务可见性” 的限制逐渐严格,对应解决的问题也不同:隔离级别解决的问题未解决的问题实现核心技术(InnoDB)读未提交(RU)无脏读、不可重复读、幻读不加锁,直接读取未提交数据读已提交(RC)脏读不可重复读、幻读行锁(写锁)+ MVCC(每次查询生成 Read View)可重复读(RR)脏读、不可重复读部分幻读MVCC(事务启动生成 Read View)+ 行锁 + Next-Key Lock串行化(Serializable)脏读、不可重复读、幻读无表锁 / 行锁(事务排队执行)低隔离级别(如 RU)性能高但数据一致性弱,高隔离级别(如串行化)一致性强但性能低,InnoDB 默认选择 “可重复读”,平衡一致性与性能。二、InnoDB 各隔离级别的实现原理:从锁与 MVCC 切入InnoDB 实现隔离性的核心技术是MVCC(多版本并发控制) 和锁机制(行锁、间隙锁、表锁),不同隔离级别通过 “是否启用 MVCC、Read View 生成时机、锁的粒度” 来区分:1. 读未提交(Read Uncommitted):无隔离,直接读未提交数据实现逻辑:事务对数据的读取不加锁,也不使用 MVCC,直接读取数据的 “最新版本”—— 即使该版本由未提交的事务修改。问题:会出现脏读(读未提交的数据)、不可重复读(同一事务内两次读同一行,结果不同)、幻读(同一条件两次读,行数不同)。场景:仅用于对一致性无要求的临时查询(如日志统计),InnoDB 中极少使用。2. 读已提交(Read Committed):避免脏读,允许不可重复读实现逻辑:结合 “行锁” 和 “MVCC”,核心是每次查询生成新的 Read View(事务可见性视图,决定哪些版本的数据可见):写事务(UPDATE/DELETE/INSERT)对修改的行加 “行锁”,未提交前其他事务无法修改,但可通过 MVCC 读 “已提交的旧版本”;读事务每次执行 SELECT 时,生成新的 Read View,只能看到 “在 Read View 生成前已提交的事务版本”,看不到未提交的版本(避免脏读);但同一事务内两次查询生成不同 Read View,若中间有其他事务修改并提交,会看到不同结果(不可重复读)。示例:事务 A 第一次查行数据为 10,事务 B 修改为 20 并提交,事务 A 第二次查看到 20(不可重复读),但不会看到事务 B 未提交时的 20(无脏读)。3. 可重复读(Repeatable Read):InnoDB 默认,避免脏读、不可重复读,仍可能幻读这是 InnoDB 的重点,通过 **“事务启动时生成 Read View + MVCC + Next-Key Lock”** 实现,需分 “快照读” 和 “当前读” 两种场景分析:4. 串行化(Serializable):完全隔离,无并发干扰实现逻辑:对所有读写操作加锁,事务按 “先来后到” 排队执行:读事务(SELECT)加 “表级共享锁”,其他事务只能读,不能写;写事务(UPDATE/DELETE/INSERT)加 “表级排他锁”,其他事务既不能读也不能写;效果:完全避免脏读、不可重复读、幻读,但并发性能极低,仅用于强一致性需求(如财务对账)。三、InnoDB 可重复读(RR)的深度解析:如何避免脏读、不可重复读,却有幻读?InnoDB 的可重复读是 “MVCC 快照读” 与 “当前读锁机制” 的结合,需先明确两个关键概念:快照读:普通 SELECT(不加锁),通过 MVCC 读取历史版本数据,生成快照;当前读:加锁的 SELECT(如SELECT ... FOR UPDATE)、UPDATE、DELETE,读取数据的最新版本,并加锁防止其他事务修改。1. 避免脏读:MVCC 的版本可见性控制脏读是 “读取到其他事务未提交的修改”,InnoDB 通过 MVCC 的Read View 版本过滤避免:InnoDB 为每条数据维护 “版本链”:每次修改数据时,会生成新的版本,旧版本存入 undo 日志,新版本记录 “修改事务 ID” 和 “指向旧版本的指针”;事务启动时生成Read View,包含 “当前活跃事务 ID 列表” 和 “最大事务 ID”;读取数据时,MVCC 会对比数据版本的 “事务 ID” 与 Read View:若数据版本的事务 ID 在 Read View 的 “活跃列表” 中(未提交),则跳过该版本,读取更早的版本;若事务 ID 不在活跃列表中(已提交),则读取该版本;结论:未提交事务的版本会被过滤,永远看不到,因此避免脏读。2. 避免不可重复读:事务全程复用 Read View不可重复读是 “同一事务内,两次读同一行,结果不同(因其他事务修改并提交)”,InnoDB 通过 **“事务启动时生成 Read View,全程复用”** 避免:读已提交(RC)的问题是 “每次查询生成新 Read View”,导致中间提交的修改可见;可重复读(RR)中,Read View 在事务第一次执行 SELECT 时生成(或事务启动时,取决于配置),之后所有查询都复用这个 Read View;即使其他事务修改数据并提交,当前事务的 Read View 仍看不到新提交的版本(因新提交的事务 ID 大于 Read View 的 “最大事务 ID”),因此两次查询看到的是同一版本,避免不可重复读。举个栗子看看:事务 A 启动,第一次 SELECT 行数据为 10,生成 Read View(活跃事务:A,最大 ID:100);事务 B(ID:101)修改该行为 20 并提交;事务 A 第二次 SELECT,因事务 B 的 ID(101)大于 Read View 的最大 ID(100),仍读取旧版本 10,无不可重复读。3. 仍可能出现幻读:快照读与当前读的差异幻读是 “同一事务内,两次执行相同条件的查询,行数不同(因其他事务插入 / 删除数据)”,InnoDB 的可重复读对快照读避免幻读,但对当前读仍可能出现幻读:(1)快照读:避免幻读普通 SELECT(快照读)时,因复用 Read View,即使其他事务插入新行并提交,新行的 “事务 ID” 大于当前事务的 Read View 最大 ID,会被过滤,因此两次查询行数相同,无幻读。示例:事务 A 启动,执行SELECT * FROM user WHERE age > 30,返回 2 行,生成 Read View(最大事务 ID:100);事务 B(ID:101)插入 1 条 age=35 的记录并提交;事务 A 再次执行相同 SELECT,因事务 B 的 ID(101)大于 Read View 的最大 ID(100),新插入的记录被过滤,仍返回 2 行,无幻读。(2)当前读:仍可能出现幻读当前读(如SELECT ... FOR UPDATE、UPDATE)会读取最新版本,并加锁,此时若其他事务插入 “符合查询条件” 的新行,会导致 “行数增加”,出现幻读:经典幻读场景:事务 A 执行SELECT * FROM user WHERE age > 30 FOR UPDATE,返回 2 行(ID=1、2),并对这两行加行锁,同时通过Next-Key Lock锁定 “age>30” 的间隙(防止插入);事务 B 尝试插入age=35的记录,因间隙锁被阻塞,无法插入(InnoDB 的 Next-Key Lock 减轻了幻读,但不是完全杜绝);若事务 B 插入的记录 “刚好在间隙外,但查询条件调整后符合”(或 Next-Key Lock 未覆盖的间隙),比如:事务 A 先执行SELECT * FROM user WHERE age BETWEEN 20 AND 30 FOR UPDATE,锁定 20-30 的间隙;事务 B 插入age=31的记录(不在 20-30 间隙内,可插入);事务 A 再执行SELECT * FROM user WHERE age > 20 FOR UPDATE,会读到事务 B 插入的 31 行,出现幻读;本质原因:当前读读取最新数据,且锁无法覆盖 “所有可能的查询条件”,若其他事务插入 “新的符合条件的行”,则会出现行数增加,即幻读。4. Next-Key Lock:减轻幻读的锁机制InnoDB 在可重复读级别下,对 “范围查询的当前读” 会使用Next-Key Lock(行锁 + 间隙锁),锁定 “满足条件的行” 和 “行之间的间隙”,防止其他事务在间隙中插入数据,从而减轻幻读:例如SELECT * FROM user WHERE id > 10 FOR UPDATE,会锁定 “id>10 的所有行”,以及 “id=10 到最大 id 的间隙”,其他事务无法在该间隙插入 id>10 的行;但 Next-Key Lock 无法覆盖 “所有可能的查询条件”(如动态调整的范围),因此仍可能出现极端场景的幻读,只是概率极低。四、总结一下下:InnoDB 隔离级别的核心逻辑隔离级别脏读不可重复读幻读核心实现(InnoDB)读未提交(RU)有有有无锁,直接读最新版本读已提交(RC)无有有行锁 + MVCC(每次查询生成 Read View)可重复读(RR)无无可能有MVCC(事务复用 Read View)+ Next-Key Lock串行化(Serializable)无无无表锁 / 行锁,事务排队InnoDB 默认的可重复读,通过 MVCC 快照读保障 “事务内数据一致性”,避免脏读和不可重复读;通过 Next-Key Lock 减少当前读的幻读风险,但因 “当前读需读最新数据”,仍无法完全杜绝幻读(需串行化级别才能彻底避免),这是 “一致性” 与 “并发性能” 的权衡结果。
总条数:1672 到第
上滑加载中