📜  设计类似 Foursquare 的城市指南系统

📅  最后修改于: 2021-09-10 02:49:39             🧑  作者: Mango

城市向导服务的目的

城市指南将是允许用户搜索和查找用户位置附近的地方的服务。您可以认为该服务类似于 Foursquare。在开始设计系统之前,最好先定义系统的目的。这意味着,在设计流程之前,需求应该是明确和明确定义的。城市指南服务的目的是根据用户的位置向用户展示位置。您可以找到任何地方,例如餐厅、剧院、电影院等。建议将从附近位置开始到远处位置,但系统也将支持基于专用位置搜索位置。

要求和系统边界

如果要设计系统,首先必须定义需求和系统边界。在开始设计之前,您将拥有服务设计文档,您将从头开始定义所有需求。由于城市指南服务的目的是创建结构良好的城市指南服务,因此它必须能够根据您的搜索查询和位置推荐地点。所以我们的系统将支持这些功能;

– 用户必须能够创建帐户。
– 用户必须能够登录系统。
– 用户必须能够从系统注销。
– 用户必须能够在登录或注销时搜索餐厅、剧院、咖啡馆、电影院等地点。
– 用户必须能够在登录后向地点添加评论。
– 用户必须能够喜欢或不喜欢地方。
– 用户必须能够添加新地点并且系统管理员应该对其进行审查。
– 用户必须能够向地点添加图片。
– 如果有权限,用户必须能够更新或删除地点。

– 系统必须能够从最相关到最不相关的位置建议地点。
– 系统必须能够根据用户的查询和位置建议地点。
– 系统必须能够根据受欢迎程度和用户评论推荐地点。
– 系统必须能够监控。
– 系统必须能够通过发送推送通知来推荐新地点。
– 系统应该是高可用的。
– 系统应高度可靠。
– 系统应该是耐用的。
– 系统应具有弹性。
– 系统应该具有很高的成本和性能效率。

基本上系统应该支持 5 个重要的支柱,这些是;

– 可用性
– 可靠性
– 弹性
– 耐用性
– 性价比

这些是我们应该一起考虑的支柱,因为它们是相互耦合的。简而言之,可用性意味着系统应该始终可用。可靠性意味着系统应该按预期工作。弹性意味着如果出现任何问题,系统将如何以及何时恢复自身。持久性是系统每个部分应该存在的支柱,直到我们删除。性价比也是一个重要的话题,基本上与在成本效率下使用服务有关。可以这样说,如果系统将建立在AWS上并且使用t2微型EC2实例就足够了,那么就没有任何理由使用更大的EC2实例并支付额外费用。

请注意,每个系统都会一天天地增长,因此系统在未来会变得更加复杂(例如 5 年后),但重要的是如果您有明确定义的需求和结构,则不会有太多工作来添加新组件到您的系统。

在定义系统边界和功能要求时,需要考虑云或承诺选项。云服务具有成本效益、高速性能、备份解决方案、维护、无限存储容量等诸多优势。您的系统可以;

– %100 承诺(您自己的数据中心/服务器)
– %100 云(AWS、谷歌云、Azure)
– 承诺和云的混合(您可以在迁移过程中同时拥有)

容量估算首先很重要(特别是对于承诺服务),因此您可以估算您需要多少服务器数量或如何分离您的服务以及您将使用哪个数据库?另请注意,您的系统将是大量读取,读取流量将超过写入流量。

假设 5 年内城市指南系统将有 1000 万个名额,每日查询次数将达到 10.000。您可以估计系统每年都会增长,您可以估计缓冲。这意味着您将保留 1000 万个地点信息及其元数据和图片。如果我们假设照片的平均大小为 25 KB,并且每个地方平均有 5 张图片;

容量将大约等于 1000 万 * 5 * 25 KB +(用户元数据信息)+(位置元数据信息)。建议有数据的复制和备份解决方案,以防止数据丢失,因此它会比我们估计的大 3 倍左右。

此计算只是如何定义系统容量的一个简短示例,我们不会计算每日下载/上传容量和元数据容量,但您应该考虑此计算(以及每日读/写容量估计)以进行服务/数据库扩展。

API 估算
城市指南服务可以同时支持 REST 或 SOAP 策略。你也可以考虑 GRPC 这是一个来自谷歌的新策略,GRPC 的最大优势是使用更小的带宽和资源。如果我们继续使用 REST,将会有四个主要的 API 来设计系统。

– AddPlace(api_dev_key, name, description, longitude, latitude, category, Pictures):该API返回带有新添加地点信息的HTTP响应。 (如果成功则接受 202)

– DeletePlace(api_dev_key, placeID):它可以根据您的响应返回 HTTP 响应 200(OK)或 204(无内容)。

– UpdatePlace(api_dev_key, updatePlaceRequest):updatePlaceRequest 将有一个 placeID 来知道我们正在更新哪个地方。

– GetPlace(search_query, userlocation, categoryfilter = null, sortby = ‘distance || popular’, page = 1, distanceRadius = 5, maximum_return_count = 20): 返回带有位置元数据的 JSON。结果将根据用户查询和位置进行排序。

设计和数据库架构

保存的数据将分为 2 部分。第一个将与保持静态内容(如位置图片)有关。如果我们正在构建我们的服务 AWS,我们可以使用 S3 来保存静态内容,并且在 S3 之前,我们可以使用 Cloudfront 作为 CDN。 Cloudfront 将充当缓存,它将帮助系统快速返回响应。如果您正在构建您的系统,那么您需要有图像存储。 S3 是 AWS 产品,它将帮助我们的系统以安全的方式保存静态媒体内容。对于元数据(地点、图片、用户),我们可以使用 SQL 和 NoSQL。 NoSQL 是轻松扩展系统的最佳方式。另一方面,如果我们考虑 SQL 的关系和约束,扩展 SQL 数据库是非常困难的。

地点: ID、AddedBy、Name、Lat、Lng、Category、Description、AddedDate、LikeCount、DisLikeCount
用户: ID、用户名、密码、电子邮件、RegisterDate、LastLoginDate
评论: ID、喜欢、不喜欢、评论、评论日期
UserPlaceComment: ID、CommentID、UserID、PlaceID

8 个字节足以保留 placeID 和 userID。此外,我们将保留 8 个字节的经度和纬度。数据将根据 placeID、lat 和 lng 编制索引。我们将根据地点位置和用户位置向地点表发送查询。所以查询将评估;

(在 userLongitude – Radius 和 userLongitude + Radius 之间的 placeLongitude 和 userLatitude – Radius 和 userLatitude + Radius 之间的 placeLatitude 的地方)

这种方式不是最佳方式,因为我们需要同时计算纬度和经度。我们可以使用网格来解决性能问题。我们可以将整个世界划分为更小的网格。这确保我们只能处理网格的邻居。由于这种方法,我们只关注用户位置网格及其邻居网格。地图是使用网格的最佳选择。键是 gridID。值是此网格中的所有位置。所以查询将评估;

(用户纬度之间的纬度 – 半径和用户纬度 + 半径和 GridID 在(相邻网格)中的地方

请注意,当然我们会有更多的数据库表,这些只是示例。遵循数据库设计过程的规范化规则会很好。

我们也可以根据系统对系统进行分区;

– 用户身份
– 区域 ID
– 位置 ID

使用 userID 和 regionID 会造成分发问题。一些地区可能比其他地区有更多的位置,在这种情况下,系统将不会均匀分布。我们可以根据 locationID 和 regionID 进行分区。

系统设计考虑

如果我们正在设计一个系统,我们需要的基本概念是;

– 客户
– 服务
– 网络服务器
– 应用服务器
– 媒体文件存储
– 数据库
– 缓存
– 复制
– 冗余
– 负载均衡
– 分片

正如我们上面提到的,复制过程是提供高可用性、高可靠性和实时体验的宝贵过程。复制和备份是提供我们之前提到的支柱的两个重要概念。复制是处理服务或服务器故障的一个非常重要的概念。复制可以是应用数据库服务器、Web 服务器、应用服务器、媒体存储等。实际上我们可以复制系统的所有部分。 (某些 AWS 服务,例如 Route53,它们本身具有高可用性,因此您无需处理 Route53、负载均衡器等的复制。)请注意,复制还有助于系统缩短响应时间。你想象一下,如果我们将传入的请求分成更多的资源而不是一个资源,系统可以轻松满足所有传入的请求。此外,每个资源的最佳副本数为 3 个或更多。您可以通过将数据保存在 AWS 中的不同可用区或不同区域来提供冗余。我们可以将相同的数据保存在三个不同的资源上,并且由于这个过程,如果一台服务器宕机,系统会自动继续工作副本。复制的另一个优点是系统可以在更新系统时继续运行。在复制过程中,Master 服务器将负责写入和读取操作;而slave负责读操作。

负载平衡是指在一组服务和服务器之间分配请求。当我们谈到复制和分片时,必须定向传入请求,这是由负载均衡器完成的。我们可以使用 Round-Robin 方法来重定向传入的请求,但在这种情况下可能会出现问题。 Round-Robin 停止向死服务器发送请求,但 Round-Robin 无法停止向暴露于大量流量的服务器发送请求。我们可以准备智能循环算法来解决这个问题。此外,我们可以使用一致的哈希来重定向传入的请求。一致的散列确保系统变得更加均匀分布。

使用负载均衡器进行分片的可能问题是当系统死机时我们如何重建系统。可能的方法是蛮力。这种方法很慢,因为我们需要从头开始重建整个系统。我们可以通过使用反向索引方法来消除这个问题。我们可以有另一个索引服务器来保存有关反向索引的所有信息。反向索引映射所有地方。我们需要构建一个地图,key 是 serverID,value 是所有地方。

对于缓存策略,我们可以通过使用缓存服务器来使用全局缓存机制。我们可以使用 Redis 或 memcache,但缓存策略最重要的部分是如何提供缓存驱逐。如果我们使用全局缓存服务器,我们会保证每个用户在缓存中看到相同的数据,但是如果我们使用全局缓存服务器,就会有时间延迟。作为缓存策略,我们可以使用 LRU(最近最少使用)算法。

对于媒体文件缓存,正如我们之前提到的,我们将使用 CDN。 CDN 位于不同的边缘位置,因此响应时间将比直接从 AWS S3 获取媒体内容要短。

我们可以拥有最流行、最相关、最新等的各种排名机制……如您所知,我们可以拥有许多服务器,我们需要从这些服务器获取数据。由于这个原因,我们需要聚合函数来组合所有这些数据以获得最理想的解决方案。

系统的基本编码

// Java Program to illustrate the Design
public enum UserStatus{
    LOGIN,
    LOGOUT
    ...
}
  
public enum ReviewStatus {
    LIKE,
    DISLIKE
}
  
public enum LocationStatus{
    PUBLIC,
    PRIVATE,
    ...
}
  
public enum LocationCategory {
    EVENT,
    EDUCATION,
    FOOD,
    HEALTH,
    ENTERTAINTMENT,
    ....
}
  
  
public class AddressDetails {
    private String street;
    private String city;
    private String country;
    private double latitude;
    private double longitude;
    ...
}
  
public class AccountDetails {
    private Date createdTime;
    private AccountStatus status;
    private boolean updateAccountStatus(AccountStatus accountStatus);
    ...
}
  
public class Comment {
    private Integer id;
    private User addedBy;
    private Date addedDate;
    private String comment;
  
    public boolean updateComment(String comment);
    ...
}
  
public class Location {
      private Integer id;
    private User createdBy;
    private LocationCategory locationCategory;
    private LocationStatus locationStatus;
  
    private HashSet pictures;
    private HashSet userLikes;
    private HashSet userDisLikes;
    private HashSet userComments;
    ...
}
  
public class User {
    private UserStatus userStatus;
    private Double latitude;
    private Double longitude;
    private AccountDetails accountDetails;
    private UserRelations userRelations;
  
    public List searchLocation(query);
    ...
}
  
public LoginUser extends Users{
    private int id;
    private String password;
    private String nickname;
    private String email;
      
    private boolean updatePassword();
    public boolean addPlace(Place place);
    public boolean updatePlace(Place place);
    public boolean addComment(Integer placeID, String comment);
    public boolean reviewPlace(Integer placeID, ReviewStatus status);
    ....
}

参考:https://developer.foursquare.com/docs/resources/