Apache Ignite Data Streaming 案例 QueryWords

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>
 */
🔍 解读:
  • 这是一个 三步流程的第三步
    1. ExampleNodeStartup:启动多个 Ignite Server 节点,组成集群。
    2. StreamWords:启动客户端,不断向集群发送《爱丽丝梦游仙境》中的单词(流式数据)。
    3. 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 是"有风险"的?
  1. 歧义性高 :系统中如果有多个缓存都存 String,都会映射到同一个 String 表,造成冲突。
  2. 不可控:表名由值类型决定,不利于设计清晰的数据模型。
  3. 难调试 :新人看不懂为什么缓存叫 words,SQL 却查 String

✅ 最佳实践建议
建议 说明
✅ 避免直接索引基础类型(如 String、Integer) 改用自定义类,如 WordEntry
✅ 显式设置 QueryEntitytableName 控制 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 类。


如果你想,我可以帮你把整个 StreamWordsQueryWords 示例重构为使用自定义类型 + 明确 SQL 表名的版本。需要吗?

相关推荐
lang201509282 小时前
使用 Docker 部署 Apache RocketMQ
docker·apache·rocketmq
吃不得辣条3 小时前
网络安全之防火墙
网络·web安全·apache
lang201509285 小时前
Apache RocketMQ中 Consumer Group(消费者组)的详细说明
apache·rocketmq
微学AI20 小时前
时序数据库选型指南:工业大数据场景下基于Apache IoTDB技术价值与实践路径
大数据·apache·时序数据库
sibylyue21 小时前
Apache HttpClient HTTP 线程池参数设置
网络协议·http·apache
lang2015092821 小时前
Apache RocketMQ 中 Topic 的概念、属性、行为约束和最佳实践
apache·rocketmq
沈健_算法小生1 天前
Apache Kafka核心组件详解
分布式·kafka·apache
lang201509282 天前
Apache Ignite 的对等类加载(Peer Class Loading, P2P Class Loading)机制
apache·ignite
chen_note2 天前
LAMP及其环境的部署搭建
linux·运维·mysql·php·apache·lamp·phpmyadmin