解耦,未解耦的区别_幂等与时间解耦之旅

解耦,未解耦的区别

HTTP中的幂等性意味着相同的请求可以执行多次,效果与仅执行一次一样。 如果用新资源替换某个资源的当前状态,则无论您执行多少次,最终状态都将与您仅执行一次相同。 举一个更具体的例子:删除用户是幂等的,因为无论您通过唯一标识符删除给定用户多少次,最终该用户都会被删除。 另一方面,创建新用户不是幂等的,因为两次请求该操作将创建两个用户。 用HTTP术语来说是RFC 2616:9.1.2等幂方法必须说的:

9.1.2等幂方法

方法还可以具有“ 幂等 ”的特性,因为[…] N> 0个相同请求的副作用与单个请求的副作用相同。 GET,HEAD,PUT和DELETE方法共享此属性。 同样,方法OPTIONS和TRACE不应有副作用,因此本质上是幂等的。

时间耦合是系统的不良特性,其中正确的行为隐含地取决于时间维度。 用简单的英语来说,这可能意味着例如系统仅在所有组件同时存在时才起作用。 阻塞请求-响应通信(ReST,SOAP或任何其他形式的RPC)要求客户端和服务器同时可用,这就是这种效果的一个示例。

基本了解这些概念的含义后,我们来看一个简单的案例研究- 大型多人在线角色扮演游戏 。 我们的人工用例如下:玩家发送优质短信,以在游戏内购买虚拟剑。 交付SMS时将调用我们的HTTP网关,我们需要通知部署在另一台计算机上的InventoryService 。 当前的API涉及ReST,其外观如下:

@Slf4j
@RestController
class SmsController {private final RestOperations restOperations;@Autowiredpublic SmsController(RestOperations restOperations) {this.restOperations = restOperations;}@RequestMapping(value = "/sms/{phoneNumber}", method = POST)public void handleSms(@PathVariable String phoneNumber) {Optional<Player> maybePlayer = phoneNumberToPlayer(phoneNumber);maybePlayer.map(Player::getId).map(this::purchaseSword).orElseThrow(() -> new IllegalArgumentException("Unknown player for phone number " + phoneNumber));}private long purchaseSword(long playerId) {Sword sword = new Sword();HttpEntity<String> entity = new HttpEntity<>(sword.toJson(), jsonHeaders());restOperations.postForObject("http://inventory:8080/player/{playerId}/inventory",entity, Object.class, playerId);return playerId;}private HttpHeaders jsonHeaders() {HttpHeaders headers = new HttpHeaders();headers.setContentType(MediaType.APPLICATION_JSON);return headers;}private Optional<Player> phoneNumberToPlayer(String phoneNumber) {//...}
}

依次产生类似于以下内容的请求:

> POST /player/123123/inventory HTTP/1.1
> Host: inventory:8080
> Content-type: application/json
>
> {"type": "sword", "strength": 100, ...}< HTTP/1.1 201 Created
< Content-Length: 75
< Content-Type: application/json;charset=UTF-8
< Location: http://inventory:8080/player/123123/inventory/1

这很简单。 SmsController只需通过发布购买的剑SmsController适当的数据转发到SmsController inventory:8080服务。 该服务立即或201 Created返回201 Created HTTP响应,确认操作成功。 此外,还会创建并返回到资源的链接,因此您可以对其进行查询。 有人会说:ReST是最新技术。 但是,如果您至少关心客户的钱并了解什么是ACID(比特币交易所还必须学习的东西:请参阅[1] , [2] , [3]和[4] )–该API也是易碎,容易出错。 想象所有这些类型的错误:

  1. 您的请求从未到达inventory服务器
  2. 您的请求已到达服务器,但被拒绝
  3. 服务器接受连接,但无法读取请求
  4. 服务器读取请求但挂起
  5. 服务器处理了请求,但发送响应失败
  6. 服务器发送了200 OK响应,但丢失了,您再也没有收到
  7. 收到服务器的响应,但客户端无法处理它
  8. 服务器的响应已发送,但客户端更早超时

在所有这些情况下,您仅在客户端获得一个异常,而您不知道服务器的状态是什么。 从技术上讲,您应该重试失败的请求,但是由于POST不具有幂等性,因此您最终可能会用一把以上的剑来奖励玩家(在5-8情况下)。 但是,如果不重试,您可能会失去游戏玩家的金钱而又不给他他宝贵的神器。 肯定有更好的办法。

将POST转换为幂等PUT

在某些情况下,通过将ID生成基本上从服务器转移到客户端,从POST转换为幂等PUT会非常简单。 使用POST的是服务器生成剑的ID,并将其发送到Location标头中的客户端。 事实证明,在客户端急切地生成UUID并稍稍更改语义加上在服务器端强制执行一些约束就足够了:

private long purchaseSword(long playerId) {Sword sword = new Sword();UUID uuid = sword.getUuid();HttpEntity<String> entity = new HttpEntity<>(sword.toJson(), jsonHeaders());asyncRetryExecutor.withMaxRetries(10).withExponentialBackoff(100, 2.0).doWithRetry(ctx ->restOperations.put("http://inventory:8080/player/{playerId}/inventory/{uuid}",entity, playerId, uuid));return playerId;
}

该API如下所示:

> PUT /player/123123/inventory/45e74f80-b2fb-11e4-ab27-0800200c9a66 HTTP/1.1
> Host: inventory:8080
> Content-type: application/json;charset=UTF-8
>
> {"type": "sword", "strength": 100, ...}< HTTP/1.1 201 Created
< Content-Length: 75
< Content-Type: application/json;charset=UTF-8
< Location: http://inventory:8080/player/123123/inventory/45e74f80-b2fb-11e4-ab27-0800200c9a66

为什么这么大? 简单地说(不需要双关语),客户端现在可以根据需要重试PUT请求多次。 服务器首次收到PUT时,会将剑以客户端生成的UUID( 45e74f80-b2fb-11e4-ab27-0800200c9a66 )作为主键45e74f80-b2fb-11e4-ab27-0800200c9a66在数据库中。 在第二次尝试PUT的情况下,我们可以更新或拒绝该请求。 使用POST不可能,因为每个请求都被视为购买新剑–现在我们可以跟踪是否已经有这样的PUT。 我们只需要记住,后续的PUT并不是错误,而是更新请求:

@RestController
@Slf4j
public class InventoryController {private final PlayerRepository playerRepository;@Autowiredpublic InventoryController(PlayerRepository playerRepository) {this.playerRepository = playerRepository;}@RequestMapping(value = "/player/{playerId}/inventory/{invId}", method = PUT)@Transactionalpublic void addSword(@PathVariable UUID playerId, @PathVariable UUID invId) {playerRepository.findOne(playerId).addSwordWithId(invId);}}interface PlayerRepository extends JpaRepository<Player, UUID> {}@lombok.Data
@lombok.AllArgsConstructor
@lombok.NoArgsConstructor
@Entity
class Sword {@Id@Convert(converter = UuidConverter.class)UUID id;int strength;@Overridepublic boolean equals(Object o) {if (this == o) return true;if (!(o instanceof Sword)) return false;Sword sword = (Sword) o;return id.equals(sword.id);}@Overridepublic int hashCode() {return id.hashCode();}
}@Data
@Entity
class Player {@Id@Convert(converter = UuidConverter.class)UUID id = UUID.randomUUID();@OneToMany(cascade = ALL, fetch = EAGER)@JoinColumn(name="player_id")Set<Sword> swords = new HashSet<>();public Player addSwordWithId(UUID id) {swords.add(new Sword(id, 100));return this;}}

上面的代码片段中很少有快捷方式,例如直接将存储库注入到控制器,以及使用@Transactional注释。 但是你明白了。 还要注意,假设没有完全同时插入两个具有相同UUID的剑,此代码相当乐观。 否则将发生约束违例异常。

旁注1:我在控制器和JPA模型中都使用UUID类型。 开箱即用不支持它们,对于JPA,您需要自定义转换器:

public class UuidConverter implements AttributeConverter<UUID, String> {@Overridepublic String convertToDatabaseColumn(UUID attribute) {return attribute.toString();}@Overridepublic UUID convertToEntityAttribute(String dbData) {return UUID.fromString(dbData);}
}

对于Spring MVC同样(仅单向):

@Bean
GenericConverter uuidConverter() {return new GenericConverter() {@Overridepublic Set<ConvertiblePair> getConvertibleTypes() {return Collections.singleton(new ConvertiblePair(String.class, UUID.class));}@Overridepublic Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {return UUID.fromString(source.toString());}};
}

附注2:如果无法更改客户端,则可以通过将每个请求的哈希存储在服务器端来跟踪重复项。 这样,当多次发送同一请求(客户端重试)时,它将被忽略。 但是有时我们可能会有合法的用例,可以两次发送完全相同的请求(例如,在短时间内购买两把剑)。

时间耦合–客户不可用

您认为自己很聪明,但是仅重试就不够了。 首先,客户端可以在重新尝试失败的请求时死亡。 如果服务器严重损坏或关闭,重试可能要花费几分钟甚至几小时。 您不能仅仅因为下游依赖项之一关闭而就阻止了传入的HTTP请求-如果可能,您必须在后台异步处理此类请求。 但是,延长重试时间会增加客户端死亡或重新启动的可能性,这可能会使我们的请求松动。 想象一下,我们收到了优质的SMS,但是InventoryService目前处于关闭状态。 我们可以在第二,第二,第四等之后重试,但是如果InventoryService停机了几个小时又碰巧我们的服务也重新启动了怎么办? 我们只是失去了短信和剑从未被赋予玩家的机会。

解决此问题的方法是先保留未决请求,然后在后台处理它。 收到SMS消息后,我们几乎没有将玩家ID存储在名为“ pending_purchases数据库表中。 后台调度程序或事件唤醒异步线程,该线程将收集所有未完成的购买并将尝试将其发送到InventoryService (甚至可能以批处理方式?)每隔一分钟甚至一秒钟运行一次的周期性批处理线程,并收集所有未完成的请求将不可避免地导致延迟和不必要数据库流量。 因此,我打算使用Quartz调度程序,它将为每个待处理的请求调度重试作业:

@Slf4j
@RestController
class SmsController {private Scheduler scheduler;@Autowiredpublic SmsController(Scheduler scheduler) {this.scheduler = scheduler;}@RequestMapping(value = "/sms/{phoneNumber}", method = POST)public void handleSms(@PathVariable String phoneNumber) {phoneNumberToPlayer(phoneNumber).map(Player::getId).map(this::purchaseSword).orElseThrow(() -> new IllegalArgumentException("Unknown player for phone number " + phoneNumber));}private UUID purchaseSword(UUID playerId) {UUID swordId = UUID.randomUUID();InventoryAddJob.scheduleOn(scheduler, Duration.ZERO, playerId, swordId);return swordId;}//...}

和工作本身:

@Slf4j
public class InventoryAddJob implements Job {@Autowired private RestOperations restOperations;@lombok.Setter private UUID invId;@lombok.Setter private UUID playerId;@Overridepublic void execute(JobExecutionContext context) throws JobExecutionException {try {tryPurchase();} catch (Exception e) {Duration delay = Duration.ofSeconds(5);log.error("Can't add to inventory, will retry in {}", delay, e);scheduleOn(context.getScheduler(), delay, playerId, invId);}}private void tryPurchase() {restOperations.put(/*...*/);}public static void scheduleOn(Scheduler scheduler, Duration delay, UUID playerId, UUID invId) {try {JobDetail job = newJob().ofType(InventoryAddJob.class).usingJobData("playerId", playerId.toString()).usingJobData("invId", invId.toString()).build();Date runTimestamp = Date.from(Instant.now().plus(delay));Trigger trigger = newTrigger().startAt(runTimestamp).build();scheduler.scheduleJob(job, trigger);} catch (SchedulerException e) {throw new RuntimeException(e);}}}

每当我们收到优质的SMS时,我们都会安排异步作业立即执行。 Quartz将负责持久性(如果应用程序关闭,则在重新启动后将尽快执行作业)。 而且,如果该特定实例出现故障,则另一个可以承担这项工作–或我们可以形成集群并在它们之间进行负载平衡请求:一个实例接收SMS,另一个实例在InventoryService请求剑。 显然,如果HTTP调用失败,则稍后重新安排重试时间,一切都是事务性的且具有故障保护功能。 在实际代码中,您可能会添加最大重试限制以及指数延迟,但是您了解了。

时间耦合–客户端和服务器无法满足

我们为正确执行重试所做的努力是客户端和服务器之间模糊的时间耦合的标志-它们必须同时生活在一起。 从技术上讲,这不是必需的。 想象玩家在48小时内向客户服务发送一封包含订单的电子邮件,他们手动更改了库存。 同样的情况也适用于我们的情况,但是用某种消息代理(例如JMS)替换电子邮件服务器:

@Bean
ActiveMQConnectionFactory activeMQConnectionFactory() {return new ActiveMQConnectionFactory("tcp://localhost:61616");
}@Bean
JmsTemplate jmsTemplate(ConnectionFactory connectionFactory) {return new JmsTemplate(connectionFactory);
}

建立ActiveMQ连接后,我们可以简单地将购买请求发送给经纪人:

private UUID purchaseSword(UUID playerId) {final Sword sword = new Sword(playerId);jmsTemplate.send("purchases", session -> {TextMessage textMessage = session.createTextMessage();textMessage.setText(sword.toJson());return textMessage;});return sword.getUuid();
}

通过用JMS主题上的消息传递完全替换同步请求-响应协议,我们暂时将客户端与服务器分离。 他们不再需要同时生活。 此外,不止一个生产者和消费者可以相互交流。 例如,您可以有多个购买渠道,更重要的是:多个利益相关方,而不仅仅是InventoryService 。 更好的是,如果您使用像Kafka这样的专用消息传递系统, 则从技术上讲,您可以保留数天(数月)的消息而不会降低性能。 好处是,如果将另一个购买事件的使用者添加到InventoryService旁边的系统,它将立即收到许多历史数据。 而且,现在您的应用程序在时间上与代理耦合,因此,由于Kafka是分布式和复制的,因此在这种情况下它可以更好地工作。

异步消息传递的缺点

在ReST,SOAP或任何形式的RPC中使用的同步数据交换很容易理解和实现。 从延迟的角度来看,谁在乎这种抽象会疯狂地泄漏(本地方法调用通常比远程方法快几个数量级,更不用说它可能因本地未知的众多原因而失败),因此开发起来很快。 消息传递的一个真正警告是反馈渠道。 因为没有响应管道,所以您可以不再只是“ 发送 ”(“ return ”)消息而已。 您要么需要带有一些相关性ID的响应队列,要么需要每个请求临时的一次性响应队列。 我们还撒谎了一点,声称在两个系统之间放置消息代理可修复时间耦合。 确实如此,但是现在我们耦合到了消息传递总线,它也可能会崩溃,特别是因为它通常处于高负载下,有时无法正确复制。

本文展示了在分布式系统中提供保证的一些挑战和部分解决方案。 但是,归根结底,请记住,“ 仅一次 ”语义几乎不可能轻松实现,因此仔细检查您确实需要它们。

翻译自: https://www.javacodegeeks.com/2015/02/journey-to-idempotency-and-temporal-decoupling.html

解耦,未解耦的区别

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/338116.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

c语言getch()的用法是什么?

C语言中getch()函数功 能&#xff1a; 从stdio流中读字符&#xff0c;即从控制台读取一个字符&#xff0c;但不显示在屏幕上用 法:int getchar(void);这个函数是一个不回显函数&#xff0c;当用户按下某个字符时&#xff0c;函数自动读取&#xff0c;无需按回车&#xff0c;有的…

rx.observable_在Spring MVC流中使用rx-java Observable

rx.observableSpring MVC现在已经支持异步请求处理流程了一段时间&#xff0c;该支持内部利用了Tomcat / Jetty等容器的Servlet 3异步支持。 Spring Web Async支持 考虑一下需要花一点时间处理的服务呼叫&#xff0c;该服务呼叫具有延迟&#xff1a; public CompletableFutur…

淮安中专学计算机哪个学校好,2021淮安初中十强排名 哪些初中比较好

对于淮安的学生来说&#xff0c;了解淮安初中排名会更有利于选择初中。那么&#xff0c;2021淮安初中十强有哪些学校呢?下面小编整理了一些相关信息&#xff0c;供大家参考!2021淮安十大初中排名1、淮安兴隆中学2、淮安郑梁梅中学华禹分校3、淮安高堰九年制学校4、淮安长江路中…

C 隐式类型转换是什么?

C 隐式类类型转换《C Primer》中提到&#xff1a;“可以用 单个形参来调用 的构造函数定义了从 形参类型 到 该类类型 的一个隐式转换。”这里应该注意的是&#xff0c; “可以用单个形参进行调用” 并不是指构造函数只能有一个形参&#xff0c;而是它可以有多个形参&#xff0…

maven 插件未找到_防止在多模块Maven中找到“未找到插件”

maven 插件未找到在多模块Maven项目的子模块上定义Maven插件会给我们一个“未找到插件”错误。 特别是如果我们有一个多模块项目&#xff0c;并且只想在一个特定模块中应用Maven插件&#xff0c;则此错误会经常发生。 假设我们有一个看起来像这样的多模块root pom。 <proj…

文科女生单招学计算机,文科女生走单招学什么专业好

对于文科女生来说&#xff0c;想要走高职单招选择什么专业好呢?有哪些专业适合文科女生来学习呢?有途网小编为大家整理了一些专业。语言类专业对于高职单招的专业来说&#xff0c;如今的社会发展对于纯中文的专业并不看好&#xff0c;所以说如果文科女生想要学习语言类高职单…

工程师必备:C/C 单元测试万能插桩工具

研发效能是一个涉及面很广的话题&#xff0c;它涵盖了软件交付的整个生命周期&#xff0c;涉及产品、架构、开发、测试、运维&#xff0c;每个环节都可能影响顺畅、高质量地持续有效交付。在腾讯安全平台部实际研发与测试工作中我们发现&#xff0c;代码插桩隔离是单元测试工作…

html拖拽手势,h5实现手势操作放大缩小拖动等

最近开发遇到了这个需求&#xff0c;使用vue开发h5加一个手势放大缩小的功能&#xff0c;移动端的手势操作用原生的写法太麻烦&#xff0c;而且体验还不好&#xff0c;所以从github找到一个hammer.js的一个手势操作插件。官方文档地址&#xff1a;http://hammerjs.github.io/文…

selenium持续集成_使用Selenium进行Spring Boot集成测试

selenium持续集成Web集成测试允许对Spring Boot应用程序进行集成测试&#xff0c;而无需进行任何模拟。 通过使用WebIntegrationTest和SpringApplicationConfiguration我们可以创建加载应用程序并在正常端口上侦听的测试。 Spring Boot的这一小增加使使用Selenium WebDriver创建…

这才是你想要的C语言学习路线!

点击上方蓝字关注我&#xff0c;了解更多咨询作为一门古老的编程语言&#xff0c;大家熟知它不仅是因为拥有48年的发展历程&#xff0c;更主要还是因为当下大部分程序员走入编程世界第一个学习的语言就是C语言。而近年来高速发展的物联网和智能设备&#xff0c;又把C语言推向了…

html流式布局插件,Jquery瀑布流网格布局插件

插件描述&#xff1a;一款简单且高度可定制的jQuery瀑布流网格布局插件。通过该瀑布流网格插件你可以动态添加和删除各种尺寸的图片&#xff0c;定义图片宽度&#xff0c;设置网格的列数&#xff0c;或使用流式布局方式&#xff0c;甚至还可以通过URL动态添加图片。使用该瀑布流…

mongodb上限集合_用Java创建MongoDB上限集合

mongodb上限集合在MongoDB中&#xff0c;可以以循环方式将文档的插入顺序保留到集合中。 这些类型的集合在MongoDB中称为上限集合。 MongoDB文档描述了上限集合&#xff1a; 上限集合是固定大小的集合&#xff0c;它们支持高吞吐量操作&#xff0c;这些操作根据插入顺序来插入…

编程语言:C语言与Java的细致对比,你知道选谁了吗?

点击上方蓝字关注我&#xff0c;了解更多咨询1.Java与C语言各自的优势C语言是面向过程的语言&#xff0c;执行效率高;Java是面向对象的语言&#xff0c;执行效率比C语言低。C语言最关键的是比Java多了指针&#xff0c;这也说明了Java的健壮性&#xff0c;还有Java的多线程机制使…

ajax实现表单验证 html,Ajax+ajax做的表单验证

//Ajx实现异步示例&#xff0c;blur实现失去焦点触发jQuery(#formname).blur(check);function check(){alert("开始执行Ajax");//判断用户是否存在var formname jQuery("#formname").val();if(formname""){jQuery(#msgName).html(表单名称不能为…

java ee maven_针对新手的Java EE7和Maven项目–第8部分

java ee maven第一部分 &#xff0c; 第2部分 &#xff0c; 第3部分 &#xff0c; 第4部分 &#xff0c; 第5部分 &#xff0c; 第6部分 &#xff0c; 第7部分 第8部分 自上一篇文章以来&#xff0c;这一系列教程已经有很长时间了。 是时候恢复并在我们的简单项目中添加…

Python、Perl 垫底,C语言才是最环保的编程语言

点击上方蓝字关注我&#xff0c;了解更多咨询提到编程语言&#xff0c;人们第一时间想到的无非是&#xff1a;哪个编程语言简单易学&#xff0c;亦或是最挣钱等。但是编程语言功耗问题却被很多人忽视。那么作为程序员的我们如何选择编程语言&#xff0c;使其以低能耗高功效地工…

改变数据类型的装饰器_用装饰器改变收藏

改变数据类型的装饰器装饰图案 自从第一次学习编程设计模式以来&#xff0c;装饰器模式一直是我的最爱。 在我看来&#xff0c;这是一个很新颖的想法&#xff0c;比其他想法有趣得多。 不要误会我的意思&#xff0c;其他大多数人也引起了我的注意&#xff0c;但没有什么比装饰器…

khoury计算机科学学院,东北大学Open House中国站

东北大学Open House中国站 -10月26日北京 & 10月27日上海东北大学向金吉列留学的学生发来诚挚邀请&#xff0c;欢迎您的学生前来参加东北大学于 10 月 26 日(周六)在北京 和 10 月 27 日(周日)在上海 举办的东北大学 Open House 教育展。在这两个 Open House 教育展上&…

jvm jstat_使用jstat报告自定义JVM指标集

jvm jstat我一直缺少在JStat中配置自定义标头的可能性 。 当然&#xff0c;有很多预定义的数据集&#xff0c;但是如果我们可以创建自己的数据集&#xff0c;那就更好了。 正如您可能已经设计的那样&#xff0c;我正在写这篇文章&#xff0c;因为这样的功能当然是可用的:)不幸的…

知识分享:值得学习的C语言经典开源项目

点击上方蓝字关注我&#xff0c;了解节气咨询听上去有些荒谬&#xff0c;C语言的产生竟然源于一个失败的项目。1969年&#xff0c;通用电气、麻省理工学院和贝尔实验室联合创立了一个庞大的项目——Multics工程。该项目的目的是创建一个操作系统&#xff0c;但显然遇到了麻烦&a…