4种多租户数据库设计方案对比及思考,一文全讲透
Posted 神州数码云基地
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了4种多租户数据库设计方案对比及思考,一文全讲透相关的知识,希望对你有一定的参考价值。
文章目录
前言
多租户是SaaS(Software-as-a-Service)下的一个概念,意思为软件即服务,即通过网络提供软件服务。
SaaS平台供应商将应用软件统一部署在自己的服务器上,客户可以根据工作的实际需求,通过互联网向厂商定购所需的应用软件服务,按定购的服务多少和时间长短向厂商支付费用,并通过互联网获得SaaS平台供应商提供的服务。SaaS服务尤其利于一些中小企业,以低成本实现自己的软件需求。
就如企业微信,它就是一个典型的多租户系统。每在企业微信上注册一个企业,也就是多租户下创建一个租户。企业微信提供各种插件、服务、第三方支持供租户购买,拓展系统功能,可以说是天花板的存在,他们的产品设计是很值得我们参考与学习的。
我本人也是对各种多租户开源项目进行过研究,参与开发过不少类型的多租户系统,对多租户系统的设计有一些自己的见解,本期就来跟大家分享一下关于多租户DB的设计方案。
一、设计方案
多租户对于用户来说,最主要的一点就在于数据隔离,不可以说,我登了A用户的号,但是看到了B用户的数据。
因此,多租户的数据库设计方案和代码实现就相当有必要考虑了。
当下,开发者们普遍接受的多租户设计方案,常见的大概就四种:
- 所有租户使用同一数据源下同一数据库下共同数据表(单数据源单数据库单数据表)
- 所有租户使用同一数据源下同一数据库下不同数据表(单数据源单数据库多数据表)
- 所有租户使用同一数据源下不同数据库下不同数据表(单数据源多数据库多数据表)
- 所有租户使用不同数据源下不同数据库下不同数据表(多数据源多数据库多数据表)
二、方案剖析
注:对于本节,我们主要采用开源的mysql数据库来分析设计。
- 方案一:单数据源单数据库单数据表
这种方案是我目前见过的最普遍的设计方案。
该系统只有一个数据库,所有租户共用数据表。在每一个数据表中增加一列租户ID,用以区分租户的数据。增删查改时,一定要带上租户ID,否则就会操作到其他租户的数据。因此,这里的设计一定要重点考虑!
一个重要的点:我们要保证的就是一定不要忘记带上租户ID。一个很好的方案就是通过AOP的方案,隐式的为我们的每一个SQL带上这个租户ID。
我个人是更喜欢使用MyBatis来操作数据库的。它提供了插件的机制,我们可以通过拦截它提供的四大组件的某些对象,某些方法,来操作SQL,动态的为我们的SQL拼接上租户ID字段。
当然,MyBatis-Plus高版本提供了更加方便的拦截器,并且已经将多租户插件放入JAR包,我们只需稍加实现,并将该插件加入到MyBatis的拦截器链中,就可以不用再显式的拼接租户ID字段了,降低了出错的概率。
因为篇幅有限,关于MyBatis和MyBatis-Plus插件这里就不做太多知识延伸,如果有兴趣可以留言我专门写一期MyBatis插件的文章。
这里我放一下MyBatis-Plus关于多租户的主要代码 ⬇
@Configuration
@MapperScan("com.baomidou.mybatisplus.samples.tenant.mapper")
public class MybatisPlusConfig
/**
* 新多租户插件配置,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存万一出现问题
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor()
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler()
@Override
public Expression getTenantId()
return new LongValue(1);
// 这是 default 方法,默认返回 false 表示所有表都需要拼多租户条件
@Override
public boolean ignoreTable(String tableName)
return !"user".equalsIgnoreCase(tableName);
));
// 如果用了分页插件注意先 add TenantLineInnerInterceptor 再 add PaginationInnerInterceptor
// 用了分页插件必须设置 MybatisConfiguration#useDeprecatedExecutor = false
// interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
// @Bean
// public ConfigurationCustomizer configurationCustomizer()
// return configuration -> configuration.setUseDeprecatedExecutor(false);
//
也就是说,我们只需要根据我们的业务,重写getTenantId()方法和ignoreTable()方法即可。
ignoreTable()其实没啥说的,主要还是getTenantId()这个方法。最方便的方法当然是使用ThreadLocal存储当前线程用户的租户信息,然后直接从里面取出即可。
如果使用了SpringSecurity,用户信息和租户信息也可以存他的内置ThreadLocal中。
一般来说,开源框架都会提供一个SecurityUtil工具,可以直接拿到当前线程的用户信息和租户信息。当然,也可以自己实现,这并不困难。
有了这个思路,其他的数据库也可以使用类似的方法实现隐式租户ID赋值,这里我就不再举例。
- 方案二:单数据源单数据库多数据表
这个方案是我之前项目所使用的,架构师是一个十几年经验的大牛,我觉得这个思路也是很好的,我可以详细的说一下:
这个项目使用的是MongoDB数据库,MongoDB使用的是类似于Json的Bson语法,实现了类似SQL的功能,使用Bson进行增删查改,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引。
它的好处就是,不用太过于关心数据库的建表,因为它的存储类似于一行一行的json对象,插入一条数据即可理解为建表。表增加一行或者减少一行这种变化也不需要去修改数据表的结构。其次,表名也可以自己来指定。
项目的设计方案是,**所有租户的数据都放在同一数据库中,不同租户的数据表使用后缀来区分。**比如租户A的用户表叫user_001,用户B的用户表叫user_002。这样,就做到了相对于方案一而言更可靠的设计。
缺点也很明显,这种数据库难以实现连表查询,只能进行多次单表查询,对事务的控制基本为0。
一般来说,对于小场景而言,这种架构的设计已经完全够用了。服务器和数据库都使用了阿里云的云服务器和云数据库,数据的可靠性由他们来保证,我们只需要专心写代码即可,发生错误的概率也是非常低的。就算真的以极小的概率发生了数据问题,对于当时项目的规模和成本考虑来说,查一下审计日志,手动做一下数据的处理就可以解决了。
实际上,这个项目还是一个电商项目,当时同时支持了3个租户同时使用,并没有出现很大的问题。因此,在我们的设计中,也要根据场景进行架构的选择,没必要追求过于超前的设计,毕竟越稳定越花钱,成本决定了很多因素,通过对这个项目的研究和与领导的沟通,我也收获颇多。
接下来,我们回头再说一下MySQL的设计方案,毕竟mysql免费,并且支持事务和多表查询,所以它永远是我们的最先考虑的。
对MySQL而言,新建租户时需要建表,每次建表,都要使用DDL建新租户所使用的所有表。我自己的做法是,定义建表的DDL,当需要创建租户时,使用MyBatis执行SQL脚本。
我自己的实现方案是在XML里面定义SQL语句,项目启动时,解析XML到配置,存为Key-Value形式,并提供一个执行SQL的工具,当需要创建租户时,调用SQL工具并传入配置参数,即可实现自动建表。
下面给出我的具体实现:
Step 1
提供一个XML,记录SQL的对应关系,这里我没有写DTD约束。如果怕出错,可以自己加上,也可以在里面定义标签,标注需要替换的键值。
我这里做的比较简单,**后期使用对应的键值替换SQL中的 x x x 元素 ∗ ∗ ,比如我这里后期使 用 0 01 替换这个 xxx元素**,比如我这里后期使用_001替换这个 xxx元素∗∗,比如我这里后期使用001替换这个tenant_id,这样就实现了动态生成创建新租户表的DDL。
<?xml version="1.0" encoding="UTF-8"?>
<document>
<statement>
<name>create-tenant</name>
<script>
DROP TABLE IF EXISTS `sys_user$tenant_id`;
CREATE TABLE `sys_user$tenant_id`
(
`user_id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '用户编号',
`account_user_id` bigint(20) UNSIGNED NOT NULL COMMENT '账户用户编号',
`nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '昵称',
`real_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '姓名',
`phone` char(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '手机号',
`avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户头像',
`email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户邮箱',
`gender` tinyint(4) UNSIGNED NOT NULL COMMENT '性别[enum]',
`department_id` bigint(20) UNSIGNED NOT NULL COMMENT '部门编号',
`enable` tinyint(4) UNSIGNED NOT NULL COMMENT '启用禁用[enum]',
`last_login_time` datetime(0) NOT NULL COMMENT '最后登录时间',
`last_login_ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '最后登录IP',
`create_time` datetime(0) NOT NULL COMMENT '创建时间',
`update_time` datetime(0) NOT NULL COMMENT '更新时间',
`create_user_id` bigint(20) UNSIGNED NOT NULL COMMENT '创建人编号',
`update_user_id` bigint(20) UNSIGNED NOT NULL COMMENT '更新人编号',
`is_deleted` tinyint(4) UNSIGNED NOT NULL DEFAULT 0 COMMENT '逻辑删除[enum]',
PRIMARY KEY (`user_id`) USING BTREE
) ENGINE = InnoDB
AUTO_INCREMENT = 2
CHARACTER SET = utf8mb4
COLLATE = utf8mb4_general_ci
ROW_FORMAT = Dynamic;
......
</script>
</statement>
</document>
Step 2
编写配置类,解析XML到JAVA,使用hashMap保存SQL的对应关系。这里我使用的是dom4j,当然你也可以选择其他解析器。
/**
* @author 谷子毅
* @date 2022/4/2
*/
@Component
public class ExecSqlConfig
private final Map<String, String> sqlContainer = new HashMap<>();
private ExecSqlConfig() throws Exception
// 1.创建Reader对象
SAXReader reader = new SAXReader();
// 2.加载xml
InputStream inputStream = new ClassPathResource("sql-script.xml").getInputStream();
Document document = reader.read(inputStream);
// 3.获取根节点
Element root = document.getRootElement();
// 4.遍历每个statement
List<Element> statements = root.elements("statement");
for (Element statement : statements)
String name = null;
String sql = null;
List<Element> elements = statement.elements();
// 5.拿到name和script加载到内存中管理
for (Element element : elements)
if ("name".equals(element.getName()))
name = element.getText();
else if ("script".equals(element.getName()))
sql = element.getText();
sqlContainer.put(name, sql);
public String get(String name)
return sqlContainer.get(name);
Step 3
编写SQL的执行器工具类,具体的用法就是,通过key从hashMap中取出对应DDL类型的SQL模板,对模版中的替换值进行替换,然后就生成了可以执行的SQL,使用MyBatis执行即可。
/**
* sql脚本执行工具
* @author 谷子毅
* @date 2022/4/2
*/
public class ExecSqlUtil
private static final ExecSqlConfig EXEC_SQL_CONFIG = SpringContextHolder.getBean(ExecSqlConfig.class);
private static final DataSource DATA_SOURCE = SpringContextHolder.getBean(DataSource.class);
@SneakyThrows
public static void execSql(String name, Map<String, String> replaceMap)
// 获取SQL脚本模板
String sql = EXEC_SQL_CONFIG.get(name);
// 替换模板变量
for (Map.Entry<String, String> entity : replaceMap.entrySet())
sql = sql.replace(entity.getKey(), entity.getValue());
ScriptRunner scriptRunner = new ScriptRunner(DATA_SOURCE.getConnection());
// 执行SQL
scriptRunner.runScript(new StringReader(sql));
Step 4
最后,在创建租户的地方,使用SQL执行工具类执行SQL即可。
public void addTenant(TenantInsertDTO dto)
// 在租户表中新增租户信息
......
// 执行sql语句创建新租户的数据库表
ExecSqlUtil.execSql(SqlStatement.CREATE_TENANT, Collections.singletonMap("$tenant_id", dto.getTenantId()));
// 向新建的数据表中插入租户的一些初始数据
......
以上即为建表方案,不过需要注意,如果租户表有变化,所有的租户表都需要同时进行相应修改,否则就会出现问题。
说完建表,那下一步需要关心的还是增删查改的问题。这个问题同样也可以使用MyBatis的插件解决,MyBatis-Plus同样给我们写好了代码放到了JAR包中,也就是动态表名插件。
这里我也只放一下MyBatis-Plus关于动态表名插件的主要代码 ⬇
@Configuration
@MapperScan("com.baomidou.mybatisplus.samples.dytablename.mapper")
public class MybatisPlusConfig
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor()
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
DynamicTableNameInnerInterceptor dynamicTableNameInnerInterceptor = new DynamicTableNameInnerInterceptor();
dynamicTableNameInnerInterceptor.setTableNameHandler((sql, tableName) ->
// 获取参数方法
Map<String, Object> paramMap = RequestDataHelper.getRequestData();
paramMap.forEach((k, v) -> System.err.println(k + "----" + v));
String year = "_2018";
int random = new Random().nextInt(10);
if (random % 2 == 1)
year = "_2019";
return tableName + year;
);
interceptor.addInnerInterceptor(dynamicTableNameInnerInterceptor);
// 3.4.3.2 作废该方式
// dynamicTableNameInnerInterceptor.setTableNameHandlerMap(map);
return interceptor;
它这里的动态表名还可以实现类似于分表的操作,比如按时间分表,2021年的数据放到order_001_2021,2022年的数据放到order_001_2011。
我们这里主要讨论的还是关于租户的分表,我们将代码稍加改造,即为:
dynamicTableNameInnerInterceptor.setTableNameHandler((sql, tableName) ->
if(IgnoreTables.contains(tableName))
return tableName;
String tenantId = SecurityUtil.getTenantId();
return tableName + "_" + tenantId;
);
如果当前操作的表名在忽略表名集合中,则不拼接租户ID,否则所有的表名后面都会自动的被插件拼接上租户ID。
如原SQL的查询select * from user就会变成select * from user_001。
- 方案三:单数据源多数据库多数据表
方案三则是将租户数据彻底隔离,给每个租户创建一个数据库。基本上和方案二类似,关于好处和坏处我们后面统一讨论,还是先说一下这个实现方案。
同样的,新建租户的时候需要建库建表,那么我们是可以直接继续使用方案二的,将SQL模板稍加改造即可,经过上一方案的代码剖析,这个应该没有难度。
<?xml version="1.0" encoding="UTF-8"?>
<document>
<statement>
<name>create-tenant</name>
<script>
CREATE DATABASE $database;
USE $database;
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user`
(
`user_id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '用户编号',
`account_user_id` bigint(20) UNSIGNED NOT NULL COMMENT '账户用户编号',
`nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '昵称',
`real_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '姓名',
`phone` char(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '手机号',
`avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户头像',
`email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户邮箱',
`gender` tinyint(4) UNSIGNED NOT NULL COMMENT '性别[enum]',
`department_id` bigint(20) UNSIGNED NOT NULL COMMENT '部门编号',
`enable` tinyint(4) UNSIGNED NOT NULL COMMENT '启用禁用[enum]',
`last_login_time` datetime(0) NOT NULL COMMENT '最后登录时间',
`last_login_ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '最后登录IP',
`create_time` datetime(0) NOT NULL COMMENT '创建时间',
`update_time` datetime(0) NOT NULL COMMENT '更新时间',
`create_user_id` bigint(20) UNSIGNED NOT NULL COMMENT '创建人编号',
`update_user_id` bigint(20) UNSIGNED NOT NULL COMMENT '更新人编号',
`is_deleted` tinyint(4) UNSIGNED NOT NULL DEFAULT 0 COMMENT '逻辑删除[enum]',
PRIMARY KEY (`user_id`) USING BTREE
) ENGINE = InnoDB
AUTO_INCREMENT = 2
CHARACTER SET = utf8mb4
COLLATE = utf8mb4_general_ci
ROW_FORMAT = Dynamic;
......
</script>
</statement>
</document>
不过我们需要注意的是,当前是在MySQL环境下,Database的概念和Scheme的概念是等价的。其实在其他数据库中,还可以存在database-scheme-table的结构,这里不说太多,如果你真正用过多种数据库后,你就理解我的意思了。
在完成数据表的创建后,然后就是具体的增删改查问题。我们还是可以使用MyBatis插件,动态的改造表名,在表名前拼接上数据库名,具体的实现也是非常简单,直接拿方案二稍微改改即可。
最后的效果是,将原SQL语句select * from user 动态改成了select * from db_001.user,连表查询也是可用的。
dynamicTableNameInnerInterceptor.setTableNameHandler((sql, tableName) ->
if(IgnoreTables.contains(tableName))
return tableName;
String tenantId = SecurityUtil.getTenantId();
return tenantId + "." + tableName;
);
以上三个方案都是单数据源的情况,使用这种单数据源方案可以轻松实现跨数据库执行SQL和事务执行。
我自己的开源项目sherly-SpringBoot目前也是这样设计的。不过我的项目主要是针对中小型项目设计的,因此,我认为这已经完全够用了,后面我还会开发针对大数据量的微服务框架来专门解决大数据量的问题。
- 方案四:多数据源多数据库多数据表
在单数据源中,跨数据库操作和数据库事务是已经被支持的。
但是在多数据源中,传统的跨库操作和数据库事务已经难以适用,甚至有时候会出现动态的新增租户,并且租户数据源动态配置等问题。
这时,光靠简单的几百行代码已经无法实现。首先我们试着想想如何自己实现该需求,这里我给大家提供几个方案来看看:
- 使用AOP加注解动态切换数据源
- 使用MyBatis插件切换数据源(可以比较方便的判断SQL类型,区分读写操作)
- 多个MyBatis配置来引入多个数据源(比较占内存)
具体的实现方案就是在配置文件中配一个公用数据源。项目启动后,从数据库加载所有租户的数据源信息,创建DataSource并注入到容器,供三种方案下一步使用。
好的,我们已经解决了一个多数据源切换问题,那么多数据源的事务应该怎么控制呢?
如果不考虑多租户和分布式事务方案的话,简单的业务多数据源处理,我们可以使用声明式事务,稍微麻烦一点,A数据源操作->B数据源操作->出现异常。
这种场景,我们使用一下编程式事务貌似也可以解决,但是代码已经很丑了,更别说多租户这种动态选择数据源的场景了,简直难度陡升。
通过以上分析,我们发现多数据源下多租户确实难以简单实现,光涉及到我提到的两点,就已经让人头痛了。
所以,我们就需要借助第三方框架了,那就是Dynamic-datasource。它是一个基于SpringBoot的快速集成多数据源的启动器,通过它的辅助,我们就可以实现方案四的多数据源下多租户问题。
下面我先把他的一些特性列出来:
这个开源组件的功能是非常强大的,大家可以自己拉Git仓库研究,在本节中,我们只讨论它是如何帮助我们实现多租户的多数据源问题。
我的设计方案是:
首先,默认只加载公共数据源,然后创建一个数据源工厂接口,提供一个获取租户数据源接口,实现就是传入租户ID,查询数据库获取租户数据库连接信息,创建并返回租户数据源,存入数据源容器。若淘汰,从数据源容器删除即可。
再通过Seata的分布式事务,就可以完美解决我们的问题了。
在创建租户时,也可以初始化表结构和数据库。当然,你也可以项目启动后,直接把所有的租户数据源查出来并放到容器,这个根据自己的业务来选择。
这里设计非常复杂,我就先提供思路吧。我会在我的下一个微服务开源项目中实现这一方案,可以期待一下。这时有人问,那如果是超大型系统,微服务,多部署又怎么办呢?怎么做到数据源容器的新增和删除同步呢?
当然,我考虑到了,可以试试Redis的listener,让所有的机器监听一个事件消息。通过这个第三方组件,再根据自己的业务稍加改造,所有问题便迎刃而解了。
三、方案总结
上面说了那么多,是时候做一下总结。
从方案一到方案四,数据的隔离性是越来越好的,成本也越来越高,但是也会出现越来越多的问题,就比方说有数据汇总的需求时,租户数据都在一个表中和分散在不同表中,处理难度是不一样的。
- 方案一总结
该方案成本最低,安全性也是最低,所有数据都放在一起,单个租户的数据恢复和备份会很复杂。
比如,有ABC三个租户共同使用,当C租户已经确定以后不再使用了,他的数据也不好从数据库中剔除。如果某一数据出现了问题,也不便于租户问题的排查。
这就得保证对开发者的严苛要求,最主要还是增加了对数据的管理复杂度。尤其注意,尽量不要去手动显式的设置租户ID到SQL,否则一旦出现问题,纠正是很复杂的,在这上面我吃过亏…
如果要做数据汇总,我们就想办法使用插件或其他方案去解决就行了。这和数据出错的影响比起来,这点设计也不值一提了。
- 方案二总结
该方案提供了一定的数据隔离,但不是完全的隔离,由于数据库是共享的,所以成本也不是很高。
- 方案三总结
该方案其实和方案二大同小异,对单数据源来说,都不是彻底的隔离。成本和方案二差不多。
不过我认为这样可以更方便的删除测试租户数据库和设置数据库权限,比如给A租户管理员的A数据库的查看权限,给B租户管理员的B数据库的查看权限,这就涉及到数据库的权限问题了,其实很多时候,很多的公司,都给了开发者数据库的root权限。
- 方案四总结
这种多数据源方案,目前也有一些开源项目实现了这一方案。就是为不同的租户提供独立的数据库,数据库由租户自己购买,或商家购买,然后配置到系统中,满足不同租户的独特需求,如果出现故障,数据恢复也比较简单。
代价是什么呢?
这种方案增加了数据库的安装数量,单个数据库的资源利用率就不高了,而且数据库的数量一多,成本就高了,安全性的代价就是成本。还要考虑到多数据库的事务问题,多数据库的数据源切换问题,这代码复杂度和成本噌噌噌就上来了,相当费钱。
四、方案选型
- 数据隔离性方面:
关于隔离性最常见的场景就是给多个银行开发的系统,那安全性是不言而喻的,甚至是容不得半点闪失。
此时的方案,我们一二三都不用考虑,可以直接选择方案四。方案四
一文讲透推荐系统提供web服务的2种方式
作者丨gongyouliu
编辑丨zandy
来源 | 大数据与人工智能(ID: ai-big-data)
推荐系统是一种信息过滤技术,通过从用户行为中挖掘用户兴趣偏好,为用户提供个性化的信息,减少用户的找寻时间,降低用户的决策成本,让用户更加被动地消费信息。
推荐系统是随着互联网技术的发展及应用深入而出现的,并在当前得到广泛的关注,它是一种软件解决方案,是toC互联网产品上的一个模块。用户通过与推荐模块交互,推荐系统通过提供的web服务,将与用户兴趣匹配的标的物筛选出来,组装成合适的数据结构,最终展示给用户。推荐系统web服务是前端和后端沟通的桥梁,是推荐结果传输的最后通道,信息传输是否通畅,传输是否足够快速,对用户体验是有极大影响的。
本文我们就来讲解推荐系统提供web服务的两种主要方式,这两种方式是企业级推荐系统最常采用的两种形式。
具体来说,这篇文章我们会从什么是推荐系统web服务、推荐系统提供web服务的两种方式、事先计算型web服务、实时装配型web服务、两种web服务方式的优劣对比、影响web服务方案的因素及选择原则等6个部分来讲解。通过本文的介绍,期望读者可以深刻理解这两种web服务方式的具体实现方案以及它们之间的差别,并具备结合具体的业务场景来决策采用哪种方式的能力。
什么是推荐系统web服务
作者在《》第一节中已经对推荐系统web服务进行了简单介绍,这里为了让读者更好地理解本文的知识点,以及为了内容的完整性,对推荐系统web服务进行简略介绍。
用户与推荐系统交互的服务流程见下面图1,用户在使用产品过程中与推荐模块(产品上提供推荐能力的功能点)交互,前端(手机、PC、Pad、智能电视等)请求推荐web服务,推荐web服务获取该用户的推荐结果,将推荐结果返回给前端,前端通过适当的渲染将最终的推荐结果按照一定的样式和排列规则在产品上展示出来,这时用户就可以看到推荐系统给他的推荐结果了。
图1:用户通过推荐web服务获取推荐结果的数据交互流程
上图中的绿色虚线框中的数据交互能力就是推荐web服务的范畴,它是前端(也叫终端)与后端的互动,图中蓝色方块(推荐web服务模块)是部署在服务器上的一类软件服务,它提供HTTP接口,让前端可以实时与之交互。用户与终端的交互属于视觉及交互设计范畴,虽然与推荐web服务无直接关系,但是是整个推荐服务能力完整实现必不可少的一环,也是用户可以肉眼直接感知到的部分,在整个推荐系统中非常重要,对推荐系统发挥价值有极大影响,不过不在我们这篇文章的讨论范围,对这一块感兴趣的读者可以参考《》这篇文章。
一是从推荐结果存储数据库中读出该用户的推荐结果,按照实时推荐算法逻辑对推荐结果进行修改,再将推荐结果存进去替换掉,另外一种做法是,增加一个中间的镜像存储(可以采用HBase等,现在业界很多推荐算法都基于Hadoop/Spark平台实现,用大数据生态系的HBase是比较好的选择),所有的算法逻辑修改只对镜像存储进行操作,操作完成后,将镜像存储中修改后的推荐结果同步到最终的推荐库中,这跟T+1更新就保持一致了,只不过现在是实时推荐,同一个用户可能一天会更新多次推荐结果。作者公司的短视频实时推荐更新就是采用后面的这种方案,感兴趣的读者可以参考《 》这篇文章第三节1中的介绍。
-
[基于TensorFlow Serving的深度学习在线预估] https://zhuanlan.zhihu.com/p/46591057 -
[手把手教你使用TF服务将TensorFlow模型部署到生产环境] https://zhuanlan.zhihu.com/p/60542828 -
https://www.tensorflow.org/serving -
https://github.com/facebookresearch/faiss
【end】
◆
精彩推荐
◆
你点的每个“在看”,我都认真当成了AI
以上是关于4种多租户数据库设计方案对比及思考,一文全讲透的主要内容,如果未能解决你的问题,请参考以下文章