04-03.eri-test 使用Spring WebFlux构建URL Shortening API(以及许多支持的强制转换)

介绍

在过去的几周中,尤其是在过去的几天里,由于我们正在与大流行病斗争而建议留在家中(呆在家里,伙计们!这绝不是夸张的建议),我读了很多文章并尝试新技术。在其中一种情况下,我遇到了很棒的文章通过苏尼尔关于URL缩短应用程序如何工作以及阅读并了解体系结构之后,我认为开发它是一种消磨时间的好方法。我之前写过关于我如何努力为实践项目提供想法的文章这里和这里,因此听起来很合理。

解释架构

借用SunilPV在其文章中的图表,我将解释我用于开发该项目的技术及其原因。首先,让我们看一下图:

到目前为止,我见过的每个URL缩短应用程序都类似地工作。当用户发送长网址时,API会生成一个短哈希来标识长网址,并将这两种信息都保存到数据库中,然后当用户请求sho。rt/杂凑API会获取哈希值及其所代表的长网址,然后重定向到该网址。很简单。

该体系结构最重要的部分是API本身,由图中的绿色框表示,该API假定在负载均衡器后面有许多实例,这意味着需要对其进行分发。我们拥有用于保存URL信息的数据库,用于缓存目的的雷迪斯和用于协调的动物园管理员。让我们进入更多细节:

  • 网址缩短API:API需要分布式且具有弹性。由于这是一个练习新事物的项目,因此我将我所知道的东西(Spring)和我想尝试一段时间的主题(Web助焊剂)混合在一起,因此该应用程序以SpringWeb助焊剂为基础。使用ApacheJMeter进行的小负载测试表明,由于采用了非阻塞方法,因此与普通的旧MVC在高负载下相比,它的弹性要强得多。除此之外,该体系结构还建议使用该库Hashids为了生成URL的哈希值,我遵循了此建议,稍后再进行介绍。

  • 数据库:本文由SunilPV描述的数据库是DynamoDB,但它也提到了关系数据库。我决定使用关系数据库进行简单设置(无需进行云配置等),因为它也为我提供了另一个进行实验的机会。由于应用程序本身是反应性的并且是非阻塞的,因此必须充分利用其余组件,以充分利用其优势。因此,我决定将PostgreSQL与它的反应性驱动程序一起使用,并由SpringR2DBC支持。SpringR2DBC使以前使用过SpringDataJPA的任何人都能轻松使用其存储库,因此虽然不是什么新鲜事物,但是控制器层和数据层都使用相同的语言来交流是很棒的单声道助焊剂遍。

  • 雷迪斯:这是最容易解释的部分,不仅因为在体系结构中建议使用,而且因为它几乎是当今缓存的标准。它用于缓存短urlx长url关系,以使数据库不会因同一URL的大量得到而负担数据库(基本缓存)。Spring还为Redis提供了响应式实现,因此将其用于体系结构是一个好主意。

  • 动物园管理员:也许这是一个开销,尽管文章专门提到了Zookeeper,但它也表示可以用Redis代替它。Zookeeper仅用于存储由API所有实例共享的计数器,该计数器用于生成给定实例将负责的ID范围。让我知道您对此有何想法,因为这对我来说是新的。我第一次使用Zookeeper。

可以找到代码这里和italsohassomeoptionalfeaturesthatIwasexperimentingwithsuchastracingwithZipkinandmetricsexporttoElasticsearch。Sinceitdoesnotmakepartofthecore,Iwillnottalkaboutithere,butfeelfreetoenablethemifyouwant。

代码详细信息

让我们从配置的细节开始代码讨论。配置了两个非常重要的bean,其中一个为方便起见。为了方便起见,我声明一个Hashids豆已经加了盐,这样我每次使用时都不需要重新创建:

扫描二维码关注公众号,回复: 10889538 查看本文章
@豆角,扁豆上市Hashids哈希值(){返回新Hashids(杂凑IdsSalt);}

然后有一个非常简单的Redis配置,但是请注意它是Reactive的。SpringDataRedis已经有一个连接工厂可供使用,我只需要一个ReactiveRedisŤemplate使用:

@豆角,扁豆上市反应性RedisOperations<串,宾语>重新操作(ReactiveRedisConnectionFactoryconnectionFactory) {GenericJackson2JsonRedisSerializer 序列化器 = GenericJackson2JsonRedisSerializer();RedisSerializationContext< 宾语> 语境 =RedisSerializationContext。< 宾语>新SerializationContext 串RedisSerializer())序列化器)。建立();返回 ReactiveRedisŤemplate<>(connectionFactory 语境);}

然后有一个叫做UrlIdRange用于控制应用程序实例负责的id范围。 如前所述,范围是在Zookeeper上配置的,但是每个实例都有自己的ID范围来分配,并且只有在用尽后,它才会再次进入Zookeeper服务器以获取新范围。 它已经联系Zookeeper来创建bean创建范围。

@豆角,扁豆上市 UrlIdRange urlIdRange共享配置服务 sharedConfigurationService {整数 计数器 = sharedConfigurationService得到共享计数erurlRangeKey);返回  UrlIdRange计数器);}

因此,现在是解释共享配置服务UrlIdRange类。

实施共享配置服务使用Apache Curator Framework连接到Zookeeper服务器并在共享计数器上执行操作。 我将花更多时间在此上,因为这是我第一次做,并且欢迎提出有关如何改进甚至说出问题的建议。 该类只有一种方法得到共享计数er以及一个从应用程序属性yml文件获取Zookeeper服务器基本URL的属性。 它收到一个类型然后开始处理:

1-启动客户端:

最后 策展人框架 客户 = 新ClientbaseUrl 重试次数3 100));客户开始();

2-创建共享计数:

共享计数 sharedCounter = new 共享计数客户  0);

3-在尝试-抓住块上,一切都已完成:

try {sharedCounter开始();版本值<整数> 计数器 = sharedCountergetVersionedValue();(!sharedCountertrySetCount计数器 计数器getValue() + 1)) {计数器 = sharedCountergetVersionedValue();}sharedCounter();客户();返回 计数器getValue();
} catch 例外 e {日志错误“启动共享计数器时出错,无法更新计数器。” e); new NotAbleŤoUpdateCounter例外();
}

共享计数 is started and then the application get its versioned value and try to set the new value for the 计数器, it will only set if the values hasn't changed, otherwise it will keep trying。 I did 这个 way because I was thinking about the distributed characteristic of the application, in a high load environment collisions could happen and it would be a trouble for the management of the ids。 I don't know if it is good practice要么even if it is really necessary, so give me a heads up about what could go wrong要么not。 Ťhen I close the 计数器和client and 返回 the value。

UrlIdRange使用当前计数器在Bean创建时调用构造函数,然后执行以下操作:

上市 UrlIdRange整数 计数器 {这个计算范围计数器);这个hasNext = 真正;
}

上市 虚空 计算范围整数 计数器 {这个初始值 = 计数器 * 100_000;这个当前值 = new 原子整数初始值);这个finalValue = 初始值 + 99_999;thishasNext = 真正;
}

的constructor calls 计算范围计算完成后,范围为100。000 id,如果计数器为0,则范围为0至99。999,依此类推。 也许我在数字上有些夸张,可以通过。yml文件进行配置。

随着初始配置的完成,现在该深入探讨Web助焊剂了。 正如我之前所说,API有两个端点,一个是GET以查找URL并重定向,另一个是开机自检以生成新的短URL。 路由器非常简单:

@豆角,扁豆
上市 路由器功能<服务器响应> 路线网址信息Handler 处理程序 错误处理程序 errorHandler {返回 路由器功能路线()onErrorExceptionclass errorHandler::handleErrorGET“ / {url}” 处理程序::findLongUrlAndRedirectPOST“ /” 接受媒体类型APPLICATION_JSON), 处理程序::generateAndSaveShortUrl建立();
}

人们可能会发现@RequestMappingSpring MVC中的注解很方便,但是我认为像这样的路由器功能(将所有路由都定义在一个地方)也有其优势,它更容易看到可用的路由,并且Spring提供了一些功能来使其在大型应用程序中井井有条。 由于Spring Boot是为微服务创建的,因此每个应用程序都不应有很多路由,因此这不是问题。

然后我们有网址信息Handler好玩的地方。 第一次面对声明式编程非常有趣,也许有点不知所措,但是由于我已经对Javascript及其声明性,非阻塞风格有一定的经验,所以还不错,但是我确实需要向一些人展示 的同事以后看看他们是否了解正在发生的事情。 让我们从findLongUrlAndRedirect方法:

上市 单声道<服务器响应> findLongUrlAndRedirect服务器请求 serverRequest { shortUrl = serverRequestpathVariable“网址”);返回 cacheServicegetFromCacheOr供应商shortUrl 网址信息class () -> urlInfoServicefindByShortUrlshortUrl))doOnNexturlInfo -> 日志信息“找到的网址信息:{}” urlInfo))flatMapurlInfo -> 永久重定向URI创建urlInfogetLongUrl())。建立())switchIfEmpty状态HttpStatus.未找到.身体价值错误响应.未找到shortUrl, 错误代码.URL_INFO_NOT_FOUND)));
}

我得到了shortUrl从请求开始,魔术就开始了。 首先我去cacheService看看是否网址信息已经在那里了,如果没有的话供应商给定被称为。 据我所知,由于@Cacheable不支持Spring中Redis缓存的响应式方法,而且它的简便性,据我所知,我决定使用这种方法。 也许我可以使用AOP,但是我认为这种方式与其余代码,充满了lambda函数或其他代码非常匹配,或者功能介面,如Java中所述。 然后我打电话给doOnNext()仅用于记录目的,然后flatMap重定向。 如果为空,则应用程序以404状态响应。 生成短网址的方法在响应式意义上的工作原理类似,尽管逻辑有所不同,您可以在资料库

实施缓存服务之所以有趣,是因为它利用了Reactor专有的缓存功能:

上市 <T> 单声道<T> getFromCacheOrSupplier , <T> 爵士乐, Supplier<单声道<T>> 或其他 {返回 Cache单声道.抬头k -> redisOps.价值()。get建立Key, 爵士乐)).地图w -> 信号.下一个爵士乐.w))), .onCacheMissResume或其他.和WriteWith((k, v -> 单声道.fromRunnable(() ->redisOps.价值()。setIfAbsentbuildKeyk, 爵士乐), v.get())。订阅()));
}

使用Cache单声道我可以获得专门用于缓存的反应式控制流程,这也使我的供应商可以如此顺畅地使用onCacheMissResume。 但首先它使用抬头 method to call Redis with the given 键, very straightforward since Redis reactive interfaces have the same usage as the blocking ones, but delivering a 单声道<T>给我们。 最后,如果未命中,则使用和WriteWith。 无论如何,对象都会返回给我们,继续进行流程。 非常有趣的逻辑,请告诉我是否有任何需要改进的地方!

尽管没有详细介绍POST路由,但有趣的是看到了保存IfNotExists来自网址信息Service,由处理程序调用以生成短网址并保存,但仅当短网址不存在时,才调用以下三个方法的链:

//尝试查找长网址,如果为空,则保存请求
上市 单声道<UrlInfo> 保存IfNotExistUrlGenerateRequest 请求 {返回 findByLongUrl请求.getLongUrl())。switchIfEmpty保存请求));
}

//这是哈希值起作用的地方,对url范围给定的id进行编码
私人的 单声道<UrlInfo> saveUrlGenerateRequest 请求 {整数 id = getUrlId(); hash = 哈希值.编码id);返回 urlInfoRepository.saveUrlInfo.建造者().longUrl请求.getLongUrl()).shortUrlhash.到期时间请求.getExpiryAt()).build());
}

//获取范围的当前值并处理范围不足
私人的 整数 getUrlId() {if (!urlIdRange.hasNext()) {整数 counter = sharedConfigurationService.getSharedCounterurlRangeKey);urlIdRange.计算范围(counter);}return urlIdRange.getCurrentValue();
}

The last important part that is left to be mentioned is the data layer using R2DBC. This is also very straightforward since Spring Data R2DBC repositories doesn't differ much来自standard, differing only by returning Mono or Flux。 我注意到的一件事是,我没有找到一种在R2DBC上映射表关系的方法,当然,对于该类东西,没有javax注释或Hibernate注释,并且在文档中也没有找到对此的任何引用。 但是,我可能会误会。 让我知道您是否对此有所了解。

结论

获得应用程序架构图,添加您自己的想法并将其具体化为代码,这真是令人愉快的体验。 希望以后能找到更多此类文章。 还有其他我在这里没有提到的实验,例如文档和测试,请看一下资料库要查看我对此所做的事情,我可能会再写一篇关于它的文章。 感谢所有到达帖子结尾的人(很长),让我听听您的想法!

from: https://dev.to//leoat12/building-an-url-shortening-api-with-spring-webflux-and-a-lot-of-supporting-cast-3ni8

发布了0 篇原创文章 · 获赞 0 · 访问量 124

猜你喜欢

转载自blog.csdn.net/cunbang3337/article/details/105559792
今日推荐