SHare——极客分享平台

前置屁话#

我想做一个并不考虑实用性的Web应用。

SHare是一个用于分享资源的平台,它为极客设计,没有漂亮的GUI,只有一个简简单单的命令行。用户可以通过它来发布、查询、获取各种各样的资源。

SHare客户端并不专为Web设计,前后端使用RestAPI交互,在设计时前端页面时需要考虑到这点,比如提供方便的将它当作非Web环境的NodeJS应用的能力。

说了这么多,其实是因为最近在学SpringCloud,所以想来点实战。SHare的后端使用SpringCloud Alibaba生态。

核心功能设计#

用户#

上传资源、下载资源的功能都需要登录(下载免费资源不需要)。

用户具有如下属性

{
    "id": "用户id",
    "email": "用户邮箱",
    "password": "密码哈希",
    "nick": "用户昵称",
    "bio": "个人介绍",
    "coin": 用户金币 保留两位小数,
    "vipExpireTime": 会员到期日,
    "buyCount": 购买次数 整数,
    "buyReturnCount": 购买退货次数 整数,
    "saleCount": 卖出次数 整数,
    "saleReturnCount": 卖出退货次数 整数,
    "buyFactor": 购买因数 两位小数 大于等于1.00,
    "saleFactor": 销售因数 两位小数 小于等于1.00
}

注册登录#

用户使用邮箱和原密码进行注册登录,密码会经过一系列私有的哈希过程。

登陆后,会发布一个为期7天的JWT token,用户在七天内可以通过该token进行登录。

金币#

金币是平台交易的虚拟属性,可以通过发布资源获得,可以通过充值获得。汇率:1元=10金币。

购买相关属性#

当用户购买一个资源后,coin将减去资源的售价,然后buyCount加1。

用户购买一个资源后在24小时内可以退款,因为资源有可能是无效资源。退款后coin将加上资源的售价,但buyCount并不改变,buyReturnCount加1。

为了避免占便宜的用户,即买完就退款的用户,引入buyFactor,它是这样计算:

buyFactor=min(2,buyReturnCount/buyCount10)

假设你购买了32次资源,其中有4次退款,buyFactor=4/3210=1.25,即你需要花费资源价格的1.25倍购买资源。

有了buyFactor后,我们可以重新描述购买的过程

  1. 当用户购买一个资源时,coin=coinpricebuyFactorprice为资源价格,buyCount=buyCount+1
  2. 当用户退款时,coin=coin+buyCostbuyCost为购买时花费的金额,由于有buyFactor存在,所以花费的钱可能并不是资源售价,buyReturnCount=buyReturnCount+1

buyFactor的最大值为2

销售相关属性#

当用户售出一个资源时,coin将加上资源的售价,然后saleCount加1。

购买资源的用户在24小时内可以退款,因为资源有可能是无效资源。退款后coin将减去资源的售价,但卖家的saleCount并不改变,saleReturnCount加1。

为了维护系统内资源的有效性,避免大量的无效资源存在,引入saleFactor,它这样计算:

saleFactor=max(0.50,1saleReturnCount/saleCount)

假设你卖出了20个资源,其中有3个退款的,saleFactor=13/20=0.85,即当你卖出时你只能得到资源售价的85%

有了saleFactor后,我们可以重新描述卖出的过程

  1. 当用户售出一个资源时,coin=coin+pricesaleFactor
  2. 当售出的资源被退款时,coin=coinsaleEarnsaleEarn是售出后实际所得的金额,由于有saleFactor存在,所以赚到的钱可能不是资源售价,saleReturnCount=saleReturnCount+1

购买记录和卖出记录#

如上所述,无论是购买还是卖出,都需要一个记录来做支持。

购买时需要一个购买记录来为用户提供退款选项,并追踪购买所花费的实际金额,卖出则需要一个卖出记录来为维护追踪卖出的实际所得。

对于购买记录,有如下属性

{
    "id": "购买id",
    "resource": {
        购买资源快照结构 可能包含资源id、名字、价格等
    },
    "buyCost": 实际购买花费的金额 两位小数,
    "buyTime": 实际购买时间
}

对于已退款订单,从购买记录中删除,以以下结构移动到退款记录

{
    "id": "购买id",
    "resource": {
        购买资源快照结构 可能包含资源id、名字、价格等
    },
    "refundCoin": 退款金额 两位小数,
    "refundTime": 退款时间
}

对于卖出记录,有如下属性

{
    "id": "卖出id",
    "resource": {
        卖出资源快照,可能包含资源id、价格、资源名等
    },
    "buyer": {
        购买者快照,可能包含头像,昵称等
    },
    "saleEarn": 实际卖出所得 两位小数,
    "saleTime": 实际卖出时间
}

对于已退款订单,从卖出记录中删除,以以下结构移动到退款记录

{
    "id": "卖出id",
    "resource": {
        卖出资源快照,可能包含资源id、价格、资源名等
    },
    "buyer": {
        购买者快照,可能包含头像,昵称等
    },
    "lostCoin": 由于退款从账户中扣减的金额,
    "refundTime": 实际退款时间
}

概念:客户端延迟同步

上面的记录中有很多都有快照的概念,它是在创建记录时对原始数据的部分克隆,而原始数据有可能发生更改,此时有两条路可以走:

  1. 原始数据更改时同步或异步的完成所有依赖该原始数据的更改
  2. 即使原始数据发生改变快照数据也不变

确实,对于不同的业务需求,某些业务可能要求原始数据不改变,比如订单中的资源快照,而京东、淘宝等各大平台也都是这么做的,而对于某些业务,我们可能需要数据跟着原始数据改变,比如购买者快照。

就购买者快照这个场景来说,不同步其实也不会导致什么问题,只是可能会让用户产生疑惑,也就是说我们并不需要非常及时的同步。考虑一个买了数亿个产品的用户(尽管不太可能),如果说它的资料发生变化,那么他所购买的所有产品订单中的购买者快照都要发生变化,不管你怎么做,比如让一个服务器开启后台进程,并具有一定间隔的异步刷新,还是会占用很多资源。

而我们的思路是,让客户端来主动发现这一点,比如当客户端在订单页面点到购买者页面时,购买者的完整即时信息会被刷出,此时客户端会发现订单页面的数据已经老旧了,它可以发一条新的指令来单独让服务器针对这一条订单数据重新拉取用户快照。这增加了客户端和服务器的复杂性,但是,客户端和服务器都可以选择性的实现这一点,即客户端可以完全不检查,服务端也可以完全不提供重新拉取这样的API。

后面我们会在很多场景中使用这个概念

资源#

资源可以来自于百度、阿里云盘。具有如下结构

{
    "url": "百度或阿里云盘链接",
    "code": "提取码",
    "title": "资源名",
    "bio": "资源简介",
    "price": 资源价格 两位小数 0.00~100.00,
    "publishTime": 资源发布时间,
    "publisherId": 资源发布者,
    "publisher": {
        资源发布者快照,包含头像,昵称等
    },
    "lastCheckTime": 上次检查时间,
    "checkPending": 是否正处于检查队列中,
    "state": OK | INVALIDATED,
    "saleCount": 卖出次数,
    "refundCount": 退款次数,
    "directory": {
        目录结构
    },
}

在此发布的资源使用阿里和百度网盘进行托管,购买后显示提取码。

资源审核#

资源发布后进入审核阶段,保存在待审核资源集合中,审核阶段检查资源有效性和合法性。当审核通过,资源被移步到资源集合中,可被检索。

资源检查#

同时,资源发布后也会以小时为单位进行资源的自动检查,主要检查资源的可用性和目录结构。这需要设立大量的服务器才能保证资源检查功能的可用性。资源发布后会立即加入待检查队列。

为了保证服务的可用性,资源检查并不是积极的,不会有后台线程或定时器什么的,保证每个资源都会在1小时后被重新投入到检测队列,而是使用前文提到的客户端延迟同步,当第一个客户端检测到最后检查时间超出1小时,则客户端主动提交重检查请求,将它计入到重检查队列,并将checkPending设为true,以免后面继续被其它客户端加入到重检查队列中。

资源举报#

用户可以对资源进行举报,举报后的资源依然可以正常的被搜索,参加交易,并且不会有任何提示,同时,该资源被加入到举报审核集合中,若审核确认举报信息属实,该资源会被下架,同时email通知发布者和举报者。

资源搜索#

用户可以检索系统中存在的所有资源,系统会对用户输入的内容进行模糊匹配,匹配项:

  1. 资源标题
  2. 资源文件结构中的目录名和文件名
  3. 资源描述(权重较低)

除此之外,用户还可以限定搜索的额外条件。

时间:

  • 全部时间
  • 最近一周
  • 最近一月
  • 最近半年
  • 最近一年

总大小:

  • 用户可以设置文件大小的两个边界

受所用数据类型限制,SHare能够表示的最大文件大小为8192PB,若超出这个大小,则认为是8192PB。

网盘类型:

  • 阿里云盘
  • 百度云盘

架构设计#

组件#

  • 服务中心/配置中心:Nacos
  • 数据库:MongoDB、Redis
  • 消息队列:RabbitMQ
  • 搜索引擎:ElasticSearch
  • 其它技术:Nginx负载均衡、Redis分布式缓存、Sentinel服务保护、Seata事务管理、JWT

微服务#

  1. user-service:负责用户的登录注册、Token的生成以及发布
  2. resource-service:负责资源的发布、更新、投诉等
  3. order-service:负责订单的创建、修改、退款等
  4. payment-service:负责支付,这个支付不是coin的支付,是用户充值coin、重置vip等业务的,实际RMB业务的支付
  5. resource-check-service:资源检测的微服务,使用python实现
  6. manual-resource-review-service:提供人工审核资源的用户界面
  7. email-service:提供email发送的微服务

认证#

我们把一个请求分为两种:

  1. 访客可访问请求,如搜索、登录、注册...
  2. 用户可访问请求,如发布资源、购买资源...

对于用户可访问请求,请求头中必须带有authentication字段,该字段是用户登录的Token凭证。

微服务间认证的问题#

对于一个用户的认证,可能分为以下几个步骤:

  1. 用户是否正在访问需要认证的资源
  2. 若需认证,用户提供的token是否正确(认证 Authentication)
  3. 若认证通过,该用户是否有权访问该资源(授权 Authorizing)

也许在单体应用中,上面的问题并不复杂,但在微服务系统中,需要考虑的事情就多了。用户的一个请求最先到达网关,网关将它转发到一个起始的微服务上,微服务开始处理,并且在这过程中可能调用很多个其它的微服务。很多问题需要考虑:

  1. 是否上述三个验证过程在每个微服务中都要重复一遍
  2. 网关是否应该承担某些认证过程中的通用部分
  3. 网关与微服务之间、微服务与微服务之间如何传递用户的认证信息或者这其中产生的中间认证信息
  4. 网关能够承担认证过程中的多少部分?
  5. 是否应该认为微服务之间的调用是安全的,无需添加额外校验?
  6. ...

最后,针对我们的系统特性,我们给出了这样的答案

网关认证、微服务授权#

认证(Authentication)

因为我们的应用中没有第三方登录,对于所有用户的认证都是相同的,即通过一个颁发给它的JWT Token来认证它的用户身份,这对于所有需要认证的微服务都是相同的。

我们认为没必要在每个微服务中都重复这个过程,这样会造成代码冗余,给负责认证的数据源(如MySQL集群)带来不必要的压力,拖慢微服务调用链响应流程。我们认为,这个认证的过程,应该在网关就处理好,所以网关负责:

  1. 判断访问的资源是否需要认证
  2. 若需认证,则开始认证过程
  3. 若通过,将Token转换成系统中的内部ID,比如该Token所对应的用户ID

授权(Authorizing)

解决了认证问题,我们再来看看授权问题。授权规则和具体的业务紧紧联系,所以,我们不认为它应该由网关处理。授权问题包括:

  1. 当前通过认证的用户id为1,它是否能够删除发布用户id为2的帖子
  2. 当前通过认证的用户id为1,它是否具有足够的金币来购买一个资源
  3. 当前通过认证的用户id为1,它购买资源时是否无需扣除金币(vip)

这些问题与具体业务息息相关,应该由具体的微服务来处理。

需要认证的资源都是已经从网关中认证完毕才会打到具体微服务上,网关调用入口微服务时以及微服务间相互调用时,需要设法传递由Token转换而来的系统内部ID(如用户ID),然后微服务根据该ID来进行授权。

需要注意的是:manual-resource-review-service使用独立的后台,不在Gateway中可访问,它们使用另外的一套认证流程。此外,一些不直接向外提供服务的微服务也是一样,如email-serviceresource-check-service

问题

  1. 微服务间的调用还可能存在部分冗余。比如服务1通过内部ID检索出整个用户的信息,然后调用服务2,服务2可能还需要再次检索整个用户的信息。可以仔细斟酌微服务的调用边界,判断是否需要将这部分消息从服务1直接传递到服务2。需要权衡网络带宽,代码可维护性,对认证数据库造成的压力等等
  2. 当前的认证流程默认系统内部是安全的,若一个攻击者打入微服务群的内部网络,它可以轻易的伪造一个微服务来给另一个微服务发送请求。

流程#

大体的画一个流程,缓存在该图中没有体现出来。

img

缓存#

用户JWT校验缓存#

实际上,用户的每次访问(非公开资源)都需要根据JWT校验用户信息,每次都查库其实是对数据库不利的,并且会让用户觉得系统响应缓慢,所以,将用户的鉴权信息进行缓存是很有必要的。

考虑资源分享站点的用户特性,一般用户都是在几分钟内频繁集中地使用,然后再使用可能需要隔很长时间,所以我们考虑将缓存的过期时间设置为10min

缓存问题的考虑:

  1. 缓存穿透:Spring Cache本就缓存空值
  2. 缓存击穿:这在用户鉴权的情况下不太可能,因为这里并不存在热点数据,每个用户的操作是均匀的。如果非要实现,可以将@Cacheablesync属性设置为true
  3. 缓存雪崩:...
  4. 一致性:当用户密码发生修改时,需要清除该email有关的缓存数据,弱一致
posted @   yudoge  阅读(485)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· winform 绘制太阳,地球,月球 运作规律
· 上周热点回顾(3.3-3.9)
历史上的今天:
2021-09-07 JVM 三 类文件结构 上
点击右上角即可分享
微信分享提示
主题色彩