分布式系统常见问题

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 分布式系统常见问题

一.概述



分布式系统存在网络,时钟,以及许多不可预测的故障。分布式事务,一致性与共识问题,迄今为止仍没有得到很好的解决方案。要想完美地解决分布式系统中的问题不太可能,但是实践中应对特定问题仍有许多可靠的解决方案。本文不会谈及诸如BASE, CAP, ACID 等空泛的理论,只基于实践中遇到的问题提出可行的解决方案。


二.常见问题



1.读自己的写

现象: 用户在发布页发布了帖子,然后访问自己的主页查看帖子列表,并没有马上看到自己刚刚发布的帖子,等待1~2s后才看到


分析:后端db采取主从结构,复制任务在负载较高的情况下会有延迟。用户读取帖子列表查询的是从节点,所以无法及时看到刚刚发布的帖子。一般情况下延迟1~2s是可以接受的,但是为了更好的体验,可以做一些改进。

1c1a3bd34313286bcf7240bd35d54e75.png


解决方案:

  • 如果用户读取的是自己的主页,就访问主节点。如果访问是他人的主页,就访问从节点。只需要在db层路由即可。
  • 客户端还可以记住最近更新时的时间戳,并附带在读请求中,据此信息,系统可以确保对该用户提供读服务时都应该至少包含了该时间戳的更新。如果不够新,要么交由另一个副本来处理,要么等待直到副本接收到了最近的更新


2.单调读


现象:用户查看某个帖子下面的评论,一会儿看到5条评论,一会儿看到6条评论。


分析:后端db采取主从结构,复制任务在负载较高的情况下会有延迟。用户读取评论列表查询的是从节点,但是两次读的是不同的从节点,当某个从节点具有明显延迟就会出现数据反复的现象。

af3025a2a6d63286d7d3c052b8e8b032.png


解决方案:

  • 确保同一个用户每次都是读取同一个副本,可以在db层进行路由。这是一种典型的sticky 请求路由。

    replica = hash(user_id) % number_of_replica


3.负载倾斜与热点问题


现象:某个分区的数据明显比其他分区多,并且访问频率高,负载压力大。


分析:在某些特殊的业务场景下,比如官方或者名人账号有百万粉丝,当这些账号发布消息事件时,人们会对该消息进行评论,如果评论数据存储使用事件id进行hash,就会造成某个分区的负载产生倾斜。


解决:

  •   在关键词,比如消息事件id,的开头或者结尾添加一个随机数。只需一个两位数的十进制随机数就可以将关键字的写做操作分布到100个不同的关键字上,从而分片到不同的分区上。这些特殊逻辑只应用在一些特殊账号上。


4.fencing令牌


现象:在采用分布式锁的情况下,数据库中的事务重复执行。

分析:在分布式锁环境中,客户端A执行事务超时,分布式锁被释放。客户端B执行事务插入数据。客户端A恢复后继续执行事务,重复插入数据。

ef355661aeb0bae5915e2c273e3d6766.png

解决方案:

  • 这不是分布式事务的范畴。可以采用fencing令牌来解决。我们假设每次锁服务授予锁或租约时,同时还会返回一个fencing令牌,该令牌每授予一次就会递增。然后,要求客户端每次向存储系统发生写请求时,都必须包含所持有的fencing令牌。当使用zookeeper 作为锁服务时,可以用事务标识zxid,或节点版本cversion来充当fencing令牌,这两个都可以满足单调递增的要求。

e040eefc16d961d1165b2509a62fcfe6.png


5.Lamport时间戳


现象:客户端从两个分区获取两条不同的数据,比如事件a, b;a的序号小于b,但事实上b比a先发生。


分析:常见的有以下几种非因果序列发生器,产生的序列号与因果关系并不严格一致。


  • 每个节点单独产生自己的一组序列号。
  • 把墙上时间戳信息(物理时钟)附加在每个操作上。
  • 预先分配好序列号的区间范围,比如节点A负责区间1~1000的序列号,节点B负责1001~2000。


解决方案:

  • 使用Lamport时间戳。Lamport时间戳是一个kv对(计数器,节点ID)。核心流程:每个节点以及每个客户端都跟踪迄今为止所见到的最大计数器,并在每个请求中附带该最大计数器值。当节点收到请求(或者回复)时,如果发现请求内嵌的最大计数器大于节点自身的计数器,则它立即把自己的计数器修改为该最大值。

ea824e13e5af8a5c778298be08d43535.png


6.端到端的重复消除问题


现象:消息重复是非常普遍的,比如

  • 生产者发送消息到消费者,消费者消费成功后宕机,但是却没有更新消费位置,消费者重启后就会重新消费。
  • 常见的rpc调用,调用方因为网络问题没有收到被调用方的响应,选择重试。
  • 2PC 分布式事务中,因为网络问题,也可能出现重复事务的问题。
  • 用户在页面重复提交POST请求。


分析:端到端的重复问题是非常普遍的,在TCP 网络中也需要处理重复数据包的问题。有以下两种解决办法:

  • 最有效的办法之一是使操作满足幂等性,即无论执行一次还是多次,确保具有相同的结果。比如以下语句无论执行多少次效果都是一致的。

update table set v = v2 where v = v1

  • 可以为操作生成一个唯一的标识符如(UUID),服务端对此UUID 进行去重校验。

33632f1f7a2c00a419dbeb2fb395722b.png

  • 在典型的电商下单接口中采用了以上两种方法的结合:使用唯一标识符来进行去重,如果写入异常返回之前的订单。
create table order(
  # ...
  dedup_key varchar(60) not null comment 'key to pretend order duplication',
  client_id,
  # ...
  unique uniq_dedup_key(dedup_key, client_id)
);
@Transactional
Order createOrder(Integer userId, String prodCode, Decimal amount, String dedupKey) {
  try {
    String orderId = createOrder(userId, prodCode, amount, deupKey); // insert a new order
    Order order = getOrderById(orderId); // read order from db
    order.setDuplicated(false); // 标记是否有重复下单
    return order;
  } catch(UniqueKeyViolationException e) {
    // if duplicated order has existed, return previous order
    Order order = getOrderByDedupKey(dedupKey, clientId);
    order.setDuplicated(true);
    return order;
  } catch (Exception e) {
    // hanlde other errors and rollback transaction ...
  }
}


7.唯一性约束


现象:在集群高并发的环境下,用户A创建用户marquezzzz,用户B同时创建了用户marquezzzz,两者的用户名相同,这违背了唯一性约束。


分析:创建用户名的逻辑是,先去db中查询是否有对应的用户名(步骤1),如果没有就创建,如果存在就更新用户的其他信息(步骤2)。用户A执行了步骤1, 用户B执行了步骤1和2,然后用户A执行了步骤2,这样生成了两个同名的用户。


解决方案:

  • 串行化请求,将创建用户的请求串行化,比如发送到队列中,这样可以确保全局唯一性。
  • 在db层进行唯一性约束,比如使用唯一索引,考虑到庞大的数据量,性能会下降。如果做了分表,唯一索引的方法也不太可行。
  • 使用分布式锁,比如redis, zookeeper,redis伪代码如下:
boolean r = redisClient.setnx("userName", currentThread, 10s); // 使用 setnx 原子命令
if (!r) {
    return false;
}
// 步骤1 查找db确保没有重名
// 步骤2 插入用户
redisClient.delete("userName");


8.时钟问题


现象:在许多app中,客户端会上报事件,但是事件的发生时间不准确

分析:app客户端时钟可能不准确,或者用户手动调整过系统时钟。


解决方案:

为了调整不正确的设备时钟,一种方法是记录三个时间戳:


根据设备的时钟,记录事件发生的时间, device_event_time

根据设备的时钟,记录将事件发生到服务器的时间, device_send_time

根据服务器时钟,记录服务器收到事件的时间, server_receive_time


事件真实发生时间 = device_event_time + (server_receive_time - device_send_time)


三.参考



《数据密集型应用系统设计》


https://cloud.tencent.com/developer/article/1121727


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
7月前
|
SQL 关系型数据库 MySQL
Flink CDC产品常见问题之读分布式mysql报连接超时如何解决
Flink CDC(Change Data Capture)是一个基于Apache Flink的实时数据变更捕获库,用于实现数据库的实时同步和变更流的处理;在本汇总中,我们组织了关于Flink CDC产品在实践中用户经常提出的问题及其解答,目的是辅助用户更好地理解和应用这一技术,优化实时数据处理流程。
|
机器学习/深度学习 存储 编解码
强化学习从基础到进阶-常见问题和面试必知必答[4]::深度Q网络-DQN、double DQN、经验回放、rainbow、分布式DQN
强化学习从基础到进阶-常见问题和面试必知必答[4]::深度Q网络-DQN、double DQN、经验回放、rainbow、分布式DQN
PolarDB-X 1.0-常见问题-分库分表问题-PolarDB-X是否支持分布式JOIN?
PolarDB-X支持大部分的JOIN语法,但对于比较复杂的情况,PolarDB-X做了一些限制。例如大表之间的JOIN,由于执行代价过高,速度过慢容易导致性能或者系统不可用等情况,因此请尽量避免,详情请参见JOIN与子查询的优化和执行。
735 0
|
2月前
|
NoSQL Java Redis
太惨痛: Redis 分布式锁 5个大坑,又大又深, 如何才能 避开 ?
Redis分布式锁在高并发场景下是重要的技术手段,但其实现过程中常遇到五大深坑:**原子性问题**、**连接耗尽问题**、**锁过期问题**、**锁失效问题**以及**锁分段问题**。这些问题不仅影响系统的稳定性和性能,还可能导致数据不一致。尼恩在实际项目中总结了这些坑,并提供了详细的解决方案,包括使用Lua脚本保证原子性、设置合理的锁过期时间和使用看门狗机制、以及通过锁分段提升性能。这些经验和技巧对面试和实际开发都有很大帮助,值得深入学习和实践。
太惨痛: Redis 分布式锁 5个大坑,又大又深, 如何才能 避开 ?
|
4月前
|
NoSQL Redis
基于Redis的高可用分布式锁——RedLock
这篇文章介绍了基于Redis的高可用分布式锁RedLock的概念、工作流程、获取和释放锁的方法,以及RedLock相比单机锁在高可用性上的优势,同时指出了其在某些特殊场景下的不足,并提到了ZooKeeper作为另一种实现分布式锁的方案。
131 2
基于Redis的高可用分布式锁——RedLock
|
4月前
|
缓存 NoSQL Java
SpringBoot整合Redis、以及缓存穿透、缓存雪崩、缓存击穿的理解分布式情况下如何添加分布式锁 【续篇】
这篇文章是关于如何在SpringBoot应用中整合Redis并处理分布式场景下的缓存问题,包括缓存穿透、缓存雪崩和缓存击穿。文章详细讨论了在分布式情况下如何添加分布式锁来解决缓存击穿问题,提供了加锁和解锁的实现过程,并展示了使用JMeter进行压力测试来验证锁机制有效性的方法。
SpringBoot整合Redis、以及缓存穿透、缓存雪崩、缓存击穿的理解分布式情况下如何添加分布式锁 【续篇】
|
22天前
|
存储 NoSQL Java
使用lock4j-redis-template-spring-boot-starter实现redis分布式锁
通过使用 `lock4j-redis-template-spring-boot-starter`,我们可以轻松实现 Redis 分布式锁,从而解决分布式系统中多个实例并发访问共享资源的问题。合理配置和使用分布式锁,可以有效提高系统的稳定性和数据的一致性。希望本文对你在实际项目中使用 Redis 分布式锁有所帮助。
65 5
|
26天前
|
NoSQL Java 数据处理
基于Redis海量数据场景分布式ID架构实践
【11月更文挑战第30天】在现代分布式系统中,生成全局唯一的ID是一个常见且重要的需求。在微服务架构中,各个服务可能需要生成唯一标识符,如用户ID、订单ID等。传统的自增ID已经无法满足在集群环境下保持唯一性的要求,而分布式ID解决方案能够确保即使在多个实例间也能生成全局唯一的标识符。本文将深入探讨如何利用Redis实现分布式ID生成,并通过Java语言展示多个示例,同时分析每个实践方案的优缺点。
55 8
|
1月前
|
NoSQL Redis
Redis分布式锁如何实现 ?
Redis分布式锁通过SETNX指令实现,确保仅在键不存在时设置值。此机制用于控制多个线程对共享资源的访问,避免并发冲突。然而,实际应用中需解决死锁、锁超时、归一化、可重入及阻塞等问题,以确保系统的稳定性和可靠性。解决方案包括设置锁超时、引入Watch Dog机制、使用ThreadLocal绑定加解锁操作、实现计数器支持可重入锁以及采用自旋锁思想处理阻塞请求。
59 16
|
1月前
|
缓存 NoSQL PHP
Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出
本文深入探讨了Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出。文章还介绍了Redis在页面缓存、数据缓存和会话缓存等应用场景中的使用,并强调了缓存数据一致性、过期时间设置、容量控制和安全问题的重要性。
43 5

热门文章

最新文章