网站建设与维护税率,网站页面设计培训班,域名建网站公司,国家企业信用信息查询公示系统广东HTTP中的幂等性意味着相同的请求可以执行多次#xff0c;并且效果与仅执行一次一样。 如果用新资源替换某个资源的当前状态#xff0c;则无论您执行多少次#xff0c;最终状态都将与您仅执行一次相同。 举一个更具体的例子#xff1a;删除用户是幂等的#xff0c;因为无论… HTTP中的幂等性意味着相同的请求可以执行多次并且效果与仅执行一次一样。 如果用新资源替换某个资源的当前状态则无论您执行多少次最终状态都将与您仅执行一次相同。 举一个更具体的例子删除用户是幂等的因为无论您通过唯一标识符删除给定用户多少次最终都会删除该用户。 另一方面创建新用户不是幂等的因为两次请求该操作将创建两个用户。 用HTTP术语来说是RFC 26169.1.2等幂方法必须说的 9.1.2等幂方法 方法还可以具有“ 幂等 ”的特性因为[…] N 0个相同请求的副作用与单个请求的副作用相同。 GETHEADPUT和DELETE方法共享此属性。 同样方法OPTIONS和TRACE不应有副作用因此本质上是幂等的。 时间耦合是系统的不良特性其中正确的行为隐含地取决于时间维度。 用简单的英语来说这可能意味着例如系统仅在所有组件同时存在时才起作用。 阻塞请求-响应通信ReSTSOAP或任何其他形式的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) {OptionalPlayer 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();HttpEntityString 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 OptionalPlayer phoneNumberToPlayer(String phoneNumber) {//...}
} 依次产生类似于以下内容的请求 POST /player/123123/inventory HTTP/1.1Host: inventory:8080Content-type: application/json{type: sword, strength: 100, ...} HTTP/1.1 201 CreatedContent-Length: 75Content-Type: application/json;charsetUTF-8Location: http://inventory:8080/player/123123/inventory/1 这很简单。 SmsController只需通过发布购买的剑SmsController适当的数据转发到SmsController inventory:8080服务。 该服务立即或201 Created返回201 Created HTTP响应确认操作成功。 此外还会创建并返回到资源的链接因此您可以对其进行查询。 有人会说ReST是最新技术。 但是如果您至少关心客户的钱并了解什么是ACID比特币交易所仍需学习的知识请参阅[1] [2] [3]和[4] –该API也是易碎容易出错。 想象所有这些类型的错误 您的请求从未到达inventory服务器 您的请求已到达服务器但被拒绝 服务器接受连接但无法读取请求 服务器读取请求但挂起 服务器处理了请求但发送响应失败 服务器发送了200 OK响应但丢失了您再也没有收到 收到服务器的响应但客户端无法处理它 服务器的响应已发送但客户端更早超时 在所有这些情况下您仅在客户端获得一个异常而您不知道服务器的状态是什么。 从技术上讲您应该重试失败的请求但是由于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();HttpEntityString 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.1Host: inventory:8080Content-type: application/json;charsetUTF-8{type: sword, strength: 100, ...} HTTP/1.1 201 CreatedContent-Length: 75Content-Type: application/json;charsetUTF-8Location: 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 JpaRepositoryPlayer, UUID {}lombok.Data
lombok.AllArgsConstructor
lombok.NoArgsConstructor
Entity
class Sword {IdConvert(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 {IdConvert(converter UuidConverter.class)UUID id UUID.randomUUID();OneToMany(cascade ALL, fetch EAGER)JoinColumn(nameplayer_id)SetSword 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 AttributeConverterUUID, 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 SetConvertiblePair 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(Cant 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是分布式和复制的因此在这种情况下它可以更好地工作。 异步消息传递的缺点 在ReSTSOAP或任何形式的RPC中使用的同步数据交换很容易理解和实现。 从延迟的角度来看谁在乎这种抽象会疯狂地泄漏本地方法调用通常比远程方法快几个数量级更不用说它可能由于许多本地未知的原因而失败因此开发起来很快。 消息传递的一个真正警告是反馈渠道。 由于没有响应管道因此您可以不再只是“ 发送 ”“ return ”消息而已。 您要么需要带有一些相关性ID的响应队列要么需要每个请求临时的一次性响应队列。 我们还撒谎了一点声称在两个系统之间放置消息代理可以修复时间耦合。 确实如此但是现在我们耦合到了消息传递总线它也可能会崩溃尤其是因为它经常处于高负载下有时无法正确复制。 本文展示了在分布式系统中提供保证的一些挑战和部分解决方案。 但是归根结底请记住“ 仅一次 ”语义几乎不可能轻松实现因此仔细检查您确实需要它们。 翻译自: https://www.javacodegeeks.com/2015/02/journey-to-idempotency-and-temporal-decoupling.html