PostgreSQL 内容随机推荐系统开发实践 - 文章随机推荐

本文涉及的产品
云原生数据库 PolarDB MySQL 版,Serverless 5000PCU 100GB
云原生数据库 PolarDB 分布式版,标准版 2核8GB
云数据库 RDS MySQL Serverless,0.5-2RCU 50GB
简介: 标签PostgreSQL , 数组 , 文章 , 随机推荐 , 论坛 , 电商背景内容推荐是蛮普遍的需求,例如论坛、电商、新闻客户端等。比较简单的需求:编辑精选一些内容ID,生成推荐列表。

标签

PostgreSQL , 数组 , 文章 , 随机推荐 , 论坛 , 电商


背景

内容推荐是蛮普遍的需求,例如论坛、电商、新闻客户端等。

比较简单的需求:编辑精选一些内容ID,生成推荐列表。(例如每天生成一个这样的推荐列表。)然后随机的推荐给用户(同时过滤已读的内容)。

更高级的推荐需求:应该是根据不同口味产生的,例如对会员本身进行画像,归类。服务端针对不同口味生成不同的推荐列表。定向推荐。

本文介绍第一种需求的实践,使用PostgreSQL,中等规格的PG实例(28核),可以轻松达到每秒50万篇内容吞吐的推荐。

DEMO

以论坛为例,有文章,有编辑精选的文章列表,有会员,有会员的阅读记录,用户打开页面时,根据精选列表随机推荐20篇(同时过滤已读内容)。

假设精选列表有2000篇文档,有1000万会员。

1、文章表

create table tbl_art (  
  artid int8,  -- 文章ID  
  content text,  -- 文章内容  
  crt_time timestamp  -- 文章创建时间  
  -- ...  -- 其他,标题,作者,。。。。  
);  

会员表(略)。

2、推荐文章ID列表

create table tbl_art_list (  
  list_time timestamp primary key,  -- 列表生成时间  
  artid int8[] not null,   -- 包含哪些文章(ID),使用数组存储  
  min_crt_time timestamp not null,  -- 这些文章中,时间最老的文章时间。取自tbl_art.crt_time 。 用于清理用户阅读日志。  
  arrlen int not null   -- artid 的长度(包含几个元素,即几篇精选文章)  
  -- classid int  如果会员有归类(标签),可以按归类创建精选列表)
);  

3、写入精选列表,每次推荐时,获取最后一个列表进行推荐。

如果有定向需求(根据会员标签进行推荐,改一下表tbl_art_list结构,加个CLASS字段,同时会员表,增加CLASS字段。会员打开页面时,通过CLASS匹配tbl_art_list里的最后一个列表)

如下,生成2000篇精选文章ID。 一条记录。

insert into tbl_art_list values (  
  now(),   
  array(select (random()*1000000)::int8 from generate_series(1,2000)),   
  now(),   
  2000  
) ;  

4、已阅读记录

create table tbl_read_rec (  
  uid int8,  -- 会员ID  
  crt_time timestamp,  -- 阅读时间  
  artid int8,  -- 文章ID  
  primary key(uid,artid)  -- 主键(一篇文档,一个会员被阅读后,仅记录一次)  
);  
  
create index idx_crt_time_1 on tbl_read_rec (crt_time);  

5、随机获取推荐文章ID

用户打开推荐页面时,输入用户ID,GET多少篇精选文档(从精选列表中,随机GET)。

返回一个数组,即GET到的来自精选文章列表,并且过滤掉已读过的,随机文章ID。

create or replace function get_artid(  
  i_uid int8,   -- 用户ID  
  rows int  -- 随机获取多少篇ID  
) returns int8[] as $$  
declare   
  v_artid int8[];  -- 精选ID列表  
  len int;  -- 精选ID列表文章个数  
  res int8[] := (array[])::int8[];   -- 结果  
  tmp int8;  -- 中间变量,从精选ID列表中得到的随机文章ID  
  loopi int := 0;  -- 循环变量,已获取到多少篇符合条件的ID  
  loopx int := 0;  -- 循环变量,已循环多少次(上限,取决于精选ID列表文章个数,例如1.5倍len)  
begin   
  select artid,arrlen into v_artid,len   
    from tbl_art_list order by list_time desc limit 1;  -- 从编辑精选列表,获取最后一条。  
  loop   
    if loopi >= rows or loopx >= 1.5*len then    -- 是否已遍历所有精选文章ID (随机遍历)  
      return res;    
    end if;  
    tmp := v_artid[floor(random()*len)+1];   -- 从精选文章IDs 获取随机ID  
    perform 1 from tbl_read_rec where uid=i_uid and artid=tmp;   -- 判断是否已读  
    if not found then  
      res := array_append(res, tmp);  -- 未读,APPEND到返回结果  
      loopi := loopi +1 ;  -- 递增  
    end if;  
    loopx := loopx +1 ;  -- 递增  
  end loop;   
  return res;  
end;  
$$ language plpgsql strict;   

6、清理阅读记录

使用 limit,每次清理若干条,

使用skip locked,支持并行DELETE。

delete from tbl_read_rec where ctid = any (array(  
  select ctid from tbl_read_rec where crt_time < (select min_crt_time from tbl_art_list order by list_time desc limit 1) limit 10000 for update skip locked  
));  

7、测试

GET精选文章ID,(满足随机,过滤已读)。

性能OK。

postgres=# select get_artid(1,20);  
                                                                  get_artid                                                                    
---------------------------------------------------------------------------------------------------------------------------------------------  
 {919755,3386,100126,761631,447551,511825,168645,211819,862572,330666,942247,600470,843042,511825,295568,829303,382312,452915,499113,164219}  
(1 row)  
  
Time: 0.377 ms  
postgres=# select get_artid(1,20);  
                                                                 get_artid                                                                    
--------------------------------------------------------------------------------------------------------------------------------------------  
 {257929,796892,343984,363615,418506,326628,91731,958663,127918,794101,49124,410347,852461,922276,366815,926232,134506,153306,123694,67087}  
(1 row)  
  
Time: 0.347 ms  

假设获取到的随机ID,立即阅读,获取到的记录全部写入。(用于压测)

postgres=# insert into tbl_read_rec select 1, now(), unnest(get_artid(1,20)) on conflict do nothing;  
INSERT 0 20  
Time: 0.603 ms  
postgres=# insert into tbl_read_rec select 1, now(), unnest(get_artid(1,20)) on conflict do nothing;  
INSERT 0 20  
Time: 0.494 ms  
postgres=# insert into tbl_read_rec select 1, now(), unnest(get_artid(1,20)) on conflict do nothing;  
INSERT 0 20  
Time: 0.479 ms  
postgres=# insert into tbl_read_rec select 1, now(), unnest(get_artid(1,20)) on conflict do nothing;  
INSERT 0 20  
Time: 0.494 ms  

8、压测

vi test.sql  
  
\set uid random(1,10000000)  
insert into tbl_read_rec select :uid, now(), unnest(get_artid(:uid,20)) on conflict do nothing;  
pgbench -M prepared -n -r -P 1 -f ./test.sql -c 28 -j 28 -T 120  
transaction type: ./test.sql  
scaling factor: 1  
query mode: prepared  
number of clients: 28  
number of threads: 28  
duration: 120 s  
number of transactions actually processed: 3074866  
latency average = 1.093 ms  
latency stddev = 0.577 ms  
tps = 25623.620112 (including connections establishing)  
tps = 25625.634577 (excluding connections establishing)  
statement latencies in milliseconds:  
         0.002  \set uid random(1,10000000)  
         1.091  insert into tbl_read_rec select :uid, now(), unnest(get_artid(:uid,20)) on conflict do nothing;  

120秒压测后,已读记录表达到3GB。

postgres=# \dt+  
                          List of relations  
 Schema |     Name     | Type  |  Owner   |    Size    | Description   
--------+--------------+-------+----------+------------+-------------  
 public | tbl_art      | table | postgres | 8192 bytes |   
 public | tbl_art_list | table | postgres | 64 kB      |   
 public | tbl_read_rec | table | postgres | 3047 MB    |   
(3 rows)  

已读记录6120万。

postgres=# select count(*) from tbl_read_rec ;  
  count     
----------  
 61206909  
(1 row)  

如下

postgres=# select uid,count(*) from tbl_read_rec group by 1 limit 10;  
 uid | count   
-----+-------  
   1 |  2000  
   6 |    20  
  11 |    20  
  12 |    20  
  14 |    19  
  21 |    20  
  22 |    40  
  26 |    19  
  31 |    20  
  34 |    19  
(10 rows)  

对于已经全部阅读的,则不再推荐,因为精选列表已全部已读。

postgres=# select get_artid(1,20);  
 get_artid   
-----------  
 {}  
(1 row)  

性能

tps : 25623

每秒推荐返回 : 512460 篇ID

推荐部分还有优化空间,例如用户对整个精选列表都已读时(已读越多,GET越慢),来获取列表会比较慢(因为需要遍历整个列表ID,都拿不到20条有效记录,消耗较大,最后返回NULL)。(实测这种情况下,GET约20毫秒)

这种情况下,建议可以给用户打个标记,表示本次已推荐完所有内容(避开GET,那么性能就会直线上升,几十万TPS没问题),返回其他内容。

相关实践学习
使用PolarDB和ECS搭建门户网站
本场景主要介绍基于PolarDB和ECS实现搭建门户网站。
阿里云数据库产品家族及特性
阿里云智能数据库产品团队一直致力于不断健全产品体系,提升产品性能,打磨产品功能,从而帮助客户实现更加极致的弹性能力、具备更强的扩展能力、并利用云设施进一步降低企业成本。以云原生+分布式为核心技术抓手,打造以自研的在线事务型(OLTP)数据库Polar DB和在线分析型(OLAP)数据库Analytic DB为代表的新一代企业级云原生数据库产品体系, 结合NoSQL数据库、数据库生态工具、云原生智能化数据库管控平台,为阿里巴巴经济体以及各个行业的企业客户和开发者提供从公共云到混合云再到私有云的完整解决方案,提供基于云基础设施进行数据从处理、到存储、再到计算与分析的一体化解决方案。本节课带你了解阿里云数据库产品家族及特性。
相关文章
|
3月前
|
存储 SQL Cloud Native
深入了解云原生数据库CockroachDB的概念与实践
作为一种全球领先的分布式SQL数据库,CockroachDB以其高可用性、强一致性和灵活性等特点备受关注。本文将深入探讨CockroachDB的概念、设计思想以及实践应用,并结合实例演示其在云原生环境下的优越表现。
|
3月前
|
Cloud Native 关系型数据库 大数据
CockroachDB:云原生数据库的新概念与实践
本文将介绍CockroachDB,一种先进的云原生数据库,它具备分布式、强一致性和高可用性等特点。我们将探讨CockroachDB的基本原理、架构设计以及在实际应用中的种种优势和挑战。
|
4月前
|
存储 关系型数据库 MySQL
存储成本最高降至原来的5%,PolarDB分布式冷数据归档的业务实践
国内某家兼具投资理财、文化旅游、票务为一体的大型综合型集团公司,2015年成立至今,由于业务高速发展,业务数据增长非常快,数据库系统屡次不堪重负。该公司数据库运维总监介绍,他们目前业务压力比较大的是票务和订单系统,他们的平台每天新增几千万的订单数据,订单的数据来自于各个终端,近几年每个月以300G的数据规模在高速增长,由于数据不断增加,数据库系统迄今为止迭代过了3次。
|
6月前
|
SQL 缓存 关系型数据库
PolarDB-X 混沌测试实践:如何衡量数据库索引选择能力
随着PolarDB分布式版的不断演进,功能不断完善,新的特性不断增多,整体架构扩大的同时带来了测试链路长,出现问题前难发现,出现问题后难排查等等问题。原有的测试框架已经难以支撑实际场景的复杂模拟测试。因此,我们实现了一个基于业务场景面向优化器索引选择的混沌查询实验室,本文之后简称为CEST(complex environment simulation test)。
|
7月前
|
存储 SQL 关系型数据库
AnalyticDB PostgreSQL构建一站式实时数仓实践
本文介绍通过 AnalyticDB PostgreSQL 版基于实时物化视图,构建流批一体的一站式实时数仓解决方案,实现一套系统、一份数据、一次写入,即可在数仓内完成实时数据源头导入到实时分析全流程。
1874 5
AnalyticDB PostgreSQL构建一站式实时数仓实践
|
8月前
|
分布式数据库 调度 数据库
直播预告 | PolarDB-X 备份恢复原理与实践
备份恢复是生产级数据库必不可少的功能,而PolarDB-X 作为一款分布式数据库,备份数据的全局一致也是最基本的要求。本期分享将介绍PolarDB-X 开源版备份恢复功能的背景与原理,以及如何使用 PolarDB-X Operator 实现备份调度。
直播预告 | PolarDB-X 备份恢复原理与实践
|
8月前
|
关系型数据库 测试技术 分布式数据库
PolarDB | PostgreSQL 高并发队列处理业务的数据库性能优化实践
在电商业务中可能涉及这样的场景, 由于有上下游关系的存在, 1、用户下单后, 上下游厂商会在自己系统中生成一笔订单记录并反馈给对方, 2、在收到反馈订单后, 本地会先缓存反馈的订单记录队列, 3、然后后台再从缓存取出订单并进行处理. 如果是高并发的处理, 因为大家都按一个顺序获取, 容易产生热点, 可能遇到取出队列遇到锁冲突瓶颈、IO扫描浪费、CPU计算浪费的瓶颈. 以及在清除已处理订单后, 索引版本未及时清理导致的回表版本判断带来的IO浪费和CPU运算浪费瓶颈等. 本文将给出“队列处理业务的数据库性能优化”优化方法和demo演示. 性能提升10到20倍.
595 4
|
10月前
|
存储 SQL 运维
亿视电子基于PolarDB-X打造能源数字基座实践
本文整理自浙江亿视电子技术有限公司的技术总监韩毅在ScaleFlux × PolarDB 线下meetup中的分享。
|
11月前
|
存储 关系型数据库 API
|
11月前
|
存储 关系型数据库 Linux

相关产品

  • 云原生数据库 PolarDB