您的位置:首页 > 其它

Couchbase案例实战分析之----用户概要存储(User Profile)数据建模讲解(Part 1)

2015-06-23 11:10 926 查看
​任何企业都有存储和查询用户数据的需求,一般情况下,这些数据是持久化的,例如统一定义的用户的账户,用户历史数据和用户偏好数据等等;同时,企业里也存在着 很多“临时的”用户相关的数据,例如一些匿名的用户会话中保存的用户近期访问产生的相关数据。无论是那种类型的用户数据,企业或机构组织都需要一个集中的用户概要存储 User Profile Store来保存和查询这些数据。因为对于很多企业来说,用户数据在很多应用中都会被用到,所以用户数据集中存储池需要有非常好的查询性能,可用性,以及高扩展能力应对用户的迅速增长。

下面在这篇博客中,我给大家讲解一下,底层使用Couchbase作为数据存储,结合数据建模,以Rest API形式访问的User Profile 服务,这是一个很有实战参考性的例子,数据建模的部分能最好的结合Couchbase产品的性能。首先讲解把所有信息都存储在一个文档里,然后讲解如何把信息进行范式化,使用不同的文档表达不同的信息,以及这些文档之间的关系;通过这篇博客,希望大家能理解怎样让用户概要数据的查询有最好的性能,以及如何更好的使用Couchbase

定义用户概要服务  User Profile Service

第一步定义示例Service 以及它的参数(具体的Service样例代码,会放在本篇系列的第二部分),以下先讲解一下这个示例项目中的设计思路和目标

设计一个用于做用户验证的Service,提供给所有机构用来做登录用用认证,并可以通过Rest API的方式来支持机构的CRUD操作。

API应该有版本,根据文档的设计变更,每个版本可能有新增和老旧的功能。

保证99.99%的在线服务

可以设置用户概要数据的生命周期,即用户最后一次登录后X 年后,删除对应的用户信息;数据的管理和删除由数据库自动完成,允许应用来设计X的数值。

服务即使基于AWS的普通示例,也应该可以支撑每秒5万用户的认证和1万用户的查找的并发操作。

定义Rest API 提供的服务

首先要分析一下数据怎样被使用,数据最多的和最少的访问路径是哪些,这些总结会直接帮我们最好的设计文档结构。

如下是这个服务需要实现的方法,并不一定是全部,但是列出如下,让大家能明白设计思路。

getUserProfile(userID) - 使用UserID取得ID对应的全部信息。 

getSecurityQuestions(userID) - 获取登录文档;如果用户是活跃状态,返回用户的安全问题文档,如果用户是非活跃状态,返回错误;

setSecurityQuestions(userID) - 写入userID对应的用户的安全问题文档

authorizeUser(userID, passwordHash, IP) - 获得该用户的认证文档,调用isEnabled(userID)检查是否活跃用户 ,如果是活跃用户检查密码是否匹配,如果匹配,那么用IP和登录时间来更新认证文档里面的IP和last-login字段,然后调用resetTTL()方法重置TTL值。

isEnabled(userID) - 检查用户的登录文档,看账户的启用状态或者是否是存在的用户,返回结果为true或者false

我将会在这篇文章的第二部分,给出全部的方法列表以及实例程序代码。

初始的用户主文档

这是一个用户概要文档的示例,这个文档的设计是里面包含了我要做的这个用户认证服务需要的全部信息,这也是一种可用的文档设计思路。

key : hernandez94
{
"firstName" : "Jennifer",
"middleName" : "Maria",
"lastName" : "Hernandez",
"addresses" : [
{ "type" : "home", "addr1" : "1929 Crisanto Ave", "address" : "Apt 123", "addr3" : "c/o J. Hernandez", "city" : "Mountain View", "state" : "CA", "country" : "USA", "pcode" : "94040" },
{ "type" : "work", "addr1" : "2700 W El Camino Real", "addr2" : "Suite #123", "city" : "Mountain View", "state" : "CA", "country" : "USA", "pcode" : "94040" },
{ "type" : "billing", "addr1" : "P.O. Box 123456", "city" : "Mountain View", "state" : "CA", "country" : "USA", "pcode" : "94041" }
],
"emails" : [
{ "type" : "work", "addr" : "work@email.com" },
{ "type" : "personal", "addr" : "personal@email.com", "primary" : true }
],
"phones" : [
{ "type" : "work", "num" : "+12345678900" },
{ "type" : "mobile", "num" : "+12345678901" }
],
"createdate" : “2014101454”,
"lastlogin": "a-date-goes-here",
"pword": "app-hashed-password",
"loc": "IP or fqdn",
"enabled" : true,
"sec-questions" : [
{ "question1" : "Security question 1 goes here", "answer" : "Answer to security question 1 goes here" },
{ "question2" : "Security question 2 goes here", "answer" : "Answer to security question 2 goes here" },
{ "question3" : "Security question 3 goes here", "answer" : "Answer to security question 3 goes here" }
],
"sec-roles" : [101, 301, 345]
}

上面这个示例文档的大小有1.4KB,而且并不是全部的属性字段都已经赋值,所以随着用户的活动这个文档可能变得更大。如果开发人员喜欢不断的增加数据,那么使用这种全文档的设计思路,随着应用和服务的演进,文档会变得越来越大。

优化用户主文档

为这个服务设计最优的数据模型设计,如果能够理
e8f3
解Couchbase的独特之处会很有帮助,例如需要明白Couchbase的易扩展,高性能和基于文档对象级别的高效内存管理。因为所有的数据都有机会缓存在管理内存层中,那么我们的数据模型设计和基于Key值的访问设计需要足够灵活,以保证我们经常访问的热度数据(本例中就是登陆频繁的活跃用户)会缓存在内存中,而不活跃用户对应的文档数据可以保存在磁盘上,相对服务的响应速度慢一些(因为用户是非活跃用户);其他的文档数据库可能会建议做应用设计时把全部信息放在一个文档里,然后使用查询query能力来查询具体的属性;数据查询,query能力Couchbase同样具备,我们的优势还在于我们提供ANSI
SQL兼容的SQL语言来操作文档数据,我会在后面讲解怎样使用数据查询能力查询数据,但是首先我想先让你理解如何使用Couchbase 内存的缓存层和灵活数据模型的能力,实现高效的数据查询性能。

所以在第一篇中,我做数据模型设计的宗旨是让尽量多的数据访问通过Key值的直接查询即可以满足,尽量少的使用数据查询来访问数据。因为基于key值的get/set操作,一定优于数据查询的心性能。所以我使用了基于key值查询和范式化的数据模型。范式化的数据模型好处在于我们只需要读写我们需要操作的数据,而不需要操作整个包含全部信息的大文档。可能这种设计思路,你并不熟悉,但是掌握这种思路后,你可以使用Couchbase完成每秒几万到几十万的操作,如果硬件能力高,OPS会更高。在本篇文章的第二部分,我会再讲解关于数据查询,以及分析型负载相关的内容。

所以本篇,我将数据模型设计的重点放在让应用尽可能只访问操作需要的数据,通过Key值访问的模式,而不是数据查询的方式,数据查询的方式包括,比如通过索引,视图,数据连接Join操作等等。这篇的设计目的是让你的应用有最好的性能和最好的扩展性。

登录文档的设计 (key 值格式为  key:login-info:)

首先,我们把用户登录信息放在单独的文档中,原因如下述,同样的原因也适用于后面讲述的其他文档。

登录信息会频繁的被访问,因为用户认证功能是认证服务最多被调用的,但是用户的其他信息在登录认证的时候,并不需要被访问。

对于用户登录验证,我们只需要返回141字节的文档而不是1.41K的全部信息(用户主文档)

从性能角度来看,将141字节的小文档缓存在内存层,比1.41K字节的大文档效率高,尤其是大部分读写操作并不需要全部信息;如果使用大文档,在10万用户规模,每秒1000操作的情况下,性能是没有问题的,但是如果用户激增到百万甚至千万,每秒的OPS增长到几十甚至十几万之上,那么为了保证性能,需要扩容的成本也相对高,比如缓存层的扩容。(因为Couchbase的机制是所有基于key值的操作都是和缓存层进行的。)

需要保证频繁访问的用户,对应的数据可以缓存在管理内存层中(缓存层),提供最快的响应速度。

每次用户成功登陆后,还是进行对应的文档更新操作,即用当前时间来更新如下示例文档的lastlogin字段,以及用客户的IP更新loc字段。更新这些值,是为后面我们做分析统计时使用;这些更新操作只需要操作下面这个示例的登录小文档。同时,除了文档更新外,我们还会更新这个文档对应的元数据中的TTL值,即登录时间+X年(X年是设计的用户的生命周期),另外的enable字段,是来标示这个用户是否活跃用户是否可以登录。

key : login-info::hernandez94
{
"lastlogin": "a-date-goes-here",
"pword": "app-hashed-password",
"loc": "IP or fqdn",
"enabled" : true
}

另一个设计重点是,这个文档的key值设计。这个设计模式我还用于这个应用的其他文档。应用可以很容易的生成key时,拼接上用户对应的唯一用户名。那么使用这个带有用户名的key值就可以完成用户登录文档的查找。把用户名放在key值里,而不是文档中的属性,好处在于可以利用key值查找而不用再对文档中的用户名字段构建索引再进行查找。尤其在数据存储在内存时,速度非常之快。所以这种设计的初衷就是让应用最多使用的查找路径有最好的性能。

同时,也可以选择将启用 enabled字段,作为一个单独的文档,因为couchbase也支持最简单的kv存储,value可以是布尔值,因为也有场景是只需要知道用户是否是活跃用户,而不需要其他信息;不过在我的示例中,我把enabled作为一个字段放在了登录文档里,因为对于我假设的应用数据访问模式,我觉得使用登录文档和如下几个要介绍到的文档已经范式化的足够满足需求了。

总结一下,我把登录信息从用户主文档里拆出来,专门作为一类文档来存储,这样每次进行登录校验的时候,只需要读取和更新一个小文档;当这种文档是频繁访问的数据时,couchbase的管理内存层中cache了访问频度高的全部文档,所以查找的效率极高。另外文档的key中包含了用户名,所以一次key值查找就能返回对应的数据,无需其他数据查询功能的使用。

安全问题文档 的设计( key 值为 key:sec-questions::用户名)

安全问题也被拆出放在单独的文档,因为安全问题不会像登录文档中的信息一样,每次登录验证都会被访问到;而且应用需要访问到这些信息的时候,也不会需要其他的用户信息,例如地址,邮件等等,所以它们很适合拆分为单独的文档,key的设计思路和登录文档一样,应用将用户名拼接在key值里,方便基于key值的直接查找。
key : sec-questions::hernandez94

{
"sec-questions" : [
{ "question1" : "Security question 1 goes here", "answer" : "Answer to security question 1 goes here" },
{ "question2" : "Security question 2 goes here", "answer" : "Answer to security question 2 goes here" },
{ "question3" : "Security question 3 goes here", "answer" : "Answer to security question 3 goes here" }
]
}


另外,安全问题文档因为访问的频度低,这类文大部分不在缓存层。当需要访问它们时,首先需要一次磁盘读,缓存进内存再返回给应用。所以访问速度一定低于内存的直接操作,不会因为是相对非活跃的数据,disk的SLA是可以接受的。

邮件文档

同时,也可以将邮件数据信息放在单独的文档中,是否要做这样的设计完全在于应用是不是有这样频繁的数据查询需求;如果有,那么可以设计单独的邮件文档,同时在服务里增加getEmail()方法,这个方法的数据参数可以是用户名,方法中很容易的可以利用用户名拼接出邮件文档的key值,例如email::hernandez94, 然后使用key值通过couchbase SDK快速查询到对应的邮件文档。

用户的安全角色映射文档

因为是安全认证的服务,每个用户都对应了相关的安全角色。而且当需要查询某个用户所拥有的安全角色的时候,并不需要其他的用户信息,所以我定义了专门存储每个用户对应的安全角色的文档。key值还是类型拼接上用户名,值是一个数组,里面保存了这个用户所拥有的安全角色编码;
key : user-sec-roles::hernandez94

{
“sec-roles” : [101, 301, 345]
}

安全角色文档

这是一类描述安全角色定义的文档。

key : sec-roles::101

{
“name” : “Administrator”,
“descr” : “Administrators of the service”
}

不过这类文档设计,有可能后面给我带来一些问题,因为这类文档可能被访问的很频繁,所以有些时候这类参考数据或者配置数据类型的文档可能是应用读的热点。 我就曾经有见到过一个客户的应用,他们把应用的配置作为一个文档存储在Couchbase里面,结果访问十分频繁,对这个配置文档的访问大约在每秒1万次;虽然Couchbase的一个单节点对于万次的访问支持没有任何问题,但是因为这个访问还和其他的数据操作和流量在一起,这个客户的环境上拥有这个配置文档的节点就超过负荷了。当然我举得是一个很少见的例子,大部分时候,Couchbase很难出现数据热点,因为我们使用的是hash算法来做sharding;
但是这也是一个例子,如果我们真的在环境里看到了某个节点访问明显和其他节点对比不均衡,哪儿是不是数据模型的设计,导致某个/类的少量文档,读取过于频繁。

新的主用户文档

下面是用户主文档。如果我的应用数据访问模式有需求,我也可以将用户的地址信息,或者电话拆分出来作为单独的文档。在我的应用场景中,我没有单独访问这类数据的需求,而且一旦我访问这些数据都是和其他用户信息一起访问,所以我没有把这些信息单独拆分出来。我还能对这部分数据范式化的更深入么?可以的,事实上,我也可以将createDate 字段放到一个单独的键值对里,因为每个用户的创建日期需要被存储下来,但是之后很少被访问到,所以可以单独放到另外的键值对里存储;不过最终我没这样设计时因为createDate本身字节数不多,而且couchbase会为每一个文档保存对应的元数据,每个元数据有56个字节,那么每多一个这样的创建日期文档,就对对应的56字节元数据。考虑到元数据的overhead,对比这样的创建日期本身的大小,我决定将这部分信息留在主用户文档里。

<span style="color: rgb(74, 73, 71); font-family: 'Kievit OT', sans-serif;font-size:14px; line-height: 25.6000003814697px;">key : hernandez94</span>
{
"firstName" : "Jennifer",
"middleName" : "Maria",
"lastName" : "Hernandez",
"addresses" : [
{ "type" : "home", "addr1" : "1929 Crisanto Ave", "address" : "Apt 123", "addr3" : "c/o  J. Hernandez", "city" : "Mountain View", "state" : "CA", "country" : "USA", "pcode" : "94040" },
{ "type" : "work", "addr1" : "2700 W El Camino Real", "addr2" : "Suite #123", "city" : "Mountain View", "state" : "CA", "country" : "USA", "pcode" : "94040" },
{ "type" : "billing", "addr1" : "P.O. Box 123456", "city" : "Mountain View", "state" : "CA", "country" : "USA", "pcode" : "94041" }
],
“phones” : [
{ "type" : "work", "num" : "+12345678900" },
{ "type" : "mobile", "num" : "+12345678901" }
],
"createdate" : “2014101454"
}

示例 Java 代码

这是一个java 代码示例,如果批量取得和某个用户相关的全部文档:

这样,我们可以很快的获取和这个用户相关的全部文档,一定程度上,可以类比关系数据库中,信息分散在不同的表里,依靠Join把信息取出来,但是这种设计方式,使用的是一个批量的key值的同时查询(key值分别为 main::用户名,email::用户名,secQuestion::用户名,等等),速度和效率远远高于数据库Join操作.

Cluster cluster = CouchbaseCluster.create();
Bucket bucket = cluster.openBucket();

List<JsonDocument> foundDocs = Observable
.just(“main::” + userID, “email::” + userID, “secQuestions::” + userID, “authInfo::” + userID, “addr::” + userID, “sec-roles::” + userID)
.flatMap(new Func1<String, Observable<JsonDocument>>() {
@Override
public Observable<JsonDocument> call(String id) {
return bucket.async().get(id);
}
})
.toList()
.toBlocking()
.single();

在下一篇博客中,我会给出更完整的代码示例和解析,上述代码中会加入,如果一次call 没有返回此用户相关的全部文档,那么做一次replica read来或缺确实的文档,来从应用层处理如果有某个节点down的处理逻辑。

上述关于couchbase的批读取操作,bulk get,详细的细节可以查看Couchbase SDK的官方文档。
http://docs.couchbase.com/developer/dev-guide-3.0/intro.html

总结:

 Couchbase 提供给你一种不同的数据建模思路,不同于其他NoSQL vendor,尤其不同于关系数据库;这篇的设计思路,是把信息分到不同的文档里,而不是用一个大文档来存储全部和用户相关的信息,同时用key值把这些不同的文档关联起来,(和同一个用户相关的文档,key值里都包含这个用户的userID),我们也可以只获取感兴趣的信息;同时Couchbase可以保证最常访问的数据一定在管理内存层,所以我们一旦需要某个用户对应的全部文档,就可以很快的通过上述Bulk Get取得。其实最重要的key值模式的设置,在这里我们可以这样设计key值,是因为我们的查询是查询时已经知道我要什么样的数据,例如我要查询ID为hernandez94的用户的登录信息;所以我们这种key
pattern访问模式的设计就非常适合这种查询,提供最快的访问速度和吞吐;但是我们忽略了另外的数据真实的查询去修,就是问题式的查询,比如邮编为98387的用户有多少个?类似这种查询,需要使用数据库的查询功能,我会在这篇系列的另一部分中讲到
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息