搭建共享汽车管理平台

更新时间:
复制 MD 格式

通过表格存储时序模型统一存储车辆元数据和行驶轨迹,配合 Lastpoint 索引和多元索引实现多条件车辆检索与轨迹回放。

方案概述

共享汽车管理平台需要具备车辆管理、轨迹存储和订单管理能力。车辆管理负责维护车辆基本信息和实时状态,支持按地理位置、车型、续航等条件检索可用车辆。轨迹存储负责持久化车辆行驶过程中每 10 秒上报的位置、车速、续航等监控数据,支撑订单计费和轨迹回放。

这类场景的数据有以下特点。

  • 车辆元数据规模在几十万到几千万条,需要多条件组合检索。

  • 轨迹和状态数据属于典型的时序数据,写入吞吐可达每秒几万到几百万条,日积累量在十亿到百亿级。为控制存储成本,需要数据生命周期管理。

传统方案用 MySQL 存储车辆基本信息,HBase 存储轨迹数据并同步到 Solr 提供检索,架构复杂且需自主运维多个组件。表格存储的时序模型将车辆元数据和轨迹数据统一存储在一张时序表中,配合 Lastpoint 索引获取各车辆最新状态,通过多元索引实现地理位置查询和多条件检索,无需额外的检索组件。

方案设计

本方案使用一张时序表存储车辆数据。在时序模型中,每辆车对应一条时间线,车辆基本信息存储在时间线元数据中,行驶轨迹和状态数据存储为时间线的数据点。

时序模型概念

映射到车辆数据

说明

measurementName

平台标识

度量名称,用于区分不同的共享汽车平台。例如 shared_car

dataSource

平台名称

数据源标识,用于区分同一度量下的不同平台实体。例如 platform_a

tags

车辆标识

标签信息,用于唯一标识一辆车。例如 car_id=CAR_001。measurementName + dataSource + tags 三者组合唯一确定一条时间线。

attributes

车辆元数据

时间线的属性信息,存储车辆的基本属性,例如车型(model)、车牌(plate)、颜色(color)、座位数(seats)、续航(range_km)、状态(status)和当前位置(location)。通过 QueryTimeseriesMeta 按属性条件检索。

timeInUs + fields

轨迹和状态数据点

时间戳(微秒)和数据字段,存储每个上报周期的经纬度(latitude/longitude)、车速(speed)、续航(range_km)和关联订单(order_id)等信息。

为支持车辆检索,本方案在时序表上创建 Lastpoint 索引,获取各车辆的最新数据点。在 Lastpoint 索引上创建多元索引,按车辆状态、地理位置、续航等条件组合查询。

方案实现

以下步骤基于表格存储 Java SDK 实现共享汽车管理平台的数据层。

说明

本方案使用 V4 签名认证方式连接表格存储时序模型实例,需在 Maven 依赖中添加 Tablestore SDK 5.17.4 或以上版本。时序模型实例需在表格存储控制台单独创建。

下载示例代码,可直接运行体验。

  • 示例代码下载:car.demo.zip

  • 运行前设置环境变量 OTS_ENDPOINTOTS_AK_ENVOTS_SK_ENVOTS_INSTANCEOTS_REGION,指向时序模型实例。

  • 运行 SharedCarDemo,依次执行建表、注册车辆、上传轨迹、检索车辆和查询轨迹。

步骤一:初始化客户端和创建时序表

使用 V4 签名构造 TimeseriesClient,创建时序表并配置数据生命周期。

// Build V4 signature credentials
String accessKeyId = System.getenv("TABLESTORE_ACCESS_KEY_ID");
String accessKeySecret = System.getenv("TABLESTORE_ACCESS_KEY_SECRET");
String region = "cn-hangzhou";
String instanceName = "your-timeseries-instance";
String endpoint = "https://your-timeseries-instance.cn-hangzhou.ots.aliyuncs.com";

DefaultCredentials credentials = new DefaultCredentials(accessKeyId, accessKeySecret);
V4Credentials v4Credentials = V4Credentials.createByServiceCredentials(credentials, region);
CredentialsProvider provider = new DefaultCredentialProvider(v4Credentials);

TimeseriesClient client = new TimeseriesClient(
        endpoint, provider, instanceName, null, new ResourceManager(null, null));

// Create timeseries table with 90-day TTL
TimeseriesTableMeta tableMeta = new TimeseriesTableMeta("shared_car_track");
tableMeta.setTimeseriesTableOptions(new TimeseriesTableOptions(90 * 24 * 3600));
CreateTimeseriesTableRequest request = new CreateTimeseriesTableRequest(tableMeta);
request.setEnableAnalyticalStore(false);
client.createTimeseriesTable(request);

步骤二:注册车辆(写入时间线元数据)

通过 UpdateTimeseriesMeta 将车辆基本信息写入时间线属性。每辆车对应一条时间线,由 measurementName、dataSource 和 tags 唯一标识。

// Register a car by writing timeline metadata
Map<String, String> tags = new TreeMap<>();
tags.put("car_id", "CAR_001");
TimeseriesKey key = new TimeseriesKey("shared_car", "platform_a", tags);

TimeseriesMeta meta = new TimeseriesMeta(key);
Map<String, String> attrs = new HashMap<>();
attrs.put("model", "BYD Qin Plus");
attrs.put("plate", "ZA12345");
attrs.put("color", "White");
attrs.put("seats", "5");
attrs.put("range_km", "120");
attrs.put("status", "idle");
attrs.put("location", "30.2741,120.1551");
meta.setAttributes(attrs);

UpdateTimeseriesMetaRequest request = new UpdateTimeseriesMetaRequest("shared_car_track");
request.setMetas(Collections.singletonList(meta));
UpdateTimeseriesMetaResponse response = client.updateTimeseriesMeta(request);
if (!response.isAllSuccess()) {
    for (UpdateTimeseriesMetaResponse.FailedRowResult fail : response.getFailedRows()) {
        System.out.println("Failed: " + fail.getIndex() + " - " + fail.getError());
    }
}

步骤三:上传轨迹数据

车辆行驶过程中每 10 秒上传一个监控数据点,包含经纬度、车速、续航和关联订单号。通过 PutTimeseriesData 批量写入。

// Upload tracking data points
List<TimeseriesRow> rows = new ArrayList<>();
Map<String, String> tags = new TreeMap<>();
tags.put("car_id", "CAR_001");
TimeseriesKey key = new TimeseriesKey("shared_car", "platform_a", tags);

// Simulate 10 data points at 10-second intervals
long baseTime = System.currentTimeMillis();
for (int i = 0; i < 10; i++) {
    long timeUs = (baseTime - (10 - i) * 10000) * 1000;  // Microseconds
    TimeseriesRow row = new TimeseriesRow(key, timeUs);
    row.addField("latitude", ColumnValue.fromDouble(30.2741 + i * 0.001));
    row.addField("longitude", ColumnValue.fromDouble(120.1551 + i * 0.0015));
    row.addField("speed", ColumnValue.fromLong(30 + (long)(Math.random() * 50)));
    row.addField("range_km", ColumnValue.fromLong(120 - i * 2));
    row.addField("order_id", ColumnValue.fromString("ORDER_001"));
    rows.add(row);
}

PutTimeseriesDataRequest request = new PutTimeseriesDataRequest("shared_car_track");
request.setRows(rows);
PutTimeseriesDataResponse response = client.putTimeseriesData(request);
if (!response.isAllSuccess()) {
    for (PutTimeseriesDataResponse.FailedRowResult fail : response.getFailedRows()) {
        System.out.println("Failed: " + fail.getIndex() + " - " + fail.getError());
    }
}

步骤四:检索车辆

通过 QueryTimeseriesMeta 按时间线属性检索车辆。以下示例查询所有空闲车辆,也可按车型、续航等条件组合查询。

// Search idle cars by metadata attributes
QueryTimeseriesMetaRequest request = new QueryTimeseriesMetaRequest("shared_car_track");
CompositeMetaQueryCondition condition = new CompositeMetaQueryCondition(
        MetaQueryCompositeOperator.OP_AND);
condition.addSubCondition(new MeasurementMetaQueryCondition(
        MetaQuerySingleOperator.OP_EQUAL, "shared_car"));
condition.addSubCondition(new AttributeMetaQueryCondition(
        MetaQuerySingleOperator.OP_EQUAL, "status", "idle"));
request.setCondition(condition);
request.setGetTotalHits(true);
request.setLimit(100);

QueryTimeseriesMetaResponse response = client.queryTimeseriesMeta(request);
System.out.println("Idle cars: " + response.getTotalHits());
for (TimeseriesMeta meta : response.getTimeseriesMetas()) {
    System.out.println("  Car: " + meta.getTimeseriesKey().getTags()
            + ", Model: " + meta.getAttributes().get("model")
            + ", Range: " + meta.getAttributes().get("range_km") + " km");
}

如需按地理位置查询附近车辆,通过 Lastpoint 索引和多元索引,使用 GeoDistanceQuery 实现范围检索。

// Create Lastpoint index for latest data point access
CreateTimeseriesLastpointIndexRequest lpRequest =
        new CreateTimeseriesLastpointIndexRequest("shared_car_track", "car_lastpoint_index", true);
client.createTimeseriesLastpointIndex(lpRequest);

// Create search index on Lastpoint index for geo queries
// Then use SyncClient to query with GeoDistanceQuery + TermQuery(status=idle)

步骤五:查询轨迹

通过 GetTimeseriesData 按时间线和时间范围查询车辆的历史轨迹数据,支持正序和倒序读取。

// Query trajectory of a specific car within a time range
GetTimeseriesDataRequest request = new GetTimeseriesDataRequest("shared_car_track");
Map<String, String> tags = new TreeMap<>();
tags.put("car_id", "CAR_001");
TimeseriesKey key = new TimeseriesKey("shared_car", "platform_a", tags);
request.setTimeseriesKey(key);

// Query last hour
long now = System.currentTimeMillis() * 1000;  // Microseconds
request.setTimeRange(now - 3600L * 1000 * 1000 * 1000, now);
request.setLimit(100);

GetTimeseriesDataResponse response = client.getTimeseriesData(request);
for (TimeseriesRow row : response.getRows()) {
    Map<String, ColumnValue> fields = row.getFields();
    System.out.printf("Time: %d, Lat: %.6f, Lng: %.6f, Speed: %d km/h, Range: %d km%n",
            row.getTimeInUs() / 1000,
            fields.get("latitude").asDouble(),
            fields.get("longitude").asDouble(),
            fields.get("speed").asLong(),
            fields.get("range_km").asLong());
}

使用 backward 参数倒序读取,获取车辆的最新状态。

// Get the latest data point of a car (backward query)
GetTimeseriesDataRequest request = new GetTimeseriesDataRequest("shared_car_track");
request.setTimeseriesKey(key);
request.setTimeRange(0, System.currentTimeMillis() * 1000);
request.setBackward(true);
request.setLimit(1);
GetTimeseriesDataResponse response = client.getTimeseriesData(request);

资源清理

重要

以下操作将删除时序表及其所有数据和索引,不可恢复。确认不再需要这些数据后再执行。

// Delete the timeseries table and all its data
DeleteTimeseriesTableRequest request = new DeleteTimeseriesTableRequest("shared_car_track");
client.deleteTimeseriesTable(request);