目次
ダブル 11 で商品をゲットしたり、イベントの場所を確保したり、電車のチケットを手に入れたりするなど、スナップやスナップアップのような活動は生活のいたるところにあります。これらのアクティビティでは、サーバーへのアクセス要求が同時に大量に発生することが多く、適切に処理しないとサーバーがクラッシュし、アクティビティを正常に開始できなくなります。
具体的には、商品を棚に並べ、在庫が100個あり、フラッシュ販売活動を行っている、といった問題に直面するかもしれません。イベントが開始されると、ユーザーはすぐに 1,000 件のリクエストを同時に送信しました。これは在庫をはるかに上回っていました。現時点で、同時リクエストに直面した場合は、次のことを確認する必要があります。
- 売られすぎ現象がない、つまり在庫がマイナスになっていない
- データベースサーバーがジャンプしないように、データベースに入るリクエストの数を制限します。
- ユーザーからの悪意のあるリクエストを回避するには、頻繁なクリックリクエストを拒否する必要があります
- 過度に長い業務処理プロセスを回避する、つまり実行時間を保証する
多数の同時リクエストの場合、通常は次のスキームに基づいて処理できます。
1. ビジネスロジック処理
アクティビティの開始時に多すぎるリクエストに応答できない場合は、最初にリクエストをフィルタリングするビジネス ロジックを検討できます。例えば、ダブルイレブンの事前注文活動を制限するには、ユーザーが最初に登録できるようにし、イベントの開始時に割り当てを持っているユーザーのみがリクエストを送信できるようにすることで、ビジネスレベルでのリクエストを削減します。
現実に応じてさまざまなビジネスを設計する必要があります。
2. データベースレベルで保証
同時リクエストに直面して、最も困難なことは多くの場合、データベースでの書き込み操作です。読み取り操作では同時実行性の問題がないため、データベースでオプティミスティック ロックを使用してデータベース データの最下位を保証できます。
楽観的ロック: つまり、タスクのすべてのリクエストが同時にデータを変更するとは考えず、変更中に他のスレッドによって変更されたかどうかのみを判断し、変更された場合には、ここでの操作はキャンセルされます。
原則: mysql は更新操作を実行するときに、そのバージョンが現在のバージョン番号であるかどうかを自動的に判断します (つまり、version=1 を読み込んだときに、更新する際の where の判断条件にも version=1 を使用します)。実行、行 データのバージョン+1。
mysql の更新操作はロックされているため、2 つのリクエストが同時に送信されると、2 つのリクエストによって読み取られるバージョンは両方とも 1 になります。前のリクエストが最初に更新操作を実行し、バージョンは 2 に変更されます。後のリクエストは更新操作の前に実行されます。 update バージョンが 2 であるため、read 1 と不整合であると判断され、操作が取り消されます。
実装方法: mybatis の @version アノテーションでマークされたフィールドをバージョン番号 (楽観的ロック フィールド) として使用し、データ テーブルにバージョン フィールドを作成します。
3. mysql更新行ロックを使用する
楽観的ロックと同様に、mysql の更新操作はロックによって実行されるため、更新ステートメントに判定条件を追加して、データベース レベルが売られすぎないようにすることができます。つまり、2 つのリクエストが同時に送信された場合、両方のリクエストによって読み取られるインベントリは 1 です。最初のリクエストは最初に更新操作を実行し、インベントリ -1 を 0 に変更します。後者のリクエストはインベントリが 0 であると判断する前に更新を実行します。更新条件を満たさない場合、アクションは実行されません。
実装方法:更新テーブルセット count = count -1 and count>0
4. Redis に基づいてリクエスト数を制御する
Redis の原則的なデクリメントを使用して、大量のリクエストに対処し、操作のためにデータベースに入る人の数を制御できます。
原則: Redis は高いシングルスレッド パフォーマンスと高速なメモリ操作を備えていますが、マルチスレッドにはコンテキスト スイッチングがあり、スレッドの安全性を確保するためにマルチスレッドをロックする必要があるため、効率が低下します。Redis のアトミックなデクリメント操作により、リクエストが 1 つずつデクリメントされるため、受信リクエストの数が確保されます。
実装方法: まず、時間指定タスクによってアクティビティが開始される前にインベントリー (つまり、許可されるリクエストの数) を Redis にキャッシュし、次に Redis のインクリメント操作を使用してインベントリーを減らし、いつになったかを判断します。在庫が 0 未満の場合、例外がスローされます。
デモ:
タイミングタスク:
//定时任务,缓存库存
@Scheduled(cron = "0 0 6 * * ?")
//秒 分 时 日 月 周 年(可选);*表所有可能的值,-指定范围值,/表示步长
public void updateSeckillStatue(){
log.info("修改球场状态及秒杀时间");
List<SeckillGoods> list = seckillCourtMapper.selectList(null);
for (SeckillGoods goods:list ){
//将库存缓存进redis
redisUtil.set("seckill-goods:" + goods.getId(), goods.getCount());
}
}
seckill インターフェースで判断します。
//判断库存,redis原子递减
long count = redisUtil.decr("seckill-goods:" + goods_id, 1);
if (count < 0){
//该球场已被订购,抛出异常
throw new GlobalException(SeckillCodeMsg.SECKILL_COURT_NULL);
}
注: Redis にキャッシュされたインベントリは、実際のインベントリではなく、サービス レイヤーに入る人数を制御するためにのみ使用されます。
5. フローピーククリッピングに mq を使用する
非同期処理に mq を使用し、コンシューマの最大数を設定できます。これにより、データベースに入るリクエストの数が減り、トラフィックのピーク クリッピングの目的が達成されます。
原則: RabbitMQ キューは先入れ先出しの順序になります。つまり、1000 個のリクエストが同時に行われ、リクエストはコンシューマの最大数に従って制限されます。コンシューマの最大数が 100 で在庫が 100 の場合、最初の 100 個のリクエストは注文の取得に成功し、最後の 900 個のリクエストは注文の取得に成功します。リクエストは順序の取得に失敗する運命にあります。
実装方法: コンシューマの最大数を設定ファイルで設定し、コントローラ内のインターフェイスがプロデューサとして機能し、サービス ビジネス クラスがコンシューマとして機能してメッセージを監視します。
デモ: (Jackson シリアル化クラスはここでは省略されています)
設定ファイル:
spring:
rabbitmq:
host: localhost
username: guest
password: guest
port: 5672
publisher-confirm-type: correlated #发布确认模式:correlated即交换机收到消息后触发回调方法
publisher-returns: true #回退消息,当找不到routing key对应的队列时,是否回退信息
listener:
simple:
concurrency: 10 #消费者最少数量
max-concurrency: 100 #消费者最大数量
prefetch: 1 #消费者每次处理一条消息
auto-startup: true
default-requeue-rejected: false #消息被拒绝是否重新进入队列
プロデューサー:
OrderMessageDto orderMessageDto = new OrderMessageDto(seckill_id,userId,ip);
//rabbitmq异步处理订单
rabbitTemplate.convertAndSend(EXCHANGE_ORDER,"order_route", orderMessageDto,
new CorrelationData(String.valueOf(seckill_id)));
消費者:
@RabbitListener(queues = QUEUE_ORDER)
@Transactional(rollbackFor = GlobalException.class)
public void receiveOrderMsg(@Headers Channel channel, Message message) {
log.info("订单队列接收到消息:"+new String(message.getBody()));
OrderMessageDto orderMessageDto = JSON.parseObject(new String(message.getBody()), OrderMessageDto.class);
//捕捉异常,方便业务失败时进行回滚
try {
//业务代码逻辑
//确认消息消费成功
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}catch (Exception e){
log.info("订单处理异常");
e.printStackTrace();
}
}
ルーティング例外コールバックに関連するコードはここでは省略されています。Springboot に統合された Rabbitmq - メッセージ永続化 - CSDN ブログを参照してください。
通常の状況では、最終的に注文を作成してデータベース在庫を減算するときに、非同期処理に mq を使用します。
6. 電流制限アルゴリズムを使用する
カウンタ メソッドを使用してフローを制限することも、Spring Cloud Gateway がトークン バケット アルゴリズムを使用してリクエストの数を制限することもできます。
カウンタ アルゴリズムの実装方法: Redis カウンタ アルゴリズムに基づいて、ポートに電流制限処理が適用されます。redisを使用してアクセス数をキャッシュし、有効期限を定義し、有効期限内に該当の値に達するとアクセスを制限します。主にカスタム アノテーション + AOP を使用して、インターフェイスのフローを制限し、1 秒あたりのリクエスト数を制限します。
デモ:
注釈:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface RequestLimit {
//允许访问次数
int permitRequest();
//过期时间
int second();
}
aop の側面:
@Slf4j
@Aspect
@Component
public class RequestLimitAop {
@Resource
private AuthJwtProperties authJwtProperties;
@Resource
private RedisUtil redisUtil;
@Resource
private JwtTokenUtil tokenUtil;
@Around("@annotation(com.seven.seckill.annotation.RequestLimit)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
//拿limit的注解
RequestLimit limit = method.getAnnotation(RequestLimit.class);
if (limit != null) {
//允许的请求数
int requestCount = limit.permitRequest();
//key过期时间
int second = limit.second();
//获取用户id
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
String token = attributes.getRequest().getHeader(authJwtProperties.getHeader());
//处理前缀
if (StringUtils.isNotEmpty(token) && token.startsWith(TokenConstants.PREFIX))
{
token = token.replaceFirst(TokenConstants.PREFIX, StringUtils.EMPTY);
}
//从token中获取用户id
String userId = tokenUtil.getUserIdFromToken(token);
Integer flag = (Integer) redisUtil.get("cloud-court-seckill:"+method.getName()+":"+userId);
if (flag==null){
//设置计数器,second 秒过期
redisUtil.set("cloud-court-seckill:"+method.getName()+":"+userId, 1, second);
}else if (flag < requestCount){
//请求数+1
redisUtil.incr("cloud-court-seckill:"+method.getName()+":"+userId,1);
}else {
//限流
log.warn("请求过于频繁");
throw new GlobalException(ExceptionCodeMsg.SYSTEM_BUSY);
}
}
return joinPoint.proceed();
}
}
インターフェースにアノテーションを追加します。
@RequestLimit(permitRequest = 5,second = 2) //限制用户每2秒点击次数为5
public Result<?> test(){
return new Result<>(ResultEnum.SUCCESS);
}
注: この方法は通常、単一ユーザーの頻繁なクリックを制限するために使用されます (そのため、コードは Redis のクリック数のキーとしてユーザー ID を使用します)。ユーザーはインターフェイスにアクセスするために最初にログインする必要があります。
springcloud ゲートウェイに基づくトークン バケット電流制限アルゴリズムについては、springcloud —— ゲートウェイ関数 extension_tang_seven のブログ - CSDN ブログを参照してください。
7. 分散ロックを使用する
最後に、パニック購入活動をさらに保護する必要がある場合は、分散ロックを使用できます。
詳細については、Springboot Integrates Redis - Implementing Distributed Locks - CSDN Blogを参照してください。
確かなことは、ロックはコードの効率に確実に影響を与えるため (1 つずつ処理する必要があるため)、ロックされたコード セグメントが長すぎることは容易ではなく、独自に設計する必要があるということです。