java
package org.apache.ignite.examples.streaming.wordcount;
import org.apache.ignite.Ignite;
import org.apache.ignite.IgniteCache;
import org.apache.ignite.Ignition;
import org.apache.ignite.cache.affinity.AffinityUuid;
import org.apache.ignite.cache.query.SqlFieldsQuery;
import org.apache.ignite.configuration.CacheConfiguration;
import org.apache.ignite.examples.ExampleNodeStartup;
import org.apache.ignite.examples.ExamplesUtils;
import org.apache.ignite.examples.IgniteConstant;
import java.util.List;
/**
* Periodically query popular numbers from the streaming cache.
* To start the example, you should:
* <ul>
* <li>Start a few nodes using {@link ExampleNodeStartup}.</li>
* <li>Start streaming using {@link StreamWords}.</li>
* <li>Start querying popular words using {@link QueryWords}.</li>
* </ul>
*/
public class QueryWords {
/**
* Schedules words query execution.
*
* @param args Command line arguments (none required).
* @throws Exception If failed.
*/
public static void main(String[] args) throws Exception {
// Mark this cluster member as client.
Ignition.setClientMode(true);
try (Ignite ignite = Ignition.start(IgniteConstant.IGNITE_CONFIG_LOCATION)) {
if (!ExamplesUtils.hasServerNodes(ignite))
return;
CacheConfiguration<AffinityUuid, String> cfg = CacheConfig.wordCache();
// The cache is configured with sliding window holding 1 second of the streaming data.
try (IgniteCache<AffinityUuid, String> stmCache = ignite.getOrCreateCache(cfg)) {
// Select top 10 words.
SqlFieldsQuery top10Qry = new SqlFieldsQuery(
"select _val, count(_val) as cnt from String group by _val order by cnt desc limit 10",
true /*collocated*/
);
// Select average, min, and max counts among all the words.
SqlFieldsQuery statsQry = new SqlFieldsQuery(
"select avg(cnt), min(cnt), max(cnt) from (select count(_val) as cnt from String group by _val)");
// Query top 10 popular numbers every 5 seconds.
while (true) {
// Execute queries.
List<List<?>> top10 = stmCache.query(top10Qry).getAll();
List<List<?>> stats = stmCache.query(statsQry).getAll();
// Print average count.
List<?> row = stats.get(0);
if (row.get(0) != null)
System.out.printf("Query results [avg=%d, min=%d, max=%d]%n",
row.get(0), row.get(1), row.get(2));
// Print top 10 words.
ExamplesUtils.printQueryResults(top10);
Thread.sleep(5000);
}
}
finally {
// Distributed cache could be removed from cluster only by #destroyCache() call.
ignite.destroyCache(cfg.getName());
}
}
}
}
这段代码是 Apache Ignite 流式处理示例中的消费者部分 ,对应类名为 QueryWords
。它的作用是:周期性地从一个正在接收单词流的缓存中查询"当前最流行的单词"及其统计信息。
我们可以把它理解为:一个实时仪表盘(dashboard),每隔 5 秒钟就去查看一下最近 1 秒内哪些单词出现得最多。
我们来逐段分析并解释其含义和逻辑。
✅ 1. 类注释说明用途
java
/**
* Periodically query popular numbers from the streaming cache.
* To start the example, you should:
* <ul>
* <li>Start a few nodes using {@link ExampleNodeStartup}.</li>
* <li>Start streaming using {@link StreamWords}.</li>
* <li>Start querying popular words using {@link QueryWords}.</li>
* </ul>
*/
🔍 解读:
- 这是一个 三步流程的第三步 :
ExampleNodeStartup
:启动多个 Ignite Server 节点,组成集群。StreamWords
:启动客户端,不断向集群发送《爱丽丝梦游仙境》中的单词(流式数据)。QueryWords
:启动另一个客户端,定期查询当前最热门的单词。
📌 这三个程序可以运行在不同机器或进程中,共同构成一个完整的 分布式流处理系统。
✅ 2. main
方法入口
java
public static void main(String[] args) throws Exception {
标准 Java 主函数。
✅ 3. 设置为客户端模式
java
Ignition.setClientMode(true);
- 表示本节点不存储数据,仅作为"查询客户端"连接到集群。
- 和
StreamWords
一样,它只是访问远程缓存。
✅ 4. 启动 Ignite 实例并连接集群
java
try (Ignite ignite = Ignition.start(IgniteConstant.IGNITE_CONFIG_LOCATION)) {
- 加载配置文件(如 XML),连接已存在的 Ignite 集群。
- 使用 try-with-resources 自动关闭资源。
✅ 5. 检查是否有服务端节点
java
if (!ExamplesUtils.hasServerNodes(ignite))
return;
- 如果集群中没有 Server 节点,则退出。
- 防止在空集群上执行查询。
✅ 6. 获取缓存配置,并创建缓存
java
CacheConfiguration<AffinityUuid, String> cfg = CacheConfig.wordCache();
try (IgniteCache<AffinityUuid, String> stmCache = ignite.getOrCreateCache(cfg)) {
- 使用与
StreamWords
相同的缓存配置(wordCache()
),确保连接的是同一个缓存。 - 缓存名为
String
(见下面 SQL 查询),类型为<AffinityUuid, String>
。 - 即使这个缓存已经在集群中存在,
getOrCreateCache
也能正确获取它。
💡 提示:这个缓存被配置为 滑动时间窗口(sliding window),只保留最近 1 秒的数据。也就是说,每次查询的结果反映的是"过去 1 秒内最频繁出现的单词"。
✅ 7. 定义两个 SQL 查询
🟢 查询 1:获取 Top 10 最频繁单词
java
SqlFieldsQuery top10Qry = new SqlFieldsQuery(
"select _val, count(_val) as cnt from String group by _val order by cnt desc limit 10",
true /*collocated*/
);
from String
:这里的String
是缓存名(Cache Name),不是 Java 类型。_val
:Ignite 中表示缓存条目的 值字段(即单词本身)。count(_val)
:统计每个单词出现的次数。group by _val
:按单词分组。order by cnt desc
:降序排列。limit 10
:只取前 10 个。true /*collocated*/
:启用本地执行优化,表示这个查询会在数据所在的节点上并行执行,最后汇总结果,提升性能。
⚡ 原理:Ignite 会将查询分发到各个节点,在本地进行聚合(map-reduce 思想),然后合并结果。
🟡 查询 2:获取所有单词计数的统计值
java
SqlFieldsQuery statsQry = new SqlFieldsQuery(
"select avg(cnt), min(cnt), max(cnt) from (select count(_val) as cnt from String group by _val)");
- 内层查询:对每个单词统计出现次数 → 得到一组
(word, count)
。 - 外层查询:计算这些
count
值的平均值、最小值、最大值。 - 输出三个指标:
- 平均每个单词出现了多少次?
- 出现最少的单词次数?
- 出现最多的单词次数?
📊 用途:监控整体流量波动、识别热点词。
✅ 8. 主循环:每 5 秒执行一次查询
java
while (true) {
// 执行查询
List<List<?>> top10 = stmCache.query(top10Qry).getAll();
List<List<?>> stats = stmCache.query(statsQry).getAll();
// 打印统计信息
List<?> row = stats.get(0);
if (row.get(0) != null)
System.out.printf("Query results [avg=%d, min=%d, max=%d]%n",
row.get(0), row.get(1), row.get(2));
// 打印 Top 10 单词
ExamplesUtils.printQueryResults(top10);
Thread.sleep(5000); // 每隔 5 秒查一次
}
🔍 重点说明:
-
stmCache.query(...).getAll()
:同步执行 SQL 查询,返回所有结果行。 -
每次查询的是 当前缓存中的数据 ------ 因为缓存是"滑动窗口",所以实际上查的是"最近 1 秒内的单词流"。
-
输出示例可能像这样:
Query results [avg=3, min=1, max=15]
+-------+----+
| _val | cnt|
+-------+----+
| the | 15 |
| and | 12 |
| to | 10 |
| a | 9 |
| of | 8 |
| ... | .. |
+-------+----+ -
Thread.sleep(5000)
:每 5 秒查询一次,形成"实时监控"的效果。
✅ 9. finally 块:销毁缓存
java
finally {
ignite.destroyCache(cfg.getName());
}
- 在程序退出时,显式销毁缓存。
- 注意:只有通过
destroyCache()
才能真正从集群中删除分布式缓存。 - ⚠️ 但这里有个问题:如果其他程序还在使用这个缓存,销毁会导致它们出错。所以这个操作通常仅用于示例或测试环境。
🛑 生产环境中一般不会随意销毁缓存。
🧠 整体逻辑总结
步骤 | 动作 | 目的 |
---|---|---|
1 | 设置客户端模式 | 不参与数据存储 |
2 | 连接集群 | 访问共享缓存 |
3 | 获取/创建缓存 | 确保能访问 wordCache |
4 | 定义 SQL 查询 | 分析流数据 |
5 | 每 5 秒执行一次查询 | 实时监控 |
6 | 输出 Top 10 和统计值 | 展示结果 |
7 | 销毁缓存(示例用) | 清理资源 |
📈 典型应用场景
这正是一个典型的 实时流数据分析系统 的消费者端:
- 场景:实时监控日志、用户行为、传感器数据等。
- 功能 :
- 查看当前热门词汇(可用于舆情监控)
- 统计流量分布(用于负载分析)
- 构建可视化仪表板
⚠️ 注意事项 & 改进建议
问题 | 建议 |
---|---|
while(true) |
应支持中断信号(如 Ctrl+C )优雅关闭 |
destroyCache() |
示例可用,生产环境慎用 |
SQL 查询未参数化 | 当前没问题,但复杂查询建议封装 |
编码假设 | 确保集群启用 SQL 查询引擎(INDEXING) |
缓存生命周期 | 滑动窗口需确认 TTL 设置正确 |
🔄 与 StreamWords
的关系图解
[ StreamWords.java ] [ QueryWords.java ]
↓ ↑
发送单词流 (DataStreamer) 定期执行 SQL 查询
↓ ↑
┌───────────────────────┐
│ Ignite Cluster │
│ Cache: "String" │ ← 滑动窗口(1秒)
│ Key: AffinityUuid │
│ Value: word (String) │
└───────────────────────┘
↑
[ Server Nodes 存储数据 ]
StreamWords
是生产者(Producer)QueryWords
是消费者(Consumer)- Ignite 缓存是中间的"消息+计算平台"
✅ 总结一句话:
QueryWords
是一个 实时查询客户端 ,它周期性地从 Ignite 的流式缓存中执行 SQL 聚合查询,展示当前最流行的单词和统计信息,构成了一个简单的 实时词频分析系统。
如果你想进一步扩展功能,比如:
- 将结果写入数据库或前端展示
- 使用 Continuous Query 实现事件驱动
- 添加时间窗口聚合(如每 10 秒统计)
✅ 10. 缓存名称为啥为String呢?
这是一个非常好的问题!
你说得对,CacheConfig.wordCache()
方法中创建的缓存名称是 "words"
:
java
CacheConfiguration<AffinityUuid, String> cfg = new CacheConfiguration<>("words");
但我们在前面 QueryWords
类的 SQL 查询中却写的是:
sql
select _val, count(_val) as cnt from String group by _val
这里用的是表名 String
,而不是缓存名 words
。这看起来矛盾了,但实际上 这是 Ignite SQL 引擎的默认映射机制导致的。
✅ 正确答案:缓存名 ≠ SQL 表名
在 Apache Ignite 中:
- 缓存名称(Cache Name) :是 Java 层面用于获取缓存的标识符,比如
ignite.getOrCreateCache("words")
。 - SQL 表名(SQL Table Name) :是 SQL 查询时使用的名称,默认情况下 不是缓存名 ,而是根据缓存配置中的
indexedTypes
自动生成的。
🔍 为什么 SQL 中能用 String
?
关键在于这行代码:
java
cfg.setIndexedTypes(AffinityUuid.class, String.class);
Ignite 的 SQL 映射规则如下:
当你调用 setIndexedTypes(K.class, V.class)
时,Ignite 会自动为这个缓存生成一个或多个 SQL 表(或类型),其命名规则是:
表名 = 值类型的简单类名(Simple Class Name)
所以:
值类型(Value Type) | 默认生成的 SQL 表名 |
---|---|
String.class |
String |
Integer.class |
Integer |
Person.class |
Person |
因此:
✅ 虽然缓存名叫 "words"
,
✅ 但它索引的是 String
类型的值,
✅ 所以 Ignite 自动生成了一个名为 String
的 SQL 表来表示这些字符串值。
📌 验证方式:你可以打印出所有可用的 SQL 表
在代码中添加:
java
for (String cacheName : ignite.cacheNames()) {
System.out.println("Cache: " + cacheName);
}
// 打印所有 SQL 表
SqlFieldsQuery tablesQry = new SqlFieldsQuery("SELECT schema_name, table_name FROM INFORMATION_SCHEMA.TABLES WHERE table_name LIKE 'STRING'");
List<List<?>> tables = ignite.context().query().querySqlFields(tablesQry, true).getAll();
tables.forEach(System.out::println);
你可能会看到输出类似:
[PUBLIC, STRING]
说明确实存在一张叫 STRING
的表(默认大写)。
✅ 如何让 SQL 表名等于缓存名?
如果你希望 SQL 查询使用缓存名(比如 words
),你可以 显式指定 SQL 表名。
✅ 方法一:使用 @QuerySqlField
注解 + 自定义类型(推荐用于 POJO)
但你现在的值类型是 String
,不能加注解。
✅ 方法二:手动设置 QueryEntity
来控制 SQL 映射
修改 wordCache()
方法:
java
public static CacheConfiguration<AffinityUuid, String> wordCache() {
CacheConfiguration<AffinityUuid, String> cfg = new CacheConfiguration<>("words");
// 手动定义查询实体
QueryEntity qryEntity = new QueryEntity();
qryEntity.setKeyType(AffinityUuid.class.getName());
qryEntity.setValueType("WordValue"); // 自定义类型名
qryEntity.setTableName("words"); // 指定 SQL 表名为 words
qryEntity.addQueryField("_val", String.class.getName(), null);
qryEntity.setKeyFieldName(null);
qryEntity.setValueFieldName("_val"); // 指定值字段名为 _val
cfg.setQueryEntities(Collections.singletonList(qryEntity));
// 设置过期策略:1秒滑动窗口
cfg.setExpiryPolicyFactory(FactoryBuilder.factoryOf(new CreatedExpiryPolicy(new Duration(SECONDS, 1))));
return cfg;
}
这样之后,你就可以用下面的 SQL 查询了:
sql
SELECT _val, COUNT(_val)
FROM words
GROUP BY _val
ORDER BY COUNT(_val) DESC
LIMIT 10
✅ 表名变成了
words
,更直观、易维护。
❌ 为什么原示例用 String
是"有风险"的?
- 歧义性高 :系统中如果有多个缓存都存
String
,都会映射到同一个String
表,造成冲突。 - 不可控:表名由值类型决定,不利于设计清晰的数据模型。
- 难调试 :新人看不懂为什么缓存叫
words
,SQL 却查String
。
✅ 最佳实践建议
建议 | 说明 |
---|---|
✅ 避免直接索引基础类型(如 String、Integer) | 改用自定义类,如 WordEntry |
✅ 显式设置 QueryEntity 和 tableName |
控制 SQL 表名 |
✅ 使用有意义的表名 | 如 words , events , logs |
✅ 缓存名与 SQL 表名尽量一致 | 提高可读性和可维护性 |
推荐重构示例:
java
public class WordEntry {
@QuerySqlField(index = true)
private String word;
// 构造函数、getter/setter...
}
然后缓存配置为:
java
cfg.setName("words");
cfg.setIndexedTypes(AffinityUuid.class, WordEntry.class);
SQL 查询变为:
sql
SELECT word, COUNT(*) FROM WordEntry GROUP BY word ORDER BY COUNT(*) DESC LIMIT 10
更清晰、更安全、更专业。
✅ 总结
❓ 为什么 SQL 能查
from String
?✔ 因为
setIndexedTypes(..., String.class)
让 Ignite 自动生成了一张名为String
的 SQL 表,对应缓存中的字符串值。
❓ 但缓存名叫"words"
啊?✔ 是的,缓存名和 SQL 表名是两个不同的概念,Ignite 默认不一致。
❓ 这样好吗?❌ 不好!这是示例代码的简化写法,不适合生产环境。
✅ 正确做法:使用QueryEntity
显式定义表名,或封装成 POJO 类。
如果你想,我可以帮你把整个 StreamWords
和 QueryWords
示例重构为使用自定义类型 + 明确 SQL 表名的版本。需要吗?