从本章开始,我们正式与用户相关服务打交道。当用户想使用一个互联网产品的完整功能时,要做的第一件事情就是在应用中注册账号并登录。这个功能由用户登录服务负责, 也是本章的主题。在正式学习用户登录服务前,我们先来了解本章的学习路径与内容组织结构。
5.1节介绍用户账号对互联网产品的意义。
5.2节介绍用户登录服务的功能要点。
5.3节介绍如何对用户密码进行最大可能的保护。
5.4节介绍手机号登录和邮箱登录的实现方式。
5.5节介绍第三方登录的实现方式。
5.6节介绍登录态问题以及登录态管理的方案。
5.7节介绍扫码登录的实现方式。
本章关键词:单向加密、用户认证、手机号一键登录、第三方登录、Session、长短令牌、扫码登录。
在早期互联网时代,大部分互联网产品是不需要用户注册与登录的。因为早期互联网产品主要是信息发布与浏览的平台,比如门户网站、搜索引擎,当时的用户行为非常简单(几乎就是浏览),不需要为用户提供个性化服务,所以不需要识别用户,更不需要用户注册与登录。
而随着互联网的发展,用户数量逐渐增加,用户的需求与行为也变得多样化和个性化,互联网产品开始逐渐转向注重社交性、交互性、个性 ...
Leaf是美团点评公司基础研发平台推出的一个唯一ID生成器服务,其具备高可靠性、低延迟、全局唯一等特点,目前已经被广泛应用于美团金融、美团外卖、美团酒旅等多个部门。Leaf根据不同业务的需求分别实现了Leaf-segment和Leaf-snowflake两种方案,前者基于数据库的自增主键,后者基于Snowflake算法。接下来介绍这两种方案的技术原理。 需要注意的是,Leaf和前几节介绍的几种技术方案非常相似,只是多了一些思考和优化,这也是我们在本节中重点着墨的部分。
4.5.1 Leaf-segment 方案Leaf-segment方案与4.4.2节介绍的批量缓存架构方案类似,只不过它没有依赖数据库的自增主键,而是在数据库中为每个业务场景都记录目前可用的唯一ID号段。具体的数据表设计如表4-1所示。
不同业务方的唯一ID需求用biz_tag字段区分,每个biz_tag的ID相互隔离。当某业务请求携带biz_tag访问Leaf服务时,数据库会通过执行如下语句生成唯一ID:
1234BEGINUPDATE table SET max_id = max_id + step WHERE b ...
基于数据库的自增主键也可以生成趋势递增的唯一 ID,且由于唯一ID不与时间戳关联,所以不会受到时钟回拨问题的影响。
4.4.1 分库分表架构数据库一般都支持设置自增主键的初始值和自增步长,以MySQL为例,自增主键的自增步长由auto_increment_increment变量表示,其默认值为1。MySQL可以使用SET命令设置这个变量值,比如SET @@auto_increment_increment=3会将自增步长设置为3。
在创建数据表时,MySQL可以为自增主键指定初始值。例如,如下建表语句会为test_primary_key表的自增主键设置初始值为10:
12345create table test_primary_key (id bigint unsigned not null auto_increment,col tinyint not null,primary key (id)) ENGINE=InnoDB AUTO_INCREMENT=10;
接下来向这个表中插入5条数据,然后看看主键的增长情况,如图4-11所示。
可以看到,第一条数据的主键为10,之后每个主键都 ...
时间戳是指计算机维护的从1970年1月1日开始到当前时间经过的秒数,并且随着时间的流逝而逐步递增。几乎所有的编程语言都仅需要一行代码,就可以轻而易举地得到当前时间戳,并支持毫秒精度,甚至是纳秒精度。时间戳自增的属性非常适合生成趋势递增的唯一ID。
4.3.1 正确使用时间戳时下最为普及的计算机普遍采用64位操作系统,对应的时间戳也是用64位表示的。在4.1.2节中已经明确了唯一ID也是64位的,这样不就意味着唯一ID正好可以用时间戳表示吗?这种做法是不可取的,原因很简单,在高并发场景下,同一时间有很多业务请求到达唯一ID生成器,如果用时间戳表示唯一ID,就会生成重复的ID。
对于基于时间戳的唯一ID,应该继续考虑高并发与分布式环境下的其他变量。例如:
服务实例:同一时间业务请求1和业务请求2分别从ID生成器服务实例A和服务实例B获取唯一 ID,为了防止生成重复的ID,在唯一ID上应该对服务实例的差别有所体现。
请求:同一时间业务请求1和业务请求2从LD生成器服务实例A获取唯一ID,在唯一ID上应该区分这两个请求。
话是没错,但是时间戳已经占用了唯一ID的全部空间,还怎么考虑其他变 ...
唯一ID生成器本身也是一个服务,为了生成单调递增的唯一ID,这个服务需要使用某种存储系统记录可分配的唯一ID。Redis和其他数据库都可以达到这个目的。
4.2.1 Redis INCRBY 命令Redis提供的INCRBY命令可以为键(Key)的数字值加上指定的增量(increment)。如果键不存在,则其数字值被初始化为0,然后执行增量操作。使用INCRBY命令限制的值类型为64位有符号整数,此命令的特性与单调递增的唯一ID的诉求非常契合。基于Redis INCRBY命令实现的唯一ID生成器的Go语言代码非常简单:
12345678func GenID() (int64, error) { // 执行 Redis 命令:INCRBY seq_id 1 cmd := rdb.IncrBy(context.TODO(), "seq_id", 1) if cmd.Err() != nil { return 0, cmd.Err() } return cmd.Val(), nil}
每 ...
在复杂的系统中,每个业务实体都需要使用ID做唯一标识,以方便进行数据操作。例如,每个用户都有唯一的用户ID,每条内容都有唯一的内容ID,甚至每条内容下的每条评论都有唯一的评论ID。
4.1.1 全局唯一与UUID在互联网还未普及的年代,由于用户量少、网络交互形式单调,互联网产品后台数据库使用单体架构就可以满足日常服务的需求。当时每个业务实体都对应数据库中的一个数据表,每条数据都简单地使用数据库的自增主键作为唯一ID。
近年来,随着互联网用户的爆发式增长,数据库从单体架构演进到分库分表的分布式架构,同一个业务实体的数据被分散到多个数据库中。由于数据表之间相互独立,在插入数据时会生成相同的自增主键。此时,如果还使用自增主键作为唯一ID,就会导致大量数据的标识相同,造成严重事故。我们应该保证无论一个业务实体的数据被分散到多少个数据库中,每条数据的唯一ID都是全局的,这个全局唯一ID就是分布式唯一ID。
RFC 4122 规范中定义了通用唯一识别码(Universally Unique Identifier, UUID),它是计算机体系中用于识别信息的一个128位标识符。UUID按照标准方法生 ...
在3.4节中,我们曾列举著名景区在节假日期间限制游客数量的例子来表述限流,而景区在节假日期间将不重要的、安全风险较大的或难以管理的游玩项目暂时关闭叫作“降级”,其目的是保障游客的游玩核心体验。与此类似,服务降级的目的是重点保障用户的核心体验和服务的可用性。在异常、高并发的情况下可以忽略非核心场景或换一种简单处理方式,以便释放资源给核心场景,保证核心场景的正常处理与高性能执行。服务降级的实施方案灵活性较大,一般与业务场景息息相关,接下来我们介绍几种思路。
3.6.1 服务依赖度降级一个服务虽然会有多个下游服务,但是每个下游服务的重要程度对它来说都是不一样的。例如,3.1节中提到的用户信息服务、内容列表服务对于个人页服务来说很重要,而地址位置服务和关系服务就不是很重要。
如果B服务是A服务的下游服务,那么B服务对A服务的重要性被称为“依赖度”。依赖度越高,表明下游服务越重要。依赖度可以用如下两种(但不限于)表示形式来反映下游服务对上游服务的重要程度。
二元:强依赖(出现故障时业务不可接受)和弱依赖(出现故障时业务可暂时接受)
三元:一级依赖(故障导致服务完全不可用)、二级依赖(故障基本 ...
无论是时间窗口、漏桶算法、令牌桶算法,还是全局限流方案,限流阈值都是人为设置的,这就意味着这些限流策略的实际效果很被动,依赖限流阈值的设置是否足够合理。
为了设置合理的限流阈值,在一个服务正式上线前,我们一般会事先对它进行一段时间的全链路压测,再根据压测期间服务节点的各项性能指标,选择服务负载接近临界值前的QPS作为限流阈值。
基于服务的压测数据得出的限流阈值看似是合理的,但是服务的性能会随着服务的不断迭代而变化,例如:
某服务的单个实例可承受的最大QPS为100,研发工程师不满意此服务的性能表现,于是对服务进行了系统性重构,性能得到大幅提升,单个实例可承受的最大QPS变为300。
某服务原本是一个轻量级服务,单个实例可承受的最大QPS为500。但是在某次产品需求变更中,为此服务增加了对多个下游服务的调用,于是服务性能下降到单个实例可承受的最大QPS为100。
可以看到,服务的每次迭代都有可能影响服务的性能,性能可能提升,也可能劣化。
如果服务的性能提升了,则原限流阈值会限制服务发挥性能,浪费服务资源;
如果服务的性能劣化了,则原限流阈值无法有效保护服务不被打垮。
理论上,服 ...
3.2节和3.3节介绍的重试、熔断、资源隔离,都是上游服务为了提高自身服务质量和适当保护下游服务而采用的策略。本节将介绍作为下游服务,为了应对多个上游服务的请求访问,以防被上游服务打垮应做好的预防机制,这个预防机制就是老生常谈的“限流”。
在现实生活中有大量应用限流的场景,比如某些著名景区在劳动节、国庆节等节假日期间往往人满为患,不仅容易破坏景区环境,而且容易发生踩踏事故,游客的体验也非常差,于是景区管理部门就会通过一系列手段对景区限流,如限定每日票量、限制游玩项目同时参与的人数等。在互联网场景中,这样的例子也随处可见,比如“双十一”电商秒杀抢购、火车票抢票等场景,都通过限流策略来防止服务被海量请求打垮。限流的表现形式主要包括如下几种。
频控:控制用户在N秒内只可执行M次操作,比如限制用户在30s内只能下载1次文件、在1h内最多只能发布5条动态。
单机限流+固定阈值:某服务的每台服务器在1s内最多可处理M个请求,M值是预先设置好的。
全局限流+固定阈值:某服务在1s内总共可处理M个请求,与前者的主要区别在于限流范围是某服务的全部服务实例。
单机自适应限流:某服务的每台服务器根据自身 ...
熔断和隔离都是上游服务可以采取的流量控制策略。
熔断可以有效防止我们的服务被下游服务拖垮,同时可以在一定程度上保护下游服务。
隔离可以防止一个服务内各个接口之间因质量问题而相互影响。
3.3.1 服务雪崩由于网络原因或服务自身设计问题,每个微服务一般都难以保证100%对外可用。如果某服务出现了质量问题,那么与其相关的上游服务网络调用就容易出现线程阻塞的情况;如果有大量的线程发生阻塞,则会导致上游服务承受较大的负载压力而发生宕机故障。在微服务架构中,由于在服务间建立了依赖关系,所以一个服务的故障会不断向上传播,最终导致整个服务链路发生宕机故障。这就是服务雪崩现象。
假设有3个服务形成如图3-7所示的依赖关系,最上游的Server-1服务直接负责与用户请求交互。
如图3-8所示,某一天,Server-3服务因请求量暴增或设计不合理而宕机。由于Server-2是其上游服务,所以Server-2服务还会有源源不断的请求继续调用Server-3服务。
如图3-9所示,随着Server-2服务内大量的请求调用线程被阻塞在对Server-3服务的调用上,Server-2服务最终也由于大量的线 ...