Presto 是一款快速的、稳定的 SQL 查询引擎。
上面的图片来自于 Presto 官网,Presto 将组件划分为三层:
底层:存储结构化数据的数据源,数据源来自于关系型数据库、非关系型数据库、OLAP、分布式数据库、消息中间件、HDFS 以及各种云存储;
中间层:Presto,支持异构数据源的查询以及异构数据源之间的联合查询;
应用层:借助于 Presto 查询引擎,在复杂的异构数据源中操作数据构建上层应用。
统计接口调用次数:未使用 Presto
在之前的项目中为了计算每天用户调用接口的次数,使用 Redis 累加调用次数,用户基本信息存储在 MySQL 中,下面是实际的实现。
MySQL 中的 user 表:
Redis 中的 key 设计:
调用接口总次数:api_tag_count
每天调用接口次数:api_tag_count_20230812
用户调用接口总次数:user_1_api_tag_count
用户每天调用接口次数:user_1_api_tag_count_20230812
使用 Jedis 客户端累加接口调用次数:
/** * 计算接口调用的次数:总次数/每天总次数/用户调用次数/每天用户调用次数 * * @param jedis jedis客户端 * @param api api接口名称 * @param user 用户名 * @param day 时间精确到天:yyyyMMdd */private void incr(Jedis jedis, String api, String user, String day) { // 声明事务 Transaction transaction = jedis.multi();
// 接口调用总次数 String apiCount = "api_" + api + "_count"; transaction.incr(apiCount); // 每天接口调用次数 transaction.incr(apiCount + "_" + day);
// 用户调用接口总量 String userApiCount = "user_" + user + "_" + apiCount; transaction.incr(userApiCount); // 每天用户调用接口总量 transaction.incr(userApiCount + "_" + day);
// 事务执行 transaction.exec();}
复制代码
实际应用:管理员可以按照时间、用户查询接口的调用次数
@Datapublic class User { private int id; private String name; private String address; private int age; private String company; private Date created; private Date updated;}
public interface UserDao { User user(int id);
List<User> users(User filer);
int add(int id);
boolean delete(int id);}
@Data@AllArgsConstructorpublic class ApiStats { private int userId; private String api; private String day; private int count;}
/** * 按照用户id、接口名称、日期查询调用次数,同时返回用户详情 * * @param userId 用户id * @param api 接口名称 * @param day 时间:yyyyMMdd * @return 用户详情和接口调用次数 */public Pair<User, ApiStats> query(int userId, String api, String day) { // 1. 获取用户 User user = userDao.user(userId); if (user == null) { throw new IllegalArgumentException("Not found userId: " + userId); }
// 2. 构建 redis key String apiKey = new StringBuilder(64) .append("user_").append(userId) .append("_api_").append(api) .append("_").append(day) .toString();
// 3. 获取调用次数 int count = 0; String value = jedis.get(apiKey); if(StringUtils.isNotEmpty(value)){ try{ count = Integer.parseInt(value); }catch (Exception e){ throw new IllegalStateException("Parsed redis value error: id=" + userId + ", value=" + value, e); }
if(count < 0){ throw new IllegalStateException("Invalid api count: id=" + userId + ", value=" + value); } }
// 4. 构建返回结果 ApiStats apiStats = new ApiStats(userId, api, day, count); return Pair.of(user, apiStats);}
复制代码
上面实现的代价:
创建 User、ApiStats 实体类;
使用 mybatis 框架构建 userDao;
使用 userDao 获取用户详情;
使用 jedis 客户端获取调用次数。
如果我们使用 Presto 实现会变的简单些吗 ?
使用 Presto 实现
1. 配置 MySQL Catalog
配置后可通过 mysql.ice.user 访问 user 表。
cat $PRESTO_HOME/etc/catalog/mysql.propertiesconnector.name=mysqlconnection-url=jdbc:mysql://192.168.1.108:3306connection-user=iceconnection-password=配置你的密码
复制代码
2. 配置 Redis Catalog
你需要配置虚拟的表名,完整的表名为 redis.schema1.api,Presto 内置了映射的列,_key 对应 Redis 中实际的键,_value 对应键的值,更详细的可参考:https://prestodb.io/docs/current/connector/redis.html。
cat $PRESTO_HOME/etc/catalog/redis.properties
connector.name=redisredis.table-names=schema1.apiredis.nodes=192.168.1.108:6379
复制代码
在 Worker 节点验证下配置的是否正确:
3. 编写实现代码
pom.xml 中加入 presto-jdbc:
<dependency> <groupId>com.facebook.presto</groupId> <artifactId>presto-jdbc</artifactId> <version>0.282</version></dependency>
复制代码
编写实现服务:ApiStatsService,该服务可通过用户 id、接口名称和日期查询调用次数。
import lombok.RequiredArgsConstructor;import java.sql.Connection;import java.sql.DriverManager;import java.sql.ResultSet;import java.sql.Statement;@RequiredArgsConstructorpublic class ApiStatsService { private static final String SQL_FORMAT = "SELECT %s, t2._value count \n" + " FROM mysql.ice.user t1, redis.schema1.api t2 \n" + " WHERE t1.id = %s \n" + " AND t2._key = concat('user_', cast(t1.id as varchar), '_api_', '%s', '_', '%s')"; private final Connection connection; public ResultSet query(String column, int userId, String api, String day) throws Exception { String sql = String.format(SQL_FORMAT, column, userId, api, day); Statement statement = connection.createStatement(); return statement.executeQuery(sql); }}
复制代码
ApiStatsService 实现的主要逻辑通过 MySQL 和 Redis 中的表关联,关联条件基于用户 id 和 Redis 键的构建规则。
4. 实际使用
// 连接 presto 信息String prestoUrl = "jdbc:presto://192.168.1.108:8080";String prestoUser = "ice";String prestoPasswd = "";
// user 表查询的列名String userColumns = "id,name,address,age";String[] columnArray = userColumns.split(",");
// 连接 presto try (Connection connection = DriverManager.getConnection(prestoUrl, prestoUser, prestoPasswd)) { // 创建查询服务 ApiStatsService prestoUserDao = new ApiStatsService(connection); // 获取用户调用次数及用户详情 ResultSet resultSet = prestoUserDao.query(userColumns, 1, "tag", "20230812"); while (resultSet.next()) { for (String column : columnArray) { System.out.println(column + ": " + resultSet.getString(column)); } System.out.println("count: " + resultSet.getString("count")); }}
复制代码
输出:
基于 Presto 的实现:面向 SQL 编程,代码实现简单,原本几个阶段的查询现在转变为一次查询,实际上是 Presto 帮你进行了异构数据源的融合,而对于开发者只需要关心 Catalog 配置的是否正确,查询的 SQL 拼装的是否正确。
Presto 为你做了什么
Presto 管理了数据源的元数据信息:数据源类型、连接地址、用户名和密码等信息;
Presto 定义了异构数据源的访问标准:Catalog、Scehma 和 Column;
Presto 定义了 SQL 语句查询的标准:DDL、DML、方法和运算符等;
Presto SQL 的查询变成了分布式查询,理论上加速了查询,同时限制 SQL 查询过程中内存的使用。
评论