滴滴打车派单系统思考 数据库设计与实现 - 每月投入6140元, 1天最多可盈利117亿 -_-!
背景
打车类应用,如果完全按调度系统来派单,而非抢单的话,调度系统要非常的健硕。
比如滴滴打车,如何处理供给双方的需求,并高效的完成派单呢?
随着业务的需求增多,调度规则也会增加,比如拼车,预约,等。
下面是一个简单的派单系统的思考,如何使用PostgreSQL与空间数据库插件PostGIS来实现一个简单的距离优先派单、拼车撮合。
采用skip lock或advisory lock来避免锁冲突。应对高峰期问题。
《HTAP数据库 PostgreSQL 场景与性能测试之 30 - (OLTP) 秒杀 - 高并发单点更新》
PostgreSQL 设计
1、空间数据库插件PostGIS
在PostgreSQL中创建空间数据库
postgres=# create extension postgis;
CREATE EXTENSION
1、滴滴车辆位置表
记录车辆的实时位置,是否被下单,有无剩余座位(不拼单的订单直接把剩余座位设为0)
create table car (
id int primary key, -- 车辆ID,主键
pos geometry, -- 实时位置, 使用PostGIS,geometry类型
sites int2 not null default 4, -- 总座位数
rest_sites int2, -- 剩余座位数 (因为有拼车业务)
mod_time timestamp, -- 位置修改时间
order_pos geometry[], -- 当前订单对应用户打车的目的地位置
check (rest_sites <= sites and rest_sites>=0 and sites>0)
);
2、用户表
create table users (
id int8 primary key, -- ID
otherinfo jsonb -- 其他信息,请允许我偷懒一下使用JSON,实际上我这里派单只需要记录ID
);
3、订单表
记录每一笔订单,以及订单的状态
create table orders (
id serial8 primary key, -- 订单号
carid int, -- 车辆ID
uid int8, -- 用户ID
crt_time timestamp, -- 订单创建时间
pos1 geometry, -- 上车位置
pos2 geometry, -- 目的地
sites int2, -- 乘坐几人
status int2 -- 订单状态(进行中 2, 取消 1, 结束 0)
);
4、车辆位置实时更新
每N秒(比如5秒),上报并更新位置。
《HTAP数据库 PostgreSQL 场景与性能测试之 29 - (OLTP) 空间应用 - 高并发空间位置更新(含空间索引)》
假设这个城市一共有1000万量车
假定车辆的活动范围经纬度(110~120, 25~30)
vi test.sql
\set id random(1,10000000)
insert into car(id, pos, mod_time) values (
:id,
ST_SetSRID(ST_Point(round((random()*(120-110)+110)::numeric,6), round((random()*(30-25)+25)::numeric,6)), 4326),
now()
) on conflict (id) do update set pos=ST_SetSRID(ST_Point(ST_X(excluded.pos)+random()-0.5, ST_Y(excluded.pos)+random()-0.5), 4326), mod_time=excluded.mod_time
where car.sites <> car.rest_sites or car.rest_sites is null; -- 不能被叫的车辆不更新位置(例如他的座位满了)
(含空间索引)压测如下:
pgbench -M prepared -n -r -P 1 -f ./test.sql -c 56 -j 56 -T 120
progress: 4.0 s, 200614.4 tps, lat 0.279 ms stddev 0.357
progress: 5.0 s, 202598.0 tps, lat 0.276 ms stddev 0.336
progress: 6.0 s, 196562.4 tps, lat 0.285 ms stddev 0.785
progress: 7.0 s, 200305.4 tps, lat 0.280 ms stddev 0.534
progress: 8.0 s, 207505.2 tps, lat 0.270 ms stddev 0.270
progress: 9.0 s, 204128.0 tps, lat 0.274 ms stddev 0.347
5、叫车,派单
1、用户上报位置
2、查询附近可用车辆(按gist索引顺序返回,同时过滤锁、SITES、距离等条件)
3、采用空间部分索引
拼车索引
create index idx_car_pos_1 on car using gist(pos) where rest_sites>0 or rest_sites is null;
不拼车索引
create index idx_car_pos_2 on car using gist(pos) where rest_sites=sites or rest_sites is null;
4、判断是否与之拼车的函数(输入参数为目的地,以及车上已有乘客目的地)
create or replace function f_isbulk(
i_pos geometry, -- 目的地
i_poss geometry[] -- 该车已有乘客目的地
) returns boolean as $$
declare
vspheroid spheroid := 'SPHEROID["WGS84",6378137,298.257223563]' ; -- WGS84椭球体参数定义
begin
-- 先使用最简单的算法,例如任意已有乘客目的地与当前请求目的地距离在2000米以内则返回TRUE,允许拼车
-- 测试时,建议把允许的拼车目的地距离调大一点,否则可能很难找到合适的车辆
-- perform 1 from unnest(i_poss) t(pos) where ST_DistanceSpheroid(i_pos, pos, vspheroid) <= 2000 limit 1;
perform 1 from unnest(i_poss) t(pos) where ST_DistanceSpheroid(i_pos, pos, vspheroid) <= 200000000 limit 1;
if found then
return true;
else
return false; -- 距离超过2000米,不与之拼车
end if;
end;
$$ language plpgsql strict;
测试时,建议把允许的拼车目的地距离调大一点,否则可能很难找到合适的车辆
5、选择拼车的用户,使用以下函数生成订单,返回订单号,车辆ID,等
create or replace function getcar_isbulk(
i_uid int8, -- 用户ID
i_pos1 geometry, -- 上车位置
i_pos2 geometry, -- 目的地
i_sites int2 -- 乘坐几人
) returns int8 as $$ -- 返回订单号
declare
v_car_ctid tid; -- car表被请求到的CAR的记录行号
v_carid int; -- carid
v_orderid int8; -- 订单号
v_pos geometry; -- 锁定的车辆位置
vspheroid spheroid := 'SPHEROID["WGS84",6378137,298.257223563]' ; -- WGS84椭球体参数定义
begin
set local enable_seqscan=off;
set local enable_bitmapscan=off;
-- 根据条件锁定车辆ID,同时使用了try advisory lock避免行锁冲突
-- 与秒杀方法类似,大幅度提高吞吐,PG中锁定单条记录的吞吐可以达到将近 30万tps
select ctid,pos into v_car_ctid,v_pos from car where
(rest_sites > 0 -- 剩余座位数大于0
and rest_sites >= i_sites or rest_sites is null) -- 剩余座位数大于等于请求座位数
and (order_pos is null or f_isbulk(i_pos2, order_pos)) -- 目的地满足拼车要求
and pg_try_advisory_xact_lock(id) -- adlock,提高秒杀吞吐
order by i_pos1 <-> pos for update limit 1; -- 根据距离排序,以上条件满足,锁定1条即可
-- 如果车辆位置超出一定公里数(比如5公里),直接返回,不生成订单
-- 测试时,建议把公里数调大,便于找到车辆
-- if not found or ST_DistanceSpheroid(i_pos1, v_pos, vspheroid) > 5000 then
if not found or ST_DistanceSpheroid(i_pos1, v_pos, vspheroid) > 500000000 then
-- raise notice 'no car near your pos, the car leave you % meters', ST_DistanceSpheroid(i_pos1, v_pos, vspheroid);
return -1;
end if;
-- 更新车辆状态
update car set
rest_sites=coalesce(rest_sites-i_sites, sites-i_sites), -- 减少剩余座位
order_pos=coalesce(order_pos||i_pos2, array[i_pos2]) -- 将目的地追加到车辆所有目的地中
where ctid=v_car_ctid
and coalesce(rest_sites-i_sites, sites-i_sites) >= 0
returning id into v_carid; -- 返回车辆ID
if found then
-- 生成订单
insert into orders (carid, uid, crt_time, pos1, pos2, sites, status)
values(v_carid, i_uid, now(), i_pos1, i_pos2, i_sites, 2) -- 状态为进行中
returning id into v_orderid; -- 返回订单号
else
return -2;
end if;
return v_orderid;
end;
$$ language plpgsql strict;
2018-04-16优化
create index idx_car_pos_pc_1 on car using gist(pos) where rest_sites=1;
create index idx_car_pos_pc_2 on car using gist(pos) where rest_sites=2;
create index idx_car_pos_pc_3 on car using gist(pos) where rest_sites=3;
create index idx_car_pos_pc_4 on car using gist(pos) where rest_sites=4;
create index idx_car_pos_pc_5 on car using gist(pos) where rest_sites>4;
create index idx_car_pos_pc_6 on car using gist(pos) where rest_sites is null;
CREATE OR REPLACE FUNCTION public.f_isbulk(i_pos geometry, i_poss geometry[])
RETURNS int
LANGUAGE plpgsql
STRICT
AS $function$
declare
vspheroid spheroid := 'SPHEROID["WGS84",6378137,298.257223563]' ; -- WGS84椭球体参数定义
begin
-- 先使用最简单的算法,例如任意已有乘客目的地与当前请求目的地距离在2000米以内则返回TRUE,允许拼车
-- 测试时,建议把允许的拼车目的地距离调大一点,否则可能很难找到合适的车辆
perform 1 from unnest(i_poss) t(pos) where ST_DistanceSpheroid(i_pos, pos, vspheroid) <= 2000000000000 limit 1;
if found then
return 1;
else
return 0; -- 距离超过2000米,不与之拼车
end if;
end;
$function$;
create or replace function getcar_isbulk(
i_uid int8, -- 用户ID
i_pos1 geometry, -- 上车位置
i_pos2 geometry, -- 目的地
i_sites int2 -- 乘坐几人
) returns int8 as $$ -- 返回订单号
declare
v_carid int; -- carid
v_orderid int8; -- 订单号
v_pos geometry; -- 锁定的车辆位置
vspheroid spheroid := 'SPHEROID["WGS84",6378137,298.257223563]' ; -- WGS84椭球体参数定义
v_sites_step int; -- 步调
-- 如果车辆位置在给定范围内(比如5公里),则退出循环,否则继续. (测试时为了更容易找到车辆,需要把范围设大一些)
-- v_dist float8 := 5000; -- 车辆离你的距离 , 如果5公里开外,那么说明附近没有车
v_dist float8 := 500000000;
begin
set local enable_seqscan=off;
set local enable_bitmapscan=off;
case
when i_sites <= 4 then
for v_sites_step in i_sites..4 loop
-- 根据条件锁定车辆ID,同时使用了try advisory lock避免行锁冲突
-- 与秒杀方法类似,大幅度提高吞吐,PG中锁定单条记录的吞吐可以达到将近 30万tps
select id,pos into v_carid,v_pos from car where
rest_sites = v_sites_step -- 剩余座位数等于请求座位数 (或大于, loop超过i_sites时)
and coalesce(f_isbulk(i_pos2, order_pos),1)=1 -- 目的地满足拼车条件
and pg_try_advisory_xact_lock(id) -- adlock,提高秒杀吞吐
and ST_DistanceSpheroid(i_pos1, pos, vspheroid) < v_dist -- 车辆位置在给定范围内(比如5公里) . (测试时为了更容易找到车辆,需要把范围设大一些)
order by i_pos1 <-> pos for update limit 1; -- 根据距离排序,以上条件满足,锁定1条即可
-- 如果有符合条件的车辆,则退出 case
if found then
exit;
end if;
end loop;
-- 如果有符合条件的车辆,则退出 case
if not found then
-- 4个座位内都没有找到符合条件的车辆, 找剩余座位大于4个的
select id,pos into v_carid,v_pos from car where
rest_sites > 4 -- 剩余座位数等于请求座位数 (或大于, loop超过i_sites时)
and coalesce(f_isbulk(i_pos2, order_pos),1)=1 -- 目的地满足拼车条件
and ST_DistanceSpheroid(i_pos1, pos, vspheroid) < v_dist -- 车辆位置在给定范围内(比如5公里) . (测试时为了更容易找到车辆,需要把范围设大一些)
and pg_try_advisory_xact_lock(id) -- adlock,提高秒杀吞吐
order by i_pos1 <-> pos for update limit 1; -- 根据距离排序,以上条件满足,锁定1条即可
-- 如果有符合条件的车辆,则退出 case
if not found then
-- 大于4个座位的车辆没有,则找刚注册的车辆,即rest_sites is null
select id,pos into v_carid,v_pos from car where
rest_sites is null -- 剩余座位数等于请求座位数 (或大于, loop超过i_sites时)
and ST_DistanceSpheroid(i_pos1, pos, vspheroid) < v_dist -- 车辆位置在给定范围内(比如5公里) . (测试时为了更容易找到车辆,需要把范围设大一些)
and pg_try_advisory_xact_lock(id) -- adlock,提高秒杀吞吐
order by i_pos1 <-> pos for update limit 1; -- 根据距离排序,以上条件满足,锁定1条即可
end if;
end if;
-- 请求座位数大于4
else
-- 4个座位内都没有找到符合条件的车辆, 找剩余座位大于4个的
select id,pos into v_carid,v_pos from car where
rest_sites > 4 -- 剩余座位数等于请求座位数 (或大于, loop超过i_sites时)
and coalesce(f_isbulk(i_pos2, order_pos),1)=1 -- 目的地满足拼车条件
and ST_DistanceSpheroid(i_pos1, pos, vspheroid) < v_dist -- 车辆位置在给定范围内(比如5公里) . (测试时为了更容易找到车辆,需要把范围设大一些)
and pg_try_advisory_xact_lock(id) -- adlock,提高秒杀吞吐
order by i_pos1 <-> pos for update limit 1; -- 根据距离排序,以上条件满足,锁定1条即可
-- 如果有符合条件的车辆,则退出 case
if not found then
-- 大于4个座位的车辆没有,则找刚注册的车辆,即rest_sites is null
select id,pos into v_carid,v_pos from car where
rest_sites is null -- 剩余座位数等于请求座位数 (或大于, loop超过i_sites时)
and ST_DistanceSpheroid(i_pos1, pos, vspheroid) < v_dist -- 车辆位置在给定范围内(比如5公里) . (测试时为了更容易找到车辆,需要把范围设大一些)
and pg_try_advisory_xact_lock(id) -- adlock,提高秒杀吞吐
order by i_pos1 <-> pos for update limit 1; -- 根据距离排序,以上条件满足,锁定1条即可
end if;
end case;
if not found then
-- raise notice 'no car near your pos, the car leave you % meters', ST_DistanceSpheroid(i_pos1, v_pos, vspheroid);
return -1;
end if;
-- 更新车辆状态
update car set
rest_sites=coalesce(rest_sites-i_sites, sites-i_sites), -- 减少剩余座位
order_pos=coalesce(order_pos||i_pos2, array[i_pos2]) -- 将目的地追加到车辆所有目的地中
where id=v_carid
and coalesce(rest_sites-i_sites, sites-i_sites) >= 0
returning id into v_carid; -- 返回车辆ID
if found then
-- 生成订单
insert into orders (carid, uid, crt_time, pos1, pos2, sites, status)
values(v_carid, i_uid, now(), i_pos1, i_pos2, i_sites, 2) -- 状态为进行中
returning id into v_orderid; -- 返回订单号
else
return -2;
end if;
return v_orderid;
end;
$$ language plpgsql strict;
5、选择不拼车的用户,使用以下函数生成订单,返回订单号,车辆ID,等
create or replace function getcar(
i_uid int8, -- 用户ID
i_pos1 geometry, -- 上车位置
i_pos2 geometry -- 目的地
) returns int8 as $$ -- 返回订单号
declare
v_car_ctid tid; -- car表被请求到的CAR的记录行号
v_carid int; -- carid
v_orderid int8; -- 订单号
v_sites int2; -- 座位数
v_pos geometry; -- 锁定的车辆位置
vspheroid spheroid := 'SPHEROID["WGS84",6378137,298.257223563]' ; -- WGS84椭球体参数定义
begin
set local enable_seqscan=off;
set local enable_bitmapscan=off;
-- 根据条件锁定车辆ID,同时使用了try advisory lock避免行锁冲突
-- 与秒杀方法类似,大幅度提高吞吐,PG中锁定单条记录的吞吐可以达到将近 30万tps
select ctid,pos into v_car_ctid,v_pos from car where
(rest_sites=sites or rest_sites is null) -- 剩余座位数等于能提供的座位数,说明没有订单在手,满足不拼车需求
and pg_try_advisory_xact_lock(id) -- adlock,提高秒杀吞吐
order by i_pos1 <-> pos for update limit 1; -- 根据距离排序,以上条件满足,锁定1条即可
-- 如果车辆位置超出一定公里数(比如5公里),直接返回,不生成订单
-- 测试时,建议把公里数调大,便于找到车辆
-- if not found or ST_DistanceSpheroid(i_pos1, v_pos, vspheroid) > 5000 then
if not found or ST_DistanceSpheroid(i_pos1, v_pos, vspheroid) > 500000000 then
-- raise notice 'no car near your pos, the car leave you % meters', ST_DistanceSpheroid(i_pos1, v_pos, vspheroid);
return -1;
end if;
-- 更新车辆状态
update car set
rest_sites=0 -- 剩余座位减少为0
where ctid=v_car_ctid
returning id,sites into v_carid,v_sites; -- 返回车辆ID
-- 生成订单
insert into orders (carid, uid, crt_time, pos1, pos2, sites, status)
values(v_carid, i_uid, now(), i_pos1, i_pos2, v_sites, 2) -- 状态为进行中
returning id into v_orderid; -- 返回订单号
return v_orderid;
end;
$$ language plpgsql strict;
6、结束订单,取消订单
1、更新订单状态,更新车辆剩余座位,删除拼车已到达的目的地
create or replace function change_order(
i_id int8, -- 订单ID
i_status int2 -- 状态, 进行中2,取消1,结束0
) returns int as $$
declare
i_carid int;
i_pos geometry;
i_sites int2;
begin
set local enable_seqscan=off;
set local enable_bitmapscan=off;
update orders set status=i_status where id=i_id and status<>0 returning carid, pos2, sites into i_carid, i_pos, i_sites;
if found then
update car set rest_sites=rest_sites+i_sites, order_pos=array_remove(order_pos, i_pos) where id=i_carid and pg_try_advisory_xact_lock(id);
-- 测试时加上这段,因为不存在锁冲突
if not found then
raise EXCEPTION '';
end if;
return 1;
end if;
raise EXCEPTION '';
exception when others then
return -1;
end;
$$ language plpgsql strict;
7、压测
创建一个函数,生成打车时,用户上车的随机位置,用于压测
create or replace function gen_pos() returns geometry as $$
select ST_SetSRID(ST_Point(round((random()*(120-110)+110)::numeric,6), round((random()*(30-25)+25)::numeric,6)), 4326);
$$ language sql strict;
创建一个函数,获取未完结的订单号,用于压测
create or replace function gen_orderid() returns int8 as $$
declare
res int8;
n int := random()*56;
begin
set local enable_seqscan=off;
set local enable_bitmapscan=off;
-- 随机采样得到订单ID,避免并发执行时大家得到的是同一个ID
-- select id into res from orders TABLESAMPLE SYSTEM(0.1) where status = 2 limit 1;
-- 或者
select id into res from orders where status = 2 offset n limit 1;
return res;
end;
$$ language plpgsql strict volatile;
create index idx_orders_1 on orders (id) where status=2;
压测,假定有20亿用户
1、拼车打车
vi test1.sql
\set uid random(1,2000000000)
select getcar_isbulk(:uid, gen_pos(), gen_pos(), 1::int2);
2、不拼车打车
vi test2.sql
\set uid random(1,2000000000)
select getcar(:uid, gen_pos(), gen_pos());
3、结束订单
vi test3.sql
select change_order(odid, 0::int2) from gen_orderid() t(odid) where pg_try_advisory_xact_lock(odid);
4、新增车辆、更新车辆位置
vi test4.sql
\set id random(1,10000000)
insert into car(id, pos, mod_time) values (
:id,
ST_SetSRID(ST_Point(round((random()*(120-110)+110)::numeric,6), round((random()*(30-25)+25)::numeric,6)), 4326),
now()
) on conflict (id) do update set pos=ST_SetSRID(ST_Point(ST_X(excluded.pos)+random()-0.5, ST_Y(excluded.pos)+random()-0.5), 4326), mod_time=excluded.mod_time
where car.sites<>car.rest_sites or car.rest_sites is null; -- 不能被叫的车辆不更新位置(例如他的座位满了)
单一场景压测性能
1、新增车辆、更新车辆位置。 约每秒16.7万行。
pgbench -M prepared -n -r -P 1 -f ./test4.sql -c 56 -j 56 -T 120
number of transactions actually processed: 20120910
latency average = 0.334 ms
latency stddev = 0.431 ms
tps = 167648.839197 (including connections establishing)
tps = 167663.075494 (excluding connections establishing)
2、拼车打车。 约每秒1.73万笔订单。
pgbench -M simple -n -r -P 1 -f ./test1.sql -c 13 -j 13 -T 120
tps = 17338.303267 (including connections establishing)
tps = 17339.077083 (excluding connections establishing)
3、不拼车打车。 约每秒1.23万笔订单。
pgbench -M simple -n -r -P 1 -f ./test2.sql -c 13 -j 13 -T 120
tps = 26224.271953 (including connections establishing)
tps = 26225.359067 (excluding connections establishing)
4、结束订单。 约每秒20万笔订单。
pgbench -M prepared -n -r -P 1 -f ./test3.sql -c 56 -j 56 -T 120
tps = 204695.541432 (including connections establishing)
tps = 204713.293460 (excluding connections establishing)
单一场景 | TPS |
---|---|
更新位置 | 16.7 万 |
拼车 | 1.73 万 |
不拼车 | 2.6 万 |
结束订单 | 20 万 |
混合场景压测性能
pgbench -M prepared -n -r -P 3 -f ./test4.sql -c 16 -j 16 -T 120 > ./log1 2>&1 &
pgbench -M simple -n -r -P 3 -f ./test1.sql -c 10 -j 10 -T 120 > ./log2 2>&1 &
pgbench -M simple -n -r -P 3 -f ./test2.sql -c 10 -j 10 -T 120 > ./log3 2>&1 &
pgbench -M prepared -n -r -P 3 -f ./test3.sql -c 16 -j 16 -T 120 > ./log4 2>&1 &
混合场景 | TPS |
---|---|
更新位置 | 5.43 万 |
拼车 | 10647 |
不拼车 | 12045 |
结束订单 | 7.4 万 |
拼车和不拼车加起来22692。
一些压测后的数据
postgres=# select * from car where rest_sites between 1 and 2 limit 10;
id | pos | sites | rest_sites | mod_time | order_pos
---------+----------------------------------------------------+-------+------------+----------------------------+----------------------------------
7317265 | 0101000020E6100000A489994A8FAB5D40CC44BFBDA9343D40 | 4 | 1 | 2018-04-14 22:58:03.407874 | {0101000020E6100000B6BFB33D7AA55B40D28A6F287C9A3940:0101000020E6100000B6BFB33D7AA55B40D28A6F287C9A3940:0101000020E6100000B6BFB33D7AA55B40D28A6F287C9A3940}
3111010 | 0101000020E6100000BB1DFDD578835B405280A6641A673C40 | 4 | 2 | 2018-04-14 23:12:53.34535 | {0101000020E6100000FEB7921D1B1E5C400CC9C9C4AD7E3B40:0101000020E6100000FEB7921D1B1E5C400CC9C9C4AD7E3B40}
7541005 | 0101000020E610000086554818387E5C40B728ADCD43D03D40 | 4 | 2 | 2018-04-14 23:11:29.206046 | {0101000020E61000000F9BC8CC05065D400B2AAA7EA5373A40:0101000020E61000000F9BC8CC05065D400B2AAA7EA5373A40}
8690828 | 0101000020E6100000000CAF61E2675C40D3DC0C1EA9D13940 | 4 | 1 | 2018-04-14 23:14:19.151509 | {}
6457811 | 0101000020E610000032F3008486A25D40EC2EF2553E843B40 | 4 | 2 | 2018-04-14 23:14:03.251394 | {0101000020E6100000AFD007CBD8555D402DAF5C6F9BD93D40:0101000020E6100000AFD007CBD8555D402DAF5C6F9BD93D40}
8742742 | 0101000020E6100000A9F5D11666C25C40BD187A2ED8943A40 | 4 | 1 | 2018-04-14 23:07:05.165694 | {0101000020E6100000B0E76B96CBC95B401CCD91955FAE3B40:0101000020E6100000B0E76B96CBC95B401CCD91955FAE3B40:0101000020E6100000B0E76B96CBC95B401CCD91955FAE3B40}
2817265 | 0101000020E610000039565329D09F5C403F56DACE20EF3C40 | 4 | 2 | 2018-04-14 23:11:18.579623 | {0101000020E61000005260014C19C55B407AC6BE64E3393D40:0101000020E61000005260014C19C55B407AC6BE64E3393D40}
7150506 | 0101000020E610000061A6600487595D405D7086861CBB3C40 | 4 | 2 | 2018-04-14 23:15:33.078539 | {0101000020E61000006743FE9941FD5D40F0FB372F4E483C40:0101000020E61000006743FE9941FD5D40F0FB372F4E483C40}
5583272 | 0101000020E6100000857D1D6801D25D40C58F015D4F2B3D40 | 4 | 2 | 2018-04-14 23:10:02.842235 | {0101000020E61000004B92E7FA3E9B5D40FCA6B05241193940:0101000020E61000004B92E7FA3E9B5D40FCA6B05241193940}
1367076 | 0101000020E610000072D31806729E5D403A3F7D95E3703D40 | 4 | 1 | 2018-04-14 23:14:02.91879 | {0101000020E61000009450FA42C88C5B4003B4AD669DCD3C40:0101000020E61000009450FA42C88C5B4003B4AD669DCD3C40:0101000020E61000009450FA42C88C5B4003B4AD669DCD3C40}
(10 rows)
postgres=# select * from orders limit 10;
id | carid | uid | crt_time | pos1 | pos2 | sites | status
---------+---------+------------+----------------------------+----------------------------------------------------+----------------------------------------------------+-------+--------
5350583 | 8219421 | 656330079 | 2018-04-14 22:58:50.85801 | 0101000020E6100000F1B913ECBFA55B40E86A2BF697213C40 | 0101000020E6100000A6F0A0D975CE5C40543A58FFE7BC3A40 | 4 | 0
5350594 | 387903 | 1251082211 | 2018-04-14 22:58:50.899007 | 0101000020E61000005E4A5D328E2F5D40BF9CD9AED0BB3B40 | 0101000020E610000073F4F8BD4D465D409DD66D50FB353C40 | 1 | 0
5350601 | 6032695 | 633527755 | 2018-04-14 22:58:50.901435 | 0101000020E6100000095053CBD6D45B401232906797BF3D40 | 0101000020E6100000FDFA213658D85C4010035DFB02023A40 | 1 | 0
5350645 | 9332236 | 1950115872 | 2018-04-14 22:58:50.906247 | 0101000020E610000021CD58349D835B400A9E42AED4F33C40 | 0101000020E6100000988922A46E9D5C40FD2E6CCD56663C40 | 4 | 0
5534249 | 1426569 | 982096157 | 2018-04-14 22:59:13.750311 | 0101000020E6100000115322895E535D404EB857E6ADEE3B40 | 0101000020E61000002EAEF199ECF65C40D845D1031FE33940 | 1 | 0
5350660 | 725764 | 1023537035 | 2018-04-14 22:58:50.907513 | 0101000020E61000004DDBBFB2D2575C404580D3BB78C73940 | 0101000020E6100000E544BB0A294E5C407CD2890453593B40 | 1 | 0
5534260 | 1777824 | 1176511003 | 2018-04-14 22:59:13.751194 | 0101000020E6100000FCE4284014305D404A0A2C80297B3B40 | 0101000020E610000079B29B19FD9E5B4015C8EC2C7A6B3940 | 1 | 0
5350677 | 9205198 | 1483861832 | 2018-04-14 22:58:50.9087 | 0101000020E61000002BD9B11188C35B4094FB1D8A024D3D40 | 0101000020E6100000F6EE8FF7AA2D5D402B685A6265203D40 | 4 | 0
5350704 | 4722183 | 1707309465 | 2018-04-14 22:58:50.910806 | 0101000020E610000010E7E104A66C5D4080F44D9A069D3A40 | 0101000020E61000007D2079E7509D5D4084D4EDEC2B3B3B40 | 1 | 0
5350729 | 1273928 | 1122725930 | 2018-04-14 22:58:50.91219 | 0101000020E6100000815CE2C803805D40AED85F764F0A3C40 | 0101000020E61000004E7B4ACE89595D404CA59F70763F3C40 | 1 | 0
(10 rows)
postgres=# select * from orders where status=2 limit 10;
id | carid | uid | crt_time | pos1 | pos2 | sites | status
---------+---------+------------+----------------------------+----------------------------------------------------+----------------------------------------------------+-------+--------
5432604 | 6569377 | 1058047186 | 2018-04-14 22:58:54.188969 | 0101000020E6100000CB13083BC5785C40DBA6785C54EF3C40 | 0101000020E6100000E0F42EDE8FE25B40FE43FAEDEB783940 | 1 | 2
5432645 | 850296 | 1314115523 | 2018-04-14 22:58:54.190902 | 0101000020E61000006878B306EF335C40A0E1CD1ABC173D40 | 0101000020E6100000139A249694D35C40F513CE6E2DE73A40 | 4 | 2
5432709 | 6569377 | 283077597 | 2018-04-14 22:58:54.194083 | 0101000020E6100000CB13083BC5785C40DBA6785C54EF3C40 | 0101000020E6100000E0F42EDE8FE25B40FE43FAEDEB783940 | 1 | 2
5432752 | 9088745 | 11001436 | 2018-04-14 22:58:54.195929 | 0101000020E61000006878B306EF335C40A0E1CD1ABC173D40 | 0101000020E6100000139A249694D35C40F513CE6E2DE73A40 | 4 | 2
5432817 | 1126957 | 577778747 | 2018-04-14 22:58:54.199477 | 0101000020E6100000CB13083BC5785C40DBA6785C54EF3C40 | 0101000020E6100000E0F42EDE8FE25B40FE43FAEDEB783940 | 1 | 2
5432857 | 480496 | 269272019 | 2018-04-14 22:58:54.201194 | 0101000020E61000006878B306EF335C40A0E1CD1ABC173D40 | 0101000020E6100000139A249694D35C40F513CE6E2DE73A40 | 4 | 2
5432917 | 1126957 | 1665973989 | 2018-04-14 22:58:54.203993 | 0101000020E6100000CB13083BC5785C40DBA6785C54EF3C40 | 0101000020E6100000E0F42EDE8FE25B40FE43FAEDEB783940 | 1 | 2
5432962 | 7515414 | 14898049 | 2018-04-14 22:58:54.206027 | 0101000020E61000006878B306EF335C40A0E1CD1ABC173D40 | 0101000020E6100000139A249694D35C40F513CE6E2DE73A40 | 4 | 2
5433012 | 7323377 | 1678751369 | 2018-04-14 22:58:54.208529 | 0101000020E61000006878B306EF335C40A0E1CD1ABC173D40 | 0101000020E6100000139A249694D35C40F513CE6E2DE73A40 | 4 | 2
5433048 | 1126957 | 281166362 | 2018-04-14 22:58:54.210329 | 0101000020E6100000CB13083BC5785C40DBA6785C54EF3C40 | 0101000020E6100000E0F42EDE8FE25B40FE43FAEDEB783940 | 1 | 2
(10 rows)
假设平均每笔订单30元,提成20%,以每秒结束订单为例,这一个PG库最多带来的盈利为 每秒22692*6 = 13.6
万RMB。 一天117亿RMB。(当然不可能一直处于高潮状态,这个是夸张说法,不过一天1小时高潮还是要有的,5亿也足矣啊。)
而这样一台数据库的硬件成本,估计在2万元左右(如果要考虑搭建主备,容灾,备份,硬件成本可能不会超过30万。)。
即使采用阿里云RDS PostgreSQL,也仅需6140每月。(56核,480G内存,2TB 高效SSD云存储)(还包含了云盘本身的可靠性、数据库的高可用、备份、等等。关键是数据库内核级的支持与服务)。
优化
1、CODE瓶颈诊断
《PostgreSQL 源码性能诊断(perf profiling)指南》
2、UDF瓶颈诊断 (调优后更新了前面的测试数据)
把逻辑都写到了UDF里面,如何诊断性能呢?
load 'auto_explain';
set auto_explain.log_analyze =on;
set auto_explain.log_buffers =on;
set auto_explain.log_min_duration =0;
set auto_explain.log_nested_statements =on;
set auto_explain.log_time=on;
set auto_explain.log_verbose =on;
set client_min_messages ='log';
postgres=# select getcar_isbulk(1, st_setsrid(st_point(120,30),4326), st_setsrid(st_point(120,30.1),4326), 1::int2);
LOG: duration: 0.047 ms plan:
Query Text: SELECT 1 from unnest(i_poss) t(pos) where ST_DistanceSpheroid(i_pos, pos, vspheroid) <= 2000000000 limit 1
Limit (cost=0.00..1.56 rows=1 width=4) (actual time=0.039..0.039 rows=1 loops=1)
Output: 1
-> Function Scan on pg_catalog.unnest t (cost=0.00..51.25 rows=33 width=4) (actual time=0.038..0.038 rows=1 loops=1)
Output: 1
Function Call: unnest('{0101000020E61000000000000000005E409A99999999193E40:0101000020E61000000000000000005E409A99999999193E40:0101000020E61000000000000000005E409A99999999193E40}'::geometry[])
Filter: (st_distancespheroid('0101000020E61000000000000000005E409A99999999193E40'::geometry, t.pos, 'SPHEROID("WGS84",6378137,298.257223562997)'::spheroid) <= '2000000000'::double precision)
LOG: duration: 0.485 ms plan:
Query Text: select id,pos from car where
(rest_sites > 0 -- 剩余座位数大于0
and rest_sites >= i_sites or rest_sites is null) -- 剩余座位数大于等于请求座位数
and (order_pos is null or f_isbulk(i_pos2, order_pos)) -- 目的地满足拼车要求
and pg_try_advisory_xact_lock(id) -- adlock,提高秒杀吞吐
order by i_pos1 <-> pos for update skip locked limit 1
Limit (cost=0.41..1.50 rows=1 width=50) (actual time=0.482..0.482 rows=1 loops=1)
Output: id, pos, (('0101000020E61000000000000000005E400000000000003E40'::geometry <-> pos)), ctid
Buffers: shared hit=9
-> LockRows (cost=0.41..1269428.33 rows=1169416 width=50) (actual time=0.481..0.481 rows=1 loops=1)
Output: id, pos, (('0101000020E61000000000000000005E400000000000003E40'::geometry <-> pos)), ctid
Buffers: shared hit=9
-> Index Scan using idx_car_pos_1 on public.car (cost=0.41..1257734.17 rows=1169416 width=50) (actual time=0.465..0.465 rows=1 loops=1)
Output: id, pos, ('0101000020E61000000000000000005E400000000000003E40'::geometry <-> pos), ctid
Order By: (car.pos <-> '0101000020E61000000000000000005E400000000000003E40'::geometry)
Filter: ((((car.rest_sites > 0) AND (car.rest_sites >= '1'::smallint)) OR (car.rest_sites IS NULL)) AND pg_try_advisory_xact_lock((car.id)::bigint) AND ((car.order_pos IS NULL) OR f_isbulk('0101000020E61000000000000000005E409A99999999193E40'::geometry, car.order_pos)))
Buffers: shared hit=7
LOG: duration: 0.102 ms plan:
Query Text: update car set
rest_sites=coalesce(rest_sites-i_sites, sites-i_sites), -- 减少剩余座位
order_pos=coalesce(order_pos||i_pos2, array[i_pos2]) -- 将目的地追加到车辆所有目的地中
where id=v_carid
and coalesce(rest_sites-i_sites, sites-i_sites) >= 0
returning id
Update on public.car (cost=0.43..2.67 rows=1 width=86) (actual time=0.098..0.099 rows=1 loops=1)
Output: id
Buffers: shared hit=15
-> Index Scan using car_pkey on public.car (cost=0.43..2.67 rows=1 width=86) (actual time=0.017..0.018 rows=1 loops=1)
Output: id, pos, sites, COALESCE((rest_sites - '1'::smallint), (sites - '1'::smallint)), mod_time, COALESCE((order_pos || '0101000020E61000000000000000005E409A99999999193E40'::geometry), '{0101000020E61000000000000000005E409A99999999193E40}'::geometry[]), ctid
Index Cond: (car.id = 1112283)
Filter: (COALESCE((car.rest_sites - '1'::smallint), (car.sites - '1'::smallint)) >= 0)
Buffers: shared hit=4
LOG: duration: 0.032 ms plan:
Query Text: insert into orders (carid, uid, crt_time, pos1, pos2, sites, status)
values(v_carid, i_uid, now(), i_pos1, i_pos2, i_sites, 2) -- 状态为进行中
returning id
Insert on public.orders (cost=0.00..0.02 rows=1 width=96) (actual time=0.030..0.031 rows=1 loops=1)
Output: id
Buffers: shared hit=8
-> Result (cost=0.00..0.02 rows=1 width=96) (actual time=0.010..0.010 rows=1 loops=1)
Output: nextval('orders_id_seq'::regclass), 1112283, '1'::bigint, now(), '0101000020E61000000000000000005E400000000000003E40'::geometry, '0101000020E61000000000000000005E409A99999999193E40'::geometry, '1'::smallint, '2'::smallint
Buffers: shared hit=1
LOG: duration: 1.686 ms plan:
Query Text: select getcar_isbulk(1, st_setsrid(st_point(120,30),4326), st_setsrid(st_point(120,30.1),4326), 1::int2);
Result (cost=0.00..0.26 rows=1 width=8) (actual time=1.681..1.681 rows=1 loops=1)
Output: getcar_isbulk('1'::bigint, '0101000020E61000000000000000005E400000000000003E40'::geometry, '0101000020E61000000000000000005E409A99999999193E40'::geometry, '1'::smallint)
Buffers: shared hit=56
getcar_isbulk
---------------
18854017
(1 row)
分库
通常分库的目的是降低单库的请求量,但是对于时空库,如何分区好呢?
1、如果按空间分库,涉及地理边界问题
2、如果按其他分库,由于任一空间数据可能在所有分库,查询车辆时,涉及所有分库全部搜索的问题
思路:
为了降低请求量,还是需要按空间来分区,但是需要克服边界的问题,以及车辆位置迁移的问题。
1、首先,按车辆经常活动的位置分库, 构建元数据,保存:多边形 <-> 库 映射关系.
2、根据用户上车位置,选择覆盖它的多边形,找到这个多边形对应的一个或多个库,多边形选择的性能,PG非常的好:
《HTAP数据库 PostgreSQL 场景与性能测试之 5 - (OLTP) 空间应用 - 空间包含查询(表内多边形 包含 输入空间对象)》
3、订单映射关系, 表示一个订单在哪个库生成的,这个数据可以按订单号分库,不存在空间这种交错问题。
4、最后,车辆位置修订(比如某个车辆因订单跑到很远的地方,点更新所属位置,将车辆信息进行迁移(从一个分库改到另一个分库),这样目标位置库内就有它的车辆信息了,可以被用户看到。)。
小结
PostgreSQL 是一个非常棒的全栈式数据库,本例用到了PG的几个特性:
1、空间数据库插件PostGIS
2、数组,为拼车提供了算法基础
3、部分索引,只对需要检索的数据创建索引
4、UDF,数据库端编程,本例的派单完全由PG的PLPGSQL函数完成
5、skip lock, advisorry lock。与秒杀类似,用于提高高峰期的打车吞吐,不会造成一辆车被多个客户抢锁,白白等待。
测试机位56核的阿里云ECS虚拟机,SSD云盘。
性能
在混合场景,这样一个主机,每秒约处理1万笔派单。如果峰值有达到每秒100万比订单,可以分100个库。
算笔账
假设平均每笔订单30元,提成20%,以每秒结束订单为例,这一个PG库最多带来的盈利为 每秒22692*6 = 13.6
万RMB。 一天117亿RMB。(当然不可能一直处于高潮状态,这个是夸张说法,不过一天1小时高潮还是要有的,5亿也足矣啊。)
而购买一个类似规格的阿里云RDS PostgreSQL实例(56核,480G内存,2TB SSD云盘存储),成本仅需6140每月(还包含了云盘本身的可靠性、数据库的高可用、备份、等等。关键是数据库内核级的支持与服务,你值得拥有)。
优化
本文即兴而写,内容可能会很糙,主要是提供一些思路和DEMO,后面肯定还有很多的优化空间,比如下面的几个思路,仅供参考,谢谢观赏。
未来优化1:对于不能被叫的车辆,形式过程中不更新其位置。本例中已优化(insert on conflict语法内支持)。
未来优化2:高峰期,撮合同一方向的订单。可以利用类似数据库的分组提交,打车时HOLD一定时间窗口,按目标方向,比如使用k-means,按目的地位置聚集归类进行撮合(当然还可以扩展更多的撮合方法)。
未来优化3:对于同一个时刻,同一个地点,有多人打车时,如果都按同样的就近选择CAR的规则,会导致同一辆CAR被多次挑选中,本文使用了ADVISORY LOCK来避免行锁冲突。但是依旧有更好的优化,因为这种方法虽然没有了锁冲突,但是扫描依旧是从近到远的,所以可能并发时,一些会话存在多行扫描才找到没有被锁定的行。 有一种方法,比如,类似组提交,对同一个地点同时打车的多人,一次取多辆CAR,在程序中分配给不同的人。 还有一种方法,需要数据库内来实现,给一个离散因子,每次获取到的可能不是最近的CAR,满足多少米内的周边的CARs,随机挑选,但是这个随机挑选必须在INDEX中完成,必须保证在库中的index scan, heap scan都只扫一条。(类似索引的位点随机扫)
未来优化4:由于car表经常更新可以设计一个合理fillfactor,包括它的索引。例如设置为70.
一些有趣的相关文章:
《人分九等,数有阶梯 - PostgreSQL 阶品(颗粒)分析函数width_bucket, kmean应用》
《在PostgreSQL中如何生成测试kmean算法的数据》
《PostgreSQL 多元线性回归 - 1 MADLib Installed in PostgreSQL 9.2》
http://postgis.net/docs/manual-2.4/