1. ホーム
  2. Redis

redis アプリケーション編 ---- スパイク、サインイン、セッション共有

2022-02-19 09:33:07
<パス

I. セコンド

フロントエンドのための商品のスパイクは塩ああ何を追加するには、ああリンクをアンチシェイクを行うことができ、バックエンドのためにしばしば直面しなければならないデータベースの永続層は、実際には、解決策は、それが上に保持しない聖歌、右保持していないことであることができます。

スパイク前は、ユーザーが商品詳細ページを何度も更新していたため、ページリクエストが大量に発生していました。そこで、スパイクの商品詳細ページと、通常の商品詳細ページを分離する必要があります。スパイク商品詳細ページでは、サーバーによる動的な判定が必要なスパイクボタン以外の要素は静的で、その他の静的データはブラウザやCDNでキャッシュできるように工夫しています。こうすることで、スパイク前にページを更新することで発生するトラフィックのうち、サーバー側に入るのはごく一部にとどまります。

CDNは、トラフィックの遮断の最初のレベルであり、トラフィックの遮断の第二レベルは、我々はRedisの読み取り/書き込み分離をサポートするために使用します。この段階では、主にデータを読んで、読み書き分離Redisは完全に需要をサポートできる、最大60万+ qpsをサポートすることができます。最初のステップは、データ制御モジュールを介して読み書きRedisに事前にスパイクアイテムをキャッシュし、次のようにスパイクの開始マーカーを設定します。

"goodsId_count": 100 // total
"goodsId_start": 0 //start marker
"goodsId_access": 0 // number of orders accepted


  • スパイクが始まる前に、サービスクラスタはgoodsId_Startを0と読み、そのままnot startedを返す。
  • データ制御モジュールは、goodsId_start を 1 に変更し、スパイクの開始を示す。
  • サービスクラスタは開始マーカービットをキャッシュし、リクエストの受け付けを開始し、RedisのgoodsId_accessに残りの商品数を(goodsId_count - goodsId_access)としてログを記録します。
  • 受け付けた注文の数がgoodsId_countに達すると、すべてのリクエストはブロックされ続け、残り個数は0になる。

発注に正常に参加した後、下位のサービスレベルに入り、注文情報の確認と在庫の控除を開始する。データベースへの直接アクセスを避けるため、在庫控除にはマスター・スレーブ版のRedisを使用することができます。Redisを使用して在庫クエリを最適化し、失敗した秒数のリクエストを事前にインターセプトすることで、システム全体のスループットを大幅に向上させることができます。
ハッシュの中に総数と発注数を格納することができるストレージ設計になっています。

"goodsId" : {
    "Total": 100
    "Booked": 0
}


金額が引き落とされると、サーバーはRedisにリクエストすることで発注資格を得ますが、これは以下のluaスクリプトで実装されています。Redisはシングルスレッドなので、luaは複数のコマンドに対してアトミック性を担保しています。

local n = tonumber(ARGV[1])
if not n or n == 0 then
    return 0       
end                
local vals = redis.call("HMGET", KEYS[1], "Total", "Booked");
local total = tonumber(vals[1])
local blocked = tonumber(vals[2])
if not total or not blocked then
    return 0       
end                
if blocked + n <= total then
    redis.call("HINCRBY", KEYS[1], "Booked", n)                                   
    return n;   
end                
return 0


Redisをメッセージキューとして使用し、受注に成功した注文を非同期にインバウンド永続化する。

II. チェックイン

毎日のチェックイン情報を永続層に書き込むと、データ量が非常に大きくなるだけでなく、統計処理も非常に不便になる。Redisのビットマップを使えば、小さなレコードを保存し、素早くクエリすることが可能です。

  • ビットマップは実際のデータ型ではなく、String型に定義されたバイト指向の操作の集合体である。文字列はバイナリセーフのブロックであるため、最大長は512Mで、2^32個の別バイトに設定するのが最適です。
  • ビットマップは、2種類のビット操作に分けられる。1. 文字列のビットを1または0に設定したり、ビットの値を取得したりするような、一定時間ごとの単一ビット操作 2. ビットの集合に対する操作で、与えられたビットの範囲内で1にセットされたビットの数を数えるもの(人口統計学など)。
  • ビットマップの最大のメリットは、データを格納する際の容量を大幅に削減できることです。例えば、あるプロジェクトでユーザーを識別するために自己増殖型IDを使用すると、40億人のユーザーに関する情報(例えば、ユーザーが新しい通知を受け取りたいかどうか、1と0で識別)を記録するのに、わずか512Mのメモリーしか使用できません。
SETBITキーオフセット値

キーの値(文字列)のビット値をオフセットで設定またはクリアします。その位置のビットは、値(0か1しかない)によって決定されるように、セットまたはクリアされる。キーが存在しない場合、新しい文字列値が作成され、その文字列はオフセットにビット値を持つのに十分な大きさであることが確認される。パラメータoffsetは0以上、2^32未満である必要があります(ビットマップサイズは512MBに制限されます)。キーに対応する文字列のサイズが大きくなると、ビット値の新しい部分が0に設定される。

GETBITキーオフセット

offset で指定されたキーに対応する文字列のビット値を返す。offsetが範囲外の場合、文字列は0ビットで埋め尽くされた連続した空間であるとみなされる。キーが存在しない場合は空文字列と見なされるため、offsetは常に範囲外であり、値も0ビットで満たされた連続した空間であると見なされます。

BITCOUNT キー [開始 終了].

戻り値:1に設定されているビットの数。startとendパラメータはGETRANGEコマンドと同様に設定され、どちらも負の値を使うことができます:例えば、-1は最後のビット、-2は最後から2番目のビット、といった具合です。存在しないキーは空文字列として扱われ、存在しないキーに対するBITCOUNT演算は0になります。
例えば、2021年7月18日にログインしたユーザーは0の位置に1、翌日もログインしたユーザーは1の位置に1というように、1週間にログインした累積日数をカウントする場合、bitcountを使用することでユーザーのビットマップを生成することができます。

iii. シェアードセッション

マルチインスタンス展開の場合、複数のインスタンス間でログイン情報を同期させることが必要ですが、redisを使用する速度を考えると、ログイン後にユーザー用のuuidをトークンとして生成し、このuuidと対応するユーザーの情報をredisに格納するのが良い方法です。次のユーザーがサービスにアクセスした後、このuuidを持っている限り、このuuidを使ってredisからユーザー情報を取得し、そのユーザーが誰で、どのような権限を持っているのかを知ることができるようにします。
uuidをキーにした場合の問題は、ユーザーの権限が変更された後に対応するキーを見つける方法がないこと、そしてもちろん、オフラインでユーザーの強制を実現する方法がないことである。この問題については、我々はMySQLを使用することができます、我々は、ユーザーが10トークンを保持することができますが、トークンが生成され、正常に設定されたとき、それは対応するトークンがユーザーを介して見つけることができるようにMySQLに書き込まれ、同時に、我々はあまりにも多くのトークンがあることを確認したら、私たちも有効期限が長い間使用されていないトークンを設定でき、また、聞いておく必要があります さらに、一度キー有効期限のレコードは自動的に削除する必要があります有効期限の切れた鍵の通知を聞くために必要があります。コード


@Configuration
public class AddAuthInterceptorConfig implements WebMvcConfigurer {

  @Autowired private AuthorizationInterceptor authorizationInterceptor;

  @Override
  public void addInterceptors(final InterceptorRegistry registry) {
    registry.addInterceptor(authorizationInterceptor);
  }
}


@Component
public class AuthorizationInterceptor implements HandlerInterceptor {

  @Autowired private AuthManager authManager;

  @Override
  public boolean preHandle(
      final HttpServletRequest request, final HttpServletResponse response, final Object handler)
      throws Exception {
    if (handler instanceof HandlerMethod) {
      Auth auth = ((HandlerMethod) handler).getMethodAnnotation(Auth.class);
      if (auth ! = null && !authManager.validate(auth, request.getHeader(TOKEN))) {
        throw new AuthException("token is invalid, please log in again!");
      }
    }
    return true;
  }
}



@Component
public class AuthManager {
  @Autowired private TokenManager tokenManager;

  public UserInfo getUserInfo() {
    String token = getRequest().getHeader(TOKEN);
    return tokenManager.getByToken(token);
  }

  private HttpServletRequest getRequest() {
    return ((ServletRequestAttributes) requireNonNull(RequestContextHolder.getRequestAttributes()))
        . getRequest();
  }

  public boolean validate(final Auth auth, final String token) {
    return ofNullable(tokenManager.getByToken(token))
        .map(user -> user.passAuth(auth)) // determine if the user's permissions can access this interface
        .orElse(false);
  }
}

@Component
public class RedisTokenManager implements TokenManager {

  @Autowired private RedisValueOps