一、项目背景
为了帮助某个公司精准营销,通过大数据分析,对客户画像,对产品画像,输出客户标签,产品标签。当某位客户进入系统时,根据客户的标签及产品的标签实时匹配规则,向客户推荐合适的产品。这里要解决的是:如何设计一个合适的应用系统及存储方案,可以满足上述标签的存储与更新、规则的制定与运算,从而实现实时的向客户推荐产品。
营销团队的营销经理期望可以自由的制定营销规则。而这些规则涉及到客户的信息,客户的标签,产品的标签。营销经理已经习惯了导入导出白名单(由于某些信息系统不完备而导致的手工操作)。公司现在的存量客户是1000万,业务团队规划三年后客户数量能达到5000万。在售的产品数量不会超过100款,更新频率低。
某位客户的标签可能是这样的:性别:男;年龄:青年; 职业:IT; 收入:白领。而某个理财产品的标签可能是这样的:产品风险:中; 准入门槛:低; 起购金额:1000元。这种形式的标签种类(即key的数量)现在生效的数量是150个,业务团队规划三年后标签的有效种类可能达到800个。
业务团队所说的标签,并没有一个来自于业务团队的明确定义,有时候标签是像上面所说的,产品风险:中; 准入门槛:低。有时候则是指类似于客户的月收入21300元这样的一些连续的数值,在公司的某个系统M里甚至是将住址、登陆次数这样的信息也在数据库表里设计成标签。这个系统M的标签表是传统的设计,每一个标签就是一列,所以系统M的客户标签表已经成了一张超大的表,目前已有120个字段左右,成一个稀疏矩阵,行数与列数都非常多,性能已严重跟不上需要,功能扩展也无法满足业务团队的需要。每当定义一个新的标签时,就需要变更数据库,在这张表上增加一个字段。所以需要一些新的思路重新设计客户标签系统。
二、初步分析
对业务场景与标签数据进行初步分析后,一个很关键的问题是,应当如何理解业务团队所说的“标签”呢?
1、一个新的思路是只将可枚举的值才认为是标签,标签的值是可以穷举的。例如“年收入”可以打成标签值30-50万,但不能是具体的“31万”。客户的标签应当区别于客户的属性,是经过分析后定性的不是定量的。即每一种类的标签有一个标签名+若干个可穷举的值。对于同一个标签不同的值,值的类型是确定的。 而客户的属性可以是连续型的数值或字符串,例如住址,月收入(可以精确到分)。
2、当前存在大量的标签是布尔类型值的;例如某个客户标签:“是否黄金类理财产品的老客户:是”。 多数标签的枚举值在15个以内。
3、负责营销的业务团队制定一些营销规则时,有些规则不仅仅涉及客户的标签,还会涉及客户一些连续数值的属性,例如:资产金额,年龄,登陆次数。甚至可能需要对如家庭地址、电话号码这样的信息进行字符串处理。
4、营销过程中有时会导入白名单,一个白名单可能最多会有10万个客户,不同的白名单有不同的用途。系统需要保存这些白名单以供规则运算。例如某款产品允许/不允许这个白名单内的人购买。
5、一个客户可能会有某些标签,但不一定会有。每个客户有哪些标签是不确定的,并且某个客户的标签数量可能很多,可能很少。通常为30+个至100+个。
三、分析用例场景
(一)主要用例场景:
1、客户在访问产品列表时,系统需要根据客户的标签及产品的标签进行实时规则运算。
2、营销过程中有时需要导入或者导出白名单,或者导出符合一定规则的客户名单。
3、需要有一个后台管理界面,提供给运营人员配置或者修改规则。
4、标签的种类过多,为了运营人员可以有效的利用标签设置规则,需要对标签进行分类管理。
5、标签是根据历史数据(T-1天),或当前事件(T+0天)进行计算的。大数据平台可以综合分析历史数据,计算客户的标签。但由于客户的数量比较庞大,计算出来的标签数量更是庞大。大数据平台不能很好的实时计算,而有些精准营销的规则要求比较实时,即客户最近的事件能及时参与到规则运算。当前事件(T+0天)要求在分钟级内计算与存储,以供实时访问。
(二)转化为初步需求(User Story)
1、根据客户ID,查找客户的所有标签。实现在标签系统,涉及Redis、MySQL。
2、根据客户ID、标签名,确认是否有某个标签或得到其标签值。实现在标签系统,涉及Redis、MySQL。
3、找到某个白名单的所有客户,并且可支持导出白名单。实现在BOSS(Back Office Support System)系统,涉及MySQL。
4、根据标签名、标签值,找到所有符合的客户。 实现在BOSS系统,涉及MySQL。
5、找出某个标签的所有枚举值。实现在BOSS系统,涉及MySQL。
6、历史数据清理,应该有物理删除及逻辑删除。标签肯定需要删除标志。实现在BOSS系统,涉及MySQL,Redis同步。客户的过期无效标签可以物理删除。停用白名单时可使用标签名的validity,customer_tag表不变,但相应的查询语句得考虑。移除白名单,物理删除关联标签名的客户标签。
7、设置白名单失效。实现在BOSS系统,涉及MySQL,Redis同步。
8、导出符合一定规则的客户名单。实现在BOSS系统,涉及MySQL,HBase;实际上是集合运算。
9、结合客户各种数据计算客户标签。实现在大数据平台,HBase+MapReduce。
10、由Kafka某个事件触发的客户标签计算及更新。 实现在标签系统,涉及MySQL,Redis。
三、基于MySQL数据库的设计
1、标签表 tag_key
create table tag_key (
tag_key_id int unsigned not null, // tag_key_id设计成short int,2个字节。
tag_key varchar(35) not null,
data_type TINYINT NOT NULL, //布尔型,字符串,数字区间;数字区间通过[ , )表示;
category TINYINT NOT NULL, //管理大类,//白名单,关于管理大类还需要另外设计一张小表。
validity TINYINT NOT NULL, //0表示无效,1表示有效;
description varchar(50) , //描述
display_name varchar(50),//显示在界面上的名字
created_date TIMESTAMP NOT NULL ,
creator VARCHAR(30),
updated_date TIMESTAMP NOT NULL,
updator VARCHAR(30),
primary_key(tag_name_id)
)
估算此表的最大行数为120行-500行,更新频度较低。可以直接缓存于应用系统。将白名单也设计成一类特殊的标签,可能只有一个特殊的标签值:“是”,这可以统一模型,简化设计。data_type数据类型的说明,有助于Java应用系统构建对象并且检查正确性,但实际在tag_value表都是以字符串形式存储,有利于扩展。相当于将数据的完整性约束从数据库转移到Java实现。
2、标签枚举值表
create table tag_value (
tag_value_id int unsigned not null,
tag_value varchar(100) not null,
tag_key_id int unsigned not null,
primary_key(tag_value_id)
)
估算最大行数:200key x 每个key对应25个value = 5000行。增加tag_key_id索引。
3、客户标签表
create table customer_tag(
customer_id int not null ,
tag_key_id int not null ,
tag_value_id int not null ,
tag_time TIMESTAMP NOT NULL,
created_date TIMESTAMP NOT NULL ,
creator VARCHAR(30),
updated_date TIMESTAMP NOT NULL,
updator VARCHAR(30),//不同的处理程序必须使用不同的身份update记录,通过不同的creator或者updator表示批处理作业程序、流处理作业程序、运营人员;
)
估算最大行数:客户数*(标签数/客户)=1000万*100=10亿;
4、客户的属性信息
客户的属性是比较确定的,确定的属性有确定的值类型,不同的属性有不同的值类型,属性不会经常增加或减少,属性可以通过系统间同步获得,也可以能过暴露出来的服务获得,但通常不能由运营人员直接导入。标签系统不是会员系统。必要时可以进行表关联操作。
5、找出某个标签的所有枚举值
select tag_value.tag_value_id tag_value_id, tag_value.value tag_value_string
from tag_value
INNER JOIN tag_key on tag_value.tag_key_id = tag_key.tag_key_id
WHERE tag_name.name=%name
四、基于Redis的缓存设计
(实际值得缓存的表只有两张)
1、缓存客户标签表
1.1 数据结构:使用Redis的散列
1.2 示例:conn.hmset(customer_id,{k1:v1,k2:v2})
customer_id k1 v1 k2 v2,全部是整数
1.3 解释:其实在进行规则匹配时,用不着标签的字符串。但可能用得着客户属性表的字符串或金额。所以Redis散列结构中不需要存放过多的字符串及其它的附属信息,可以直接使用,与数据库表结构保持一致,也可以在必要时更具效率。
1.4 占用空间评估:customer_id 4字节,tag_name_id 2字节,tag_value_id 4字节,一条MySQL记录大概10B,
假设全量缓存:1000万*100 *10字节 = K 万 *KB =万 MB = 10GB ,加上额外的存储空间,即20GB以内。需要3个节点集群*8GB/节点。
假设只用Redis服务器简单的主从部署结构,单个Redis节点最多可缓存一半的客户标签数据。足以应对活跃客户的查询请求。
1.5 如何保持同步更新:
确保MySQL数据库只有一个应用入口,即插入与更新操作,只有同一个Application可以操作。在这个Application进行删改操作时检查Redis同步删改。而在insert时不需理会Redis,在查时没有命中Redis缓存则从MySQL缓存至Redis,设置Reids缓存有效期为一个月(视Redis大小而定)。
2、缓存tag_name表
表的数据量很少,可以直接缓存在应用系统内存(评估最多为50KB),不需要缓存在Redis; Java应用多个实例无法共享这个缓存,需要有一个监听变化的机制。可以通过注册中心或配置中心实现。
3、缓存tag_value表
采用Redis数据结构:string,以tag.<tag_value_id> 为key ,以标签值对象序列化后的json字符串或字节数组为值。这种方式可以轻松获得一个完整的标签值对象。虽然在规则匹配时用不着,但在配置管理等后台界面中特别适用。
如何更新缓存:保持一个Application入口,改删同步,启动时全量缓存,定期轮询纠错。
4、白名单是否适合存放于Redis?
不适合,假设一个白名单有10万客户,一个白名单一个key,那对应的集合有10万*1KB,即100MB。这个对象太大了,传输时网络IO容易成为性能瓶颈,而且使用频次很低。
5、如何保持java缓存、Redis数据、MySQL数据的及时更新一致呢?
先访问缓存,如果不命中,则访问数据库,再更新缓存。先更新数据库,再移除缓存。
五、基于Hbase数据库的设计
1、表设计customer_tag
【三个列族】tag,p,ext;
tag:表示客户的标签;
p:客户的基本属性;客户的基本属性包括:姓名、性别、手机号、邮箱、年龄、信用评分 等。
ext:客户的扩展信息; 客户的扩展信息包括:总资产、登陆次数等;
行键:customer_id
对于tag列族,有列限定符:tag_key_id,对应的列值则为:tag_value_id。
六、基于标签的匹配规则
规则组: 表达式1 AND 表达式2 OR 表达式3 AND 表达式4
默认AND的运算优先级更高,相当于 (表达式1 AND 表达式2 ) OR ( 表达式3 AND 表达式4)
NOT ?
表达式1:%tag_name = %tag_value ,例如:age = 20-30
表达式2:线下渠道客户 !=某某公司
表达式3:新手客户 = 是; 表达式4:资产级别 = 白领级别。
支持EQ 或 NE,等值比较运算符;AND、 OR ,逻辑运算符。用负数表示,-1,-2,-3,-4。
一个规则在UI上显示为(表达式1 AND 表达式2 ) OR ( 表达式3 AND 表达式4)形式,存储于数据库及应用系统执行规则时,则是语法树形式,某个规则可能是:-4 -3 -1 873 945 -2 303 934 -3 -1 833 974 -1 893 865 。即:运算符在前,两个值在后。由于标签的值都是离散型可穷举的值,所以只能进行等值比较,不能进行大于或者小于这样的范围比较。
1、设计rule表
create table rule{
rule_id int primary key,
rule_name varchar(30), // 用于BOSS系统被运营人员识别的规则名字。
expression varbinary(100), //序列化字节数组表达的规则模板。
valid_from datetime, //规则的有效起始日期
valid_to datetime, //规则的失效结束日期
created_date TIMESTAMP NOT NULL ,
creator VARCHAR(30),
updated_date TIMESTAMP NOT NULL,
updator VARCHAR(30),
}
提供给营销人员自由配置的规则是基于UI简单拖拽而成的,不能支持非常复杂的规则。对于特别复杂的规则通过内置SPI实现,不仅仅可以利用标签库内的标签 ,还可以利用会员系统的客户表信息,甚至其它更复杂的数据计算、配置管理。
七、Java设计:
在应用系统中通过设计一个解释器(设计模式)专门用于匹配规则。这些规则可以构成一个解释器链。从数据库中一次SQL取出某个客户的所有标签及其值,然后ORM为一组标签对象,放到解释器链中进行匹配。
class Expression( Operator, Expression1,Expression2)
Enum Operator{EQ,NE,LE,GE,LT,GT,AND,OR,NOT} //运算符的枚举值
class TagOperator(Operator,tagNameId,tagValueId) extends Expression
客户属性的表,例如年龄、资产、登陆次数。客户属性不等于客户标签。客户标签有可能是客户属性。客户的属性可以参与规则运算吗?可以通过API接口与SPI算法实现。当涉及客户属性时,不能简单的通过BOSS UI配置规则,而是需要开发人员开发相应的SPI实现规则。BOSS UI可以指定某些营销场景使用特定的SPI规则而不仅仅是标签规则。
如何进行API与SPI的设计呢?
整个标签(客户画像、产品画像)的应用过程(规则运算),会涉及到3个实体:客户、产品、营销活动。上面的表达式、规则也都是通过BOSS UI设置绑定到这3个实体之一的,当客户访问系统某些功能或页面时触发计算。举个例子,公司推出了某个营销活动,主要是对某款理财产品补贴1%的收益用于拉新获客,要求满足下面要求的客户才可以参与这个活动:(1)来源于某个互联网平台(假设公司K)的客户,(2)不曾购买过同类产品的客户,(3)根据公司K的客户授权信息评估月收入达到白领标准。这个例子只是用于方便大家理解,不讨论其运营规则设置的合理性。客户授权信息是不能预先存储与分析打标签的,每个合作公司所能给出的评估信息却又各有不同,又要求实时规则运算,这就要求SPI的特殊定制的存在。
场景分析:客户访问系统时,可知客户的基本信息。要向客户推荐产品就要将所有在售产品过滤,找到合适的产品再排个优先级,将top1/2/3推荐出去。一个营销活动可能涉及多款产品,而一款产品也可能参与到多个营销活动。所以API可以简单设计成
Rule.execute(Customer cust, Prodcut ... prods);
SPI(Service Provider Interface)是JDK提供的一个机制,以Jar的形式存在,当Application在classpath中找到这个Jar,能加载相应的算法。SPI可以做得非常灵活,可以复用已有的标签,可以运算各种非标签信息,可以做成这个标签系统的强大的扩展机制。这些SPI规则虽然不同于BOSS UI的标签规则,却可以在BOSS UI中同等的被营销人员绑定到产品与营销活动中。
将这样的SPI规则设计成微服务(RPC或Restful API)是否可以呢?单从技术上来说是可以的。但这样的SPI不一定是完整的服务,更重要的是性能。产品可能有100个,运算一个SPI规则可能只需要3ms,而一次最简单的RPC网络IO耗时至少10ms,SPI本地Jar服务一个客户只需300ms,而设计成RPC则需1.5秒。这还没有计算可能的数据传输与数据访问的开销,实际差异应该更明显。
八、小结
这个设计方案相对于旧系统而言,最关键的在于对标签的重新理解,将标签的概念区别于客户的属性。将标签的值理解为可穷举的离散值,这样就可以用一个整数ID指代一个标签,参与到整个系统的存储与运算过程。字符串的存储及运算消耗的资源要比整数的存储及运算消耗的资源要多很多。将tag_key与tag_value设计成维度表的方式,所有涉及字符串的存储及运算在这里作为访问入口一次完成,使得客户标签这样的大表避开了占据大量存储资源及运算资源的字符串处理。为什么上面多次强调是可穷举的离散值呢?例如某客户的具体月收入,可能是2135.50元,可能是35605元,可能是58000元,它是连续的有两位小数的精确到金额“分”的值,如果存进tag_value表每个分值一行,就意味着可能需要数千万行,这是不现实的。另一方面,营销的规则确实需要考虑月收入,它是一个很重要的维度,这就要求系统设计要给出替代的方案,通过大数据分析的聚类算法或分类算法将连续的值转变为离散的值,例如月收入,可以经聚类分析确认(假设)3000元-30000元是“白领”,3万元-10万元是“金领”,那月收入就可以用标签“白领”、“金领”替代,而不用出现具体的金额。而这又会引入另外的2个问题。一是如何实时计算更新标签,这是设计中有Spark流计算的原因。二是可能有些规则还是需要一些更复杂的可能会涉及原始属性的计算,要说服运营人员放弃这些规则是不可能的,要设计出一个傻瓜式的用于设置这样规则的BOSS UI 也是非常困难的,这就需要引入SPI的设计。
关键的关键,理解业务,分析需求,抛开业务分析谈所谓的架构设计都是耍流氓。