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();
}
复制代码
实际应用:管理员可以按照时间、用户查询接口的调用次数
@Data
public 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
@AllArgsConstructor
public 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.properties
connector.name=mysql
connection-url=jdbc:mysql://192.168.1.108:3306
connection-user=ice
connection-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=redis
redis.table-names=schema1.api
redis.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;
@RequiredArgsConstructor
public 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 查询过程中内存的使用。
评论