私たちの注文ビジネスでは、インターフェースのべき等性を確保する方法
1.トークンメカニズム
1.サーバーは、トークンを送信するためのインターフェイスを提供します。べき等の問題があるビジネスを分析する場合、ビジネスを実行する前にトークンを取得する必要があります。サーバーはトークンをredisに保存します。
2.次に、ビジネスインターフェイス要求を呼び出すときに、トークンを運び、通常は要求のヘッダーに配置します
3.サーバーはトークンにredisがあるかどうかを判断します。存在とは最初のリクエストを意味し、次にトークンを削除してビジネスを実行し続けます。
4.トークンがredisに存在しないと判断された場合は、操作が繰り返され、繰り返されたマークがクライアントに直接返されるため、ビジネスコードが繰り返し実行されることはありません。
危険な:
1.最初にトークンを削除するか、後でトークンを削除します。
最初に削除すると、ビジネスが実際に実行されない可能性があります。再試行すると、前のトークンを取得する必要があります。繰り返し防止設計のため、リクエストを実行できず
、削除するとビジネスが正常に処理される可能性がありますが、サービスはが中断され、タイムアウトが発生し、削除されません。トークン、その他は再試行を続け、ビジネスが2回実行されるようになりました。
最終的に最初にトークンを削除するように設計しました。ビジネスコールが失敗した場合は、トークンを再度取得してください。もう一度リクエストしてください
2 。トークンの取得、比較、削除はアトミックである必要があります
redis.get(token)、token.equals、redis.del(token)、これら2つの操作がアトミックでない場合、高い同時実行性の下で、すべてが同じデータを取得し、判断が成功し、ビジネスが継続する可能性があります。同時に実行
この操作は、luaスクリプトを使用してredisで実行できます
"if redis.call( 'get'、KEYS [1])== ARGV [1] then return redis.call( 'del'、KEYS [1])else return 0終了」
2.さまざまなロック機構
1.悲観的なデータベースロック
select * from xxx where id = 1 for update;
更新を照会するときにこのレコードをロックします。他の人は待つ必要があります
ペシミスティックロックは通常、トランザクションと一緒に使用されます。データロック時間は非常に長くなる可能性があり、実際の状況に応じて選択する必要があります。さらに、idフィールドは主キーまたは一意のインデックスである必要があることに注意してください。ロックテーブルの結果を引き起こして処理する可能性があります。非常に面倒です。
2.データベースの楽観的なロック
この方法は、更新シナリオに適しています
t_goods set count = count-1、version = version + 1を更新します。ここで、good_id = 2およびversion = 1
バージョンバージョンに応じて、つまり、データインベントリを操作する前に現在の製品のバージョン番号を取得し、操作時にバージョンバージョン番号を取得します。インベントリを初めて操作するときに、次のように分類します。
バージョンが1の場合、在庫サービスバージョン= 2が呼び出されますが、注文サービスに戻る際に問題が発生します。注文サービスは在庫サービスを再度呼び出します。注文サービスによって渡されたバージョンが1のままの場合は、上記を実行します。
バージョンが2になり、where条件が確立されていないため、sqlステートメントは実行されません。これにより、何度呼び出されても1回だけ処理されます。楽観的ロックは主に問題の処理に使用されます。より多くの読み取りとより少ない書き込みの。
3.ビジネス層の分散ロック。
たとえば、複数のマシンが同じデータを同時に処理する可能性がある場合、複数のマシンが時間指定タスクで同じデータを取得する場合、分散ロックを追加してこのデータをロックし、後でロックを解放できます。処理が完了しました。ロックを取得するには、最初にデータが処理されたかどうかを確認する必要があります。
3.さまざまな固有の制約
1.データベースの一意の制約。
データを挿入するときは、注文番号などの一意のインデックスに従って挿入する必要があります。同じ注文に対して2つの注文を挿入することはできません。データベースレベルでの重複を防ぎます。
このメカニズムは、データベースの主キーの一意性制約を利用して、挿入シナリオのべき等問題を解決しますが、主キーの要件は自動インクリメントされた主キーではないため、ビジネスはグローバルに一意性を生成する必要があります主キー
サブデータベースとサブテーブルのシナリオの場合、ルーティングルールでは、同じリクエストが同じデータベースと同じテーブルに送信されるようにする必要があります。そうしないと、主キーが異なるため、データベースの主キー制約が有効になりません。データベースとテーブルは関連していません
2. Redisはアンチウェイトを設定します。
多くのデータを処理する必要があり、一度しか処理できません。たとえば、データのMD5を計算してredisに入れることができます。
セット、データが処理されるたびに、最初にMD5がすでに存在するかどうかを確認し、存在する場合は処理しないでください
4.アンチウェイトテーブル
重複排除テーブルの一意のインデックスとして注文テーブルorderNoを使用し、一意のインデックスを重複排除テーブルに挿入してからビジネスオペレーションを実行すると、それらは同じトランザクション内にあるため、重複排除により繰り返し要求が保証されます。 -重複排除テーブルは一意です
制約により、要求が失敗し、べき等などの問題が回避されるため、重複排除テーブルとビジネステーブルは同じデータベースに存在する必要があります。これにより、同じトランザクションで、ビジネスオペレーションが失敗した場合でも、重複排除テーブルのデータが返されるこれを降りる
データの整合性を保証し、
Redisアンチウェイトもトークンメカニズムです
5.グローバルリクエストの一意のID
インターフェイスが呼び出されると、一意のIDが生成され、redisがデータをコレクションに保存し(重複排除)、存在する場合に処理されます。nginxを使用して、リクエストごとに一意のIDを設定できます。
proxy_set_header X-Request-Id $ Request_id。
ここで示すのは、次のようなトークンメカニズムです。
クリックして決済し、注文決済ページにジャンプするときは、バックエンドでべき等の繰り返し防止トークンを生成してフロントエンドに送り返し、検証のためにバックエンドに渡す必要があります。注文が送信されます。
/**
* 去结算
* 给订单确认ye返回数据
*/
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
//要返回的大对象
OrderConfirmVo confirmVo = new OrderConfirmVo();
//在TreadLocal中获取用户
MemberResVo memberResVo = LoginUserInterceptor.loginUser.get();
//Feign + 异步任务 需要共享RequestAttributes 每个任务都要setRequestAttributes()
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
//每一个线程都要共享之前的请求数据
RequestContextHolder.setRequestAttributes(requestAttributes);
//1 远程查询大对象的第一个属性 收货地址列表
List<MemberAddressVo> address = memberFeignService.getAddress(memberResVo.getId());
confirmVo.setAddress(address);
//Feign在远程调用之前要构造请求,调用很多的拦截器
}, executor);
CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
//每一个线程都要共享之前的请求数据
RequestContextHolder.setRequestAttributes(requestAttributes);
//2 远程查询大对象的第二个属性 所有购物项
List<OrderItemVo> items = cartFeignService.currentUserItems();
confirmVo.setItems(items);
}, executor).thenRunAsync(() -> {
List<OrderItemVo> items = confirmVo.getItems();
List<Long> ids = items.stream().map((item) -> {
return item.getSkuId();
}).collect(Collectors.toList());
//远程查看库存
R r = wareFeignService.getSkuHasStock(ids);
List<SkuStockVo> skuStockVos = r.getData(new TypeReference<List<SkuStockVo>>() {
});
if (skuStockVos != null) {
Map<Long, Boolean> booleanMap = skuStockVos.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
confirmVo.setStocks(booleanMap);
}
}, executor);
//3 远程查询用户积分
Integer integration = memberResVo.getIntegration();
confirmVo.setIntegration(integration);
//4 其他的数据自动计算
//TODO 5 防重令牌
String token = UUID.randomUUID().toString().replace("-", "");
//给服务器一个 并指定过期时间
redisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberResVo.getId(), token, 30, TimeUnit.MINUTES);
//给页面一个
confirmVo.setOrderToken(token);
CompletableFuture.allOf(getAddressFuture, cartFuture).get();
return confirmVo;
}
クリックして注文を送信すると、注文が生成され、支払いページにリダイレクトされます。
将来、注文を生成するときに、注文の繰り返し送信を回避するためのインターフェイスのべき等性を確保するために、検証のために繰り返し防止トークントークンを渡す必要があります。ここでは、アトミックトークンを検証するときに、スクリプトを使用して次のことを確認します。比較と削除はアトミックセクシャルです。分散ロックに似ていますが、ここでは
// 1、最初にトークンを確認します
// 0失敗-1成功|存在しません0削除しますか?1:0
String script = "if redis.call( 'get'、KEYS [1])== ARGV [1] then return redis.call( 'del'、KEYS [1])else return 0 end";
/**
* 提交订单 去支付
*
* @Transactional 是一种本地事物,在分布式系统中,只能控制住自己的回滚,控制不了其他服务的回滚
* 分布式事物 最大的原因是 网络问题+分布式机器。
* (isolation = Isolation.REPEATABLE_READ) MySql默认隔离级别 - 可重复读
*/
//分布式事务 全局事务
//@GlobalTransactional 不用
@Transactional
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
//共享前端页面传过来的vo
confirmVoThreadLocal.set(vo);
//要返回到大对象
SubmitOrderResponseVo responseVo = new SubmitOrderResponseVo();
//登录的用户
MemberResVo memberResVo = LoginUserInterceptor.loginUser.get();
responseVo.setCode(0);
//1、首先验证令牌
//0失败 - 1成功 | 不存在0 存在 删除?1:0
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
String orderToken = vo.getOrderToken();
//原子验证令牌 和 删除令牌
Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),
Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberResVo.getId()),
orderToken);
if (result == 0) {
//令牌验证失败 非0失败码
responseVo.setCode(1);
return responseVo;
} else {
//令牌验证成功 -> 执行业务代码
//下单 去创建订单 验证令牌 验证价格 锁库存
//TODO 3 保存订单
OrderCreatTo orderCreatTo = creatOrder();
BigDecimal payAmount = orderCreatTo.getOrder().getPayAmount();
BigDecimal payPrice = vo.getPayPrice();
//金额对比
if (Math.abs(payAmount.subtract(payPrice).doubleValue()) < 0.01) {
//保存到数据库
saveOrder(orderCreatTo);
//库存锁定 只要有异常就回本订单数据
WareSkuLockVo wareSkuLockVo = new WareSkuLockVo();
wareSkuLockVo.setOrderSn(orderCreatTo.getOrder().getOrderSn());
List<OrderItemVo> collect = orderCreatTo.getOrderItems().stream().map((item) -> {
OrderItemVo orderItemVo = new OrderItemVo();
orderItemVo.setSkuId(item.getSkuId());
orderItemVo.setCount(item.getSkuQuantity());
orderItemVo.setTitle(item.getSkuName());
return orderItemVo;
}).collect(Collectors.toList());
wareSkuLockVo.setLocks(collect);
/**
* TODO 4 远程锁库存 非常严重
* 库存成功了,但是网络原因超时了,订单可以回滚,库存不能回滚
* 为了保证高并发,库存需要自己回滚。 这样可以采用发消息给库存服务
* 库存服务本身也可以使用自动解锁模式 使用消息队列完成 使用延时队列
*/
R r = wareFeignService.orderLockStock(wareSkuLockVo);
if (r.getCode() == 0) {
//锁成功
responseVo.setOrder(orderCreatTo.getOrder());
//TODO 5 远程扣减积分
//库存成功了,但是网络原因超时了,订单回滚,库存不回滚
// int i = 1 / 0;//模拟积分系统异常
//TODO 订单创建成功,发消息给MQ
rabbitTemplate.convertAndSend("order-event-exchange", "order.create.order", orderCreatTo.getOrder());
return responseVo;
} else {
//锁定失败
String msg1 = (String) r.get("msg");
throw new NoStockException(msg1);
}
} else {
responseVo.setCode(2);
return responseVo;
}
}
}