0%

分布式表连接问题

Mycat提供了几种方式来解决表连接问题

使用全局表

全局表是一种冗余的表,每个节点都是全量的数据,有以下特性

  • 全局表的插入、更新操作会实时在所有节点上执行,保持各个分片的数据一致性
  • 全局表的查询操作,只从一个节点获取
  • 全局表可以跟任何一个表进行join操作

在配置表的时候加上global就是全局表

1
<table name="company" primaryKey="ID" type="global" dataNode="dn1,dn2,dn3">
阅读全文 »

数据库密码错误引发连接风暴:Druid 配置防护与故障排查

数据库连接是应用的核心依赖,一旦密码配置错误,可能引发连锁反应:连接池持续重试、数据库连接耗尽、应用响应超时。本文将深入分析密码错误导致的连接风暴原理,详解如何通过 Druid 配置防护,并提供故障排查与应急处理方案,帮助你避免类似问题。

密码错误引发连接风暴的原理

问题现象

当数据库密码配置错误时,应用会出现以下症状:

  • 页面加载缓慢(十几秒甚至超时);
  • 数据库 show full processlist 显示大量 Connect 状态的连接(来自应用服务器 IP);
  • 应用日志频繁出现 Access denied for user 'xxx'@'xxx' (using password: YES) 错误;
  • 连接池 activeCount 持续飙升,最终达到 maxActive 上限。

底层原因分析

密码错误触发了连接池的无限重试机制,形成恶性循环:

  1. 初始连接失败:应用启动或首次获取连接时,因密码错误被数据库拒绝,抛出认证异常;
  2. 连接池重试:默认配置下,Druid 会重试连接(默认重试 1 次),若仍失败,会不断尝试创建新连接;
  3. 数据库连接堆积:每次重试都会向数据库发起 Connect 请求,即使认证失败,数据库仍需消耗资源处理请求,导致连接队列拥堵;
  4. 连接池耗尽:大量失败的连接请求占用连接池资源,正常请求无法获取连接,形成 “雪崩效应”。

为何影响其他项目?

若多个项目共享同一数据库,一个项目的密码错误重试会占用数据库的连接资源(如 MySQL 默认 max_connections=151),导致其他项目的正常连接请求被阻塞,出现 “一损俱损” 的连锁反应。

Druid 关键配置:限制重试与熔断

Druid 提供了连接错误重试和熔断配置,可有效避免密码错误引发的连接风暴。核心配置包括:

连接错误重试次数(connectionErrorRetryAttempts

  • 作用:设置连接失败后的最大重试次数,超过次数后停止重试;
  • 默认值:1(仅重试 1 次);
  • 最佳实践:建议设为 0-2(减少无效重试)。
阅读全文 »

HBase协处理器详解:原理、实践与加载方式

HBase 协处理器(Coprocessor)是 HBase 提供的高级特性,允许用户在 RegionServer 或 Master 节点上运行自定义代码,实现类似 “触发器” 或 “存储过程” 的功能。本文将深入解析协处理器的类型、使用场景、实战示例及加载方式,帮助开发者扩展 HBase 的功能边界。

协处理器核心概念

协处理器的设计目标是将计算逻辑下沉到数据存储节点,减少数据传输开销,同时扩展 HBase 的原生功能(如触发器、聚合计算等)。其核心价值在于:

  • 靠近数据计算:避免全表扫描后的数据传输,提升计算效率;
  • 功能扩展:实现 HBase 原生不支持的特性(如数据同步、自定义权限校验)。

协处理器类型

HBase 协处理器分为 ObserverEndpoint 两大类,适用场景不同:

Observer 协处理器(触发器模式)

Observer 类似数据库的 “触发器”,在特定事件发生时自动执行,如数据写入、表结构修改等。根据监听对象不同,分为以下三类:

类型 监听对象 典型场景
RegionObserver Region 级事件(如 Put、Get、Delete、Scan) 数据写入校验、二级索引同步、数据脱敏
MasterObserver Master 级事件(如建表、删表、Region 拆分) 表结构权限控制、DDL 操作审计
WALObserver 预写日志(WAL)事件(如日志写入、同步) 日志加密、自定义 WAL 存储

Endpoint 协处理器(存储过程模式)

Endpoint 类似 “存储过程”,允许通过自定义 RPC 方法扩展 HBase 的 API,支持在服务端执行聚合计算(如 COUNTSUM),减少客户端与服务端的数据传输。

阅读全文 »

HBase客户端API实战指南:从基础操作到高级功能

HBase 提供了丰富的 Java 客户端 API,用于实现数据的增删改查、过滤器查询和计数器等功能。本文基于实战场景,详细讲解 HBase 客户端 API 的核心用法,包括连接管理、数据操作、过滤器使用及计数器功能,帮助开发者快速上手。

环境准备与连接管理

在使用 HBase API 前,需确保项目引入 HBase 依赖(以 Maven 为例),并正确配置 HBase 连接信息(hbase-site.xml 需放在项目 classpath 下)。

依赖引入

1
2
3
4
5
<dependency>  
<groupId>org.apache.hbase</groupId>
<artifactId>hbase-client</artifactId>
<version>2.2.7</version> <!-- 与 HBase 集群版本一致 -->
</dependency>

连接初始化

HBase 客户端通过 Connection 对象管理与集群的连接,Admin 用于 DDL 操作,Table 用于 DML 操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import org.apache.hadoop.hbase.client.Admin;  
import org.apache.hadoop.hbase.client.Connection;
import org.apache.hadoop.hbase.client.ConnectionFactory;
import org.apache.hadoop.hbase.client.Table;
import org.apache.hadoop.hbase.TableName;

public class HBaseClient {
// 全局连接对象(线程安全,推荐单例)
private static Connection conn;
// DDL 操作对象
private static Admin admin;
// DML 操作对象(针对 test 表)
private static Table table;

static {
try {
// 从配置文件加载连接信息(hbase-site.xml)
conn = ConnectionFactory.createConnection();
admin = conn.getAdmin();
// 指定操作的表名(需提前创建)
table = conn.getTable(TableName.valueOf("test"));
} catch (IOException e) {
e.printStackTrace();
}
}
}

核心数据操作 API

插入数据(Put)

使用 Put 类向表中插入或更新数据,需指定 RowKey、列族、列名和值(均为字节数组)。

方法实现
阅读全文 »

EasyExcel合并单元格:基于 AbstractMergeStrategy 的灵活实现

在生成 Excel 报表时,合并单元格是优化表格可读性的常用手段(如合并相同值的相邻单元格)。EasyExcel 并未直接提供合并单元格的 API,但通过其钩子机制(尤其是 AbstractMergeStrategy 抽象类),可灵活实现自定义合并逻辑。本文将详细讲解如何基于 AbstractMergeStrategy 实现单元格合并,并通过实例演示 “相同值自动合并” 的功能。

合并单元格的核心思路

EasyExcel 合并单元格的核心是通过 单元格钩子(CellWriteHandler) 监听单元格创建过程,在数据写入时动态判断是否需要合并。具体步骤:

  1. 记录当前单元格的值及其所在行索引;
  2. 与上一行同列单元格的值对比,若相同则标记为待合并区域;
  3. 当值发生变化或到达最后一行时,执行合并操作(通过 POI 的 Sheet.addMergedRegion 方法)。

AbstractMergeStrategy 是 EasyExcel 提供的合并策略抽象类,实现了 CellWriteHandler 接口,简化了合并逻辑的开发(只需重写 merge 方法)。

自定义合并策略:相同值自动合并

以下实现一个通用的合并策略:对指定字段(列),自动合并相邻的相同值单元格。

实现自定义合并策略类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
import com.alibaba.excel.metadata.Head;  
import com.alibaba.excel.write.merge.AbstractMergeStrategy;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellRangeAddress;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
* 自定义合并策略:自动合并指定字段的相邻相同值单元格
*/
public class SameValueMergeStrategy extends AbstractMergeStrategy {

// 需要合并的字段名(与实体类的 @ExcelProperty 字段对应)
private final List<String> mergeFieldNames;
// 数据总条数(用于判断是否为最后一行)
private final int totalRowCount;

// 存储上一行的数据:key=字段名,value=Pair(值, 行索引)
private final Map<String, RowData> lastRowDataMap = new HashMap<>();

// 内部类:存储行数据(值和行索引)
private static class RowData {
Object value; // 单元格值
int rowIndex; // 行索引(从 0 开始)

RowData(Object value, int rowIndex) {
this.value = value;
this.rowIndex = rowIndex;
}
}

public SameValueMergeStrategy(List<String> mergeFieldNames, int totalRowCount) {
this.mergeFieldNames = mergeFieldNames;
this.totalRowCount = totalRowCount;
}

@Override
protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) {
// relativeRowIndex:相对行索引(0 表示表头行,1 开始为数据行)
// 跳过表头行(只处理数据行)
if (relativeRowIndex == null || relativeRowIndex < 1) {
return;
}

// 当前数据行的绝对索引(0 开始,表头行占 1 行)
int currentRowIndex = relativeRowIndex;
// 当前字段名(从 Head 中获取)
String currentFieldName = head.getFieldName();

// 仅处理需要合并的字段
if (mergeFieldNames.contains(currentFieldName)) {
// 获取当前单元格的值
Object currentValue = getCellValue(cell);

// 从缓存中获取上一行同字段的数据
RowData lastRowData = lastRowDataMap.get(currentFieldName);

if (lastRowData == null) {
// 首次处理该字段,直接缓存当前行数据
lastRowDataMap.put(currentFieldName, new RowData(currentValue, currentRowIndex));
} else {
// 对比当前值与上一行值
if (!Objects.equals(currentValue, lastRowData.value)) {
// 值不同:合并上一段相同值的单元格(如果行数 >1)
mergeIfNeeded(sheet, lastRowData, currentRowIndex - 1, cell.getColumnIndex());
// 更新缓存为当前行数据
lastRowDataMap.put(currentFieldName, new RowData(currentValue, currentRowIndex));
} else if (currentRowIndex == totalRowCount) {
// 值相同且为最后一行:合并到最后一行
mergeIfNeeded(sheet, lastRowData, currentRowIndex, cell.getColumnIndex());
}
}
}
}

/**
* 执行合并操作(如果需要)
* @param sheet 工作表
* @param lastRowData 上一行数据
* @param endRowIndex 结束行索引
* @param columnIndex 列索引
*/
private void mergeIfNeeded(Sheet sheet, RowData lastRowData, int endRowIndex, int columnIndex) {
int startRowIndex = lastRowData.rowIndex;
// 只有当行数差 >0 时才合并(至少 2 行)
if (endRowIndex - startRowIndex > 0) {
CellRangeAddress mergeRegion = new CellRangeAddress(
startRowIndex, // 起始行
endRowIndex, // 结束行
columnIndex, // 起始列
columnIndex // 结束列(同列)
);
// 添加合并区域
sheet.addMergedRegion(mergeRegion);
}
}

/**
* 获取单元格的值(兼容不同数据类型)
*/
private Object getCellValue(Cell cell) {
if (cell == null) {
return "";
}
CellType cellType = cell.getCellType();
switch (cellType) {
case STRING:
return cell.getStringCellValue();
case NUMERIC:
// 处理数字和日期(简化处理,实际可根据需求优化)
return cell.getNumericCellValue();
case BOOLEAN:
return cell.getBooleanCellValue();
default:
return "";
}
}
}

实体类定义

假设需要合并 “部门” 列,实体类如下:

阅读全文 »