# op-user-center
**Repository Path**: liyuncc/op-user-center
## Basic Information
- **Project Name**: op-user-center
- **Description**: 亿级用户中心的设计与实践
- **Primary Language**: Java
- **License**: Apache-2.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 2
- **Created**: 2023-11-17
- **Last Updated**: 2024-08-11
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# op-user-center
## 介绍
**亿级用户中心的设计与实践**
参考连接:
> 作者:vivo互联网技术
> 链接:[亿级用户中心的设计与实践](https://juejin.cn/post/6937165940933165070)
> 来源:稀土掘金
>
> 作者:架构师之路
> 链接:[用户中心,1亿数据,架构如何设计?](https://cloud.tencent.com/developer/article/1663332)
> 来源:腾讯云开发者社区
用户中心,顾名思义就是管理用户的地方。其核心功能包括注册与登录,主要功能是修改密码、换绑手机号码、获取用户信息、修改用户信息和一些延伸服务,
同时还有登录之后生成Token以及校验Token的功能。
### 1、服务架构
基于业务特性,可以将用户中心拆分为3个独立的微服务:网关服务,核心服务,异步消费者服务。
#### 1.1、网关服务
提供http服务,聚合了各种业务逻辑和服务调用,比如登录时候需要校验的风控或者短信;
#### 1.2、核心服务
处理简单的业务逻辑以及数据存储,核心服务处在调用链路的终端,几乎不依赖调用其他服务,比如校验Token或者获取用户信息,他们就只依赖于redis或者数据库;
#### 1.3、异步消费者服务
处理并消费异步消息;
#### 1.4、架构示意图

这样的设计之后,当有新功能上线时,核心服务和异步消费服务几乎不需要重新发布,只需要发布网关服务,依赖我们核心服务的第三方非常放心,层级也非常地清晰。
当然,这样做的代价就是服务的调用链路变长了。由于涉及到网关和核心服务,就需要发布两个服务,而且要做兼容性测试。
### 2、接口设计
用户中心的接口涉及到用户的核心信息,安全性要求高;同时承接了较多的第三方调用,可用性也要求高。因此,对用户中心的接口做以下设计:
- 接口可以拆分为面向Web和面向App的接口。Web接口需要做到跨域情况下的单点登录,加密、验签和token校验的方式也同App端的不一样。
- 对核心接口做处理。(在思考思考,还是有点不是很理解的透彻。)
- 用户核心信息表做简单。可拆分为用户账号表(userId、username、手机号码、密码、盐值等)、用户资料表(性别、头像、昵称、邮箱、地址等);
- 将登录的核心链路做短,短到只依赖于读库。一般情况下,用户登录后,需要记录用户登录信息,调用风控或者短信等服务。对于登录链路来说,任何一个环节出现问
题都有可能导致用户无法登录,那么怎么样才能做到最短的链路呢?方法就是依赖的服务可自动降级。比如说反欺诈校验出问题了,那么它自动降级后使用它的默认策略,
极端情况下只做密码校验,主库挂了之后还能到从库读取用户信息。
- 接口的安全性校验。对App接口我们需要做防重放和验签。得益于大数据的支持,结合终端,我们还可以把每个用户行为画像存储在系统中(或者调用第三方服务)。
用户发起请求后,我们的接口会根据用户画像对用户进行诸如手机号码校验、实名认证、人脸或者活体校验。
### 3、数据存储
#### 3.1、分库分表
随着用户的增长,数据量过大,常见的方式就是分库分表。用户中心常见的一些表结构:用户信息表、第三方登录关联表、用户事件表等。从上述表中可以看出,用户相关的
数据表增长是相对缓慢的,因为用户的增长是有天花板的。用户事件表的增长是呈指数级增长,因为每个用户登录、变更等密码及变更手机号码等操作是不限次数。
##### 3.1.1、用户信息表垂直切分
正如上面说的,将用户ID、密码、手机号、盐值等常见字段从用户信息表中拆分,其他用户相关的信息用单独一张表。另外,把用户事件表迁移至其他库中。
相比于水平切分,垂直切分的代价相对较少,操作起来相对简单。用户核心信息表由于数据量相对较少,即使是亿级别的数据,利用数据库缓存的机制,也能够解决性能问题。
##### 3.1.2、业务的特性区分处理
可以利用前后台业务的特性采用不同的方式来区别对待;对于不同的业务特性使用不同的解决方案;有两类典型的业务需求:第一类是用户侧的前台访问;第二类是运营侧的后台访问。
**用户侧前台访问**
最典型的有两类需求:用户通过username/mobile登录或者通过uid来查询用户信息。
1. 用户登录查询:通过username/mobile查询用户账号信息,1%请求属于这种类型;
2. 用户信息查询:登录之后,通过UID来查询用户基本信息,99%属于这种请求;
用户侧信息的访问通常是单条数据的查询,访问量较大,服务需要高可用,并且对一致性的要求较高,我们可以通过索引多次查询来解决一致性和高可用问题。
**运营侧后台访问**
根据年龄、性别、登录时间段、注册时间段等来进行查询,基本上都是批量分页查询。但是由于是内部系统,查询量低,对一致性要求低。如果用户侧和运营侧的查询采用
同一个数据库,那么运营侧的排序查询会导致整个库的CPU上升,查询效率下降,影响到用户侧。因此,运营侧使用的数据库可以是和用户侧同样的MySQL离线库,
如果想要增加运营侧的查询效率,可以采用ES非关系型数据库。ES支持分片与复制,方便水平分割和扩展,复制保证了ES的高可用与高吞吐,同时能够满足运营侧的查询需求。
##### 3.1.3、水平切分来保证系统性能
常见的方法有四种:
1. 索引表法
2. 缓存映射法
3. 生成UID法
4. 基因法
**索引表法**
索引表法的思路主要是UID能够直接定位到库,但是手机号码或者username是无法直接定位到库的,需要建立一个索引表来记录mobile与UID或者username与UID的
映射关系的方式来解决这个问题。通常这类数据比较少,可以不用分库分表,但是相比直接查询,多了一次数据库查询的同时,在新增数据的时候还多了一次映射关系的插入,事务变大。
具体解决方案如下:
1. 建立一个索引表记录username/mobile与UID的映射关系;
2. 用username/mobile来访问时,先通过username/mobile来访问时,先通过索引表查询到UID,再根据UID定位到相应的库;
3. 索引表属性较少,可以容纳非常多的数据,一般不需要分库分表;
4. 如果数据量较大,可以通过username/mobile来分库;
缺点:数据访问,会增加一次数据库查询,事务变大,性能会有所下降。
**缓存映射法**
缓存映射法的思路和索引表法类似,唯一不同的就是,访问索引表性能较低,把映射关系放在缓存里,能够提升性能。
具体解决方案如下:
1. 用username/mobile来访问时,先到cache中查询UID,再根据UID定位数据库;
2. 假设cache Miss,扫描所有分库,获取username/mobile对应的UID,放入cache;
3. username/mobile到UID的映射关系不会变化,映射关系一旦放入缓存,不会更改,无需淘汰,缓存命中率超高;
4. 如果数据量过大,可以通过login_name进行cache水平切分;
缺点:仍然会增加一次网络交互,即一次cache查询。
**生成IUD法**
生成IUD法的思路是不进行远程查询,由username/mobile直接得到UID;
具体解决方案如下:
1. 在用户注册时,设计函数username/mobile生成UID,UID=f(username/mobile),按UID分库插入数据;
2. 用username/mobile来访问时,先通过函数计算出UID,再根据UID定位数据库;
缺点:该函数设计需要非常讲究技巧,且有uid生成冲突风险;uid冲突,是业务无法接受的,故生产环境中,一般不使用这个方法。只适用于某些特定场景。
**基因法**
基因法的思路是我们将username或者mobile融入到UID中。
具体解决方案如下:
1. 用户注册时,根据用户的手机号码,利用函数生成N bit的基因uid_gen,使得uid_gen=f(username/mobile);
2. 生成M bit全局唯一的id,作为用户标识;
3. 拼接M和N,作为UID赋给用户;
4. 根据N bit来取余来插入到特定数据库;
5. 查找用户数据的时候,将用户UID的后N bit取余来落到最终的库中。

*注意:基因法只适用于某类经常查询的场景。*
##### 3.1.4、前台与后台分离
前台业务和后台业务共用一批服务和一个数据库,有可能导致,由于后台的“少数几个请求”的“批量查询”的“低效”访问,导致数据库的cpu偶尔瞬时100%,
影响前台正常用户的访问(例如,登录超时)。而且,为了满足后台业务各类“奇形怪状”的需求,往往会在数据库上建立各种索引,这些索引占用大量内存,
会使得用户侧前台业务uid/username/mobile上的查询性能与写入性能大幅度降低,处理时间增长。对于这一类业务,应该采用“前台与后台分离”的架构方案。
用户侧前台业务需求架构依然不变,产品运营侧后台业务需求则抽取独立的 web / service / db 来支持,解除系统之间的耦合,
对于“业务复杂”、“并发量低”、“无需高可用”、“能接受一定延时”的后台业务可以稍作调整:
1. 可以去掉service层,在运营后台web层通过dao直接访问db;
2. 不需要反向代理,不需要集群冗余;
3. 不需要访问实时库,可以通过MQ或者线下异步同步数据;
4. 在数据库非常大的情况下,可以使用更契合大量数据允许接受更高延时的“索引外置”(例如ES搜索系统)或者“大数据处理”(例如HIVE)的设计方案;

#### 3.2、缓存
将热门或频繁访问的用户数据缓存起来,减轻数据库的压力,并提高响应速度。
#### 3.3、数据一致性
采用分布式事务或最终一致性方案,保证不同存储节点之间的数据一致性。
### 4、Token
用户登录之后,另一个重要的事情就是Token的生成与校验。用户的Token分为两类, 一类是web端登陆生成的Token,这个Token可以和Cookie结合,达到单点登陆的效果;
另外一类就是APP端登录生成的Token。
**Token生成**
用户在我们的APP输入用户名密码之后,服务端会对用户的用户名密码进行校验,成功之后从系统配置中心获取加密算法的版本以及秘钥,并按照一定的格式排列用户ID,
手机号、随机码以及过期时间,经过一系列的加密之后,生成了Token之后并将其存入Redis缓存。而Token的校验就是把用户ID和Token组合并校验是否在Redis中存在。
那么假如Redis不可用了怎么办呢?这里有一个高可用和自动降级的设计。当Redis不可用的时候, 服务端会生成一个特殊格式的Token。当校验Token的时候,
会对Token的格式进行一个判断。
**Token降级**
假如判断为Redis不可用时生成的Token,那么服务端会对Token进行解密,而Token的生成是由用户ID,手机号、随机码和过期时间等数据按照特定顺序排列并加密而来的,
那么解密出来的数据中也包含了ID,手机号码,随机码和过期时间。服务端会根据获取到的数据查询数据库,比对之后告诉用户是否登录成功。
由于内存缓存redis和数据库缓存性能的差距,在redis不可用的情况下,降级有可能会导致数据库无法及时响应,因此需要在降级的方法上加入限流。
### 5、数据安全
数据安全对用户中心来说非常重要。敏感数据需要脱敏处理,对密码更是要做多重的加密处理。应用虽然有自己的安全策略,但如果把黑客限制在登录之前,那应用的安全性将得到大幅度的提升。
而即使使用了MD5和salt的加密方式,依然可以使用彩虹表的方式来破解。那么用户中心对用户信息是怎么保存的呢?
**信息分离**
将不同的数据分离出来,比如用户账号、用户密码、手机号码等登录信息和其他信息的分离,并且保存在不同的数据库中。
**黑名单校验**
对用户设置的密码进行了黑名单校验,只要符合条件的弱密码,都会拒绝提交,因为不管使用了什么加密方式的弱密码,都极其容易破解。因为人的记性很差,大部分人总是最倾向于选择生日,
单词等来当密码。6位纯数字可以生成100万个不同的密码,8位小写字母和数字的组合大概可以生成2.8万亿个不同的密码。一个规模为7.8万亿的密码库足以覆盖大部分用户的密码,
对于不同的加密算法都可以拥有这样一个密码库,这也就是为什么大部分网站都建议用户使用8位以上数字加字母密码的原因。当然,如果一方面加了盐值,
另一方面对密钥分开保管,破解难度会指数级增加。
**加密方式**
可以用bcrypt/scrypt的方式来加密。bcrypt算法是基于Blowfish块密钥算法来实现的,bcrypt内部实现了随机加盐处理,使用bcrypt之后每次加密后的密文都不一样,
同时还会使用内存初始化hash过程。由于使用内存,虽然在CPU上运行很快,但是在GPU上并行运算并不快。随着新的FPGA集成了大型RAM,解决了内存密集IO的问题,
但是破解难度依然不小。而scrypt算法弥补了bcrypt算法的不足,它将CPU计算与内存使用开销都指数级提升了。bcrypt和scrypt算法能够有效抵御彩虹表,
但是安全性的提升带来了用户登录性能的下降。用户登录注册并不是一个高并发的接口,所以影响并不会特别大。因此在安全和性能方面需要依据业务类型和大小来做平衡,
并不是所有的应用都需要使用这种加密方式来保护用户密码。
### 6、异步消费设计
用户在做完登录注册等操作后,需要记录用户的操作日志。同时,用户注册登录完毕后,下游业务需要对用户增加积分,赠送礼券等奖励操作。这些系统如果都同步依赖于用户中心,
那么整个用户中心将异常庞大,链路非常冗长,也不符合业内的“大系统做小“的原则。依赖的服务不可用之后将会造成用户无法登录注册。因此,用户中心在用户操作完之后,
将用户事件入库后发送至MQ,第三方业务监听用户事件。用户中心和下游业务解耦,同时用户操作事件入库后,在MQ不可用或者消息丢失的时候可做补偿处理。
用户的画像数据也在很大程度上来源于此处的数据。
### 7、监控
用户中心涉及到用户的登录注册更改密码等核心功能,能否及时发现系统的问题成为关键指标,因此对业务的监控显得尤为重要。需要对用户中心重要接口的QPS、机器的内存使用量、
垃圾回收的时间、服务的调用时间等做详细的监控。当某个接口的调用量下降的时候,监控会及时发出报警。除了这些监控之外,还有对数据库Binlog的写入,前端组件,
以及基于ZipKin全链路调用时间的监控,实现从用户发起端到结束端的全面监控,哪怕出现一点问题,监控随时会告诉你哪里出问题了。比如运营互动推广注册量下降的时候,
用户中心就会发出报警,可以及时通知业务方改正问题,挽回损失。
### 8、其他(待补充。。。)
用户中心的设计远不止这些,还会包含用户数据的分库分表,熔断限流,第三方登录等。
存在一些比较大的挑战:在鉴权服务增长的情况下,如何平滑的从用户中心剥离;监控的侵入性以及监控的粒度的完善;另外服务的安全性、可用性、性能的提升等。
## 软件架构
软件架构说明
**使用版本**
| 框架 | 版本 |
|------------------------------|----------------|
| spring.cloud.alibaba.version | 2022.0.0.0-RC2 |
| spring.cloud.version | 2022.0.0 |
| spring.boot.version | 3.0.0 |
| java.version | 17 |
**中间件**
| 名称 | 部署版本 | 依赖版本 | 官方文档 |
|--------------|--------|-------|--------------------------------------------------------------|
| Nacos | 2.3.0 | ~ | [Nacos 文档](https://nacos.io/zh-cn/docs/quick-start.html) |
| Apache Dubbo | ~ | 3.2.2 | [Dubbo 文档](https://cn.dubbo.apache.org/zh-cn/overview/home/) |
| Redis | 6.2.14 | ~ | [Redis 文档](https://redis.com.cn/) |
| Sentinel Version | Nacos Version | RocketMQ Version | Apache Dubbo Version | Seata Version |
|------------------|---------------|------------------|----------------------|------------------|
| 1.8.6 | 2022.0.0.0 | 4.9.4 | 3.2.2 | 1.7.0-native-rc2 |
注意:Spring Cloud Dubbo 从2021.0.1.0 起已被移除主干,不再随主干演进;因此我们使用Apache Dubbo。
**使用到的组件**
> - 服务注册与发现:Nacos
> - 分布式事务:Seata
> - 网关:Spring Cloud Gateway
> - 服务调用:OpenFeign (或者Apache Dubbo)
> - 鉴权:Spring Authorization Server 、Oauth2.1
> - 消息队列:rocketmq
> - 限流、熔断:sentinel
> - 链路追踪:Micrometer Tracing
> - 接口文档:knife4j
## 项目集成
### 父级依赖集成
**父级工程POM**
```xml
17
17
UTF-8
3.0.0
2022.0.0
2022.0.0.0-RC2
3.2.2
1.8.6
4.9.4
1.7.0-native-rc2
org.springframework.boot
spring-boot-dependencies
${spring-boot.version}
pom
import
org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import
com.alibaba.cloud
spring-cloud-alibaba-dependencies
${spring-cloud-alibaba.version}
pom
import
```
### Nacos集成
**模块POM:pom.xml**
```xml
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-config
org.springframework.cloud
spring-cloud-starter-bootstrap
org.springframework.cloud
spring-cloud-starter-loadbalancer
```
**模块配置文件:bootstrap.yaml**
```yaml
spring:
application:
name: your-application
cloud:
nacos:
discovery:
server-addr: hostname:port
namespace: your-namespace
username: your-nacos-username
password: your-nacos-password
config:
server-addr: hostname:port
namespace: your-namespace
username: your-nacos-username
password: your-nacos-password
file-extension: yml
```
### SpringCloud Gateway集成
**模块POM:pom.xml**
```xml
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-config
org.springframework.cloud
spring-cloud-starter-bootstrap
org.springframework.cloud
spring-cloud-starter-gateway
```
**模块配置文件:bootstrap.yaml**
```yaml
spring:
application:
name: your-application-gateway
cloud:
nacos:
discovery:
server-addr: hostname:port
namespace: your-namespace
username: your-nacos-username
password: your-nacos-password
config:
server-addr: hostname:port
namespace: your-namespace
username: your-nacos-username
password: your-nacos-password
file-extension: yml
gateway:
routes:
- id: your-application
uri: hostname:port
predicates:
- Method=GET,POST
- Path=/your-service/**
filters: # 让gateWay自动去掉一节路径
- StripPrefix=1
```
**注意:**
1. 在Spring Cloud 2020版本以后,默认移除了对Netflix的依赖,其中就包括Ribbon,官方默认推荐使用Spring Cloud Loadbalancer正式
替换Ribbon,并成为了Spring Cloud负载均衡器的唯一实现。
```xml
org.springframework.cloud
spring-cloud-loadbalancer
```
2. 由于服务路径的问题,在gateway配置的content-path路径不是路由服务的content-path,可能回报404,需要检查配置路径;可以使用filters来
截取掉不匹配的路径。
```yaml
spring:
cloud:
gateway:
routes:
filters:
- StripPrefix=1 # 需要截取几层,值就等于几
```
### Apache Dubbo集成
阿里早已把dubbo捐赠给了Apache,现在dubbo由Apache在维护更新,dubbo也已经成了Apache下的顶级项目。
#### 服务提供者
**模块POM:pom.xml**
```xml
org.springframework.cloud
spring-cloud-starter-bootstrap
org.apache.dubbo
dubbo-spring-boot-starter
${apache-dubbo.version}
org.apache.dubbo
dubbo-registry-nacos
${apache-dubbo.version}
```
**模块配置文件:bootstrap.yaml**
```yaml
dubbo:
application:
name: your-application-name
registry:
address: nacos://host:port
parameters:
namespace: your-namespace
username: your-nacos-username
password: your-nacos-password
protocol:
name: dubbo
port: 20880 # your-dubbo-port
```
**主程序启动类:Application添加 @EnableDubbo 注解**
```java
@EnableDubbo(scanBasePackages = "your-interface-impl-package-path") // 开启dubbo
@SpringBootApplication
public class ServiceProviderApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceProviderApplication.class, args);
}
}
```
**Dubbo通用接口实现类:添加 @DubboService 注解**
```java
@DubboService
public class DubboXXXApiImpl implements DubboXXXApi {
// to impl interface
}
```
#### 服务调用者
**模块POM:pom.xml**
```xml
org.apache.dubbo
dubbo-spring-boot-starter
${apache-dubbo.version}
```
**模块配置文件:bootstrap.yaml**
```yaml
dubbo:
application:
name: your-application-name
registry:
address: nacos://host:port
parameters:
namespace: your-namespace
username: your-nacos-username
password: your-nacos-password
protocol:
name: dubbo
port: 20880 # your-dubbo-port
```
**Dubbo通用接口调用:**
```java
@Service
public class XXXServiceImpl {
@DubboReference
private DubboXXXApi dubboXXXApi;
}
```
### Redis集成
**模块POM:pom.xml**
```xml
org.springframework.boot
spring-boot-starter-data-redis
```
**模块配置文件:bootstrap.yaml**
```yaml
spring:
data:
redis:
host: your-redis-host
port: your-redis-port
timeout: 3000ms
lettuce:
pool:
max-active: 20 # 最大连接数,负值表示没有限制,默认8
max-wait: -1 # 最大阻塞等待时间,负值表示没限制,默认-1
max-idle: 8 # 最大空闲连接,默认8
min-idle: 0 # 最小空闲连接,默认0
```