ホームページ  >  記事  >  データベース  >  Redis での分散セッションの不整合についてどうすればよいか

Redis での分散セッションの不整合についてどうすればよいか

青灯夜游
青灯夜游転載
2021-11-12 10:47:142528ブラウズ

分散セッションに一貫性がない場合はどうすればよいですか?次の記事では、redis における分散セッションの不一致の解決策を紹介します。

Redis での分散セッションの不整合についてどうすればよいか

分散セッションの不整合ソリューション

1. セッションの役割は何ですか?

  • #セッションは、クライアントとサーバー間の通信セッション追跡テクノロジーであり、サーバーとクライアントは通信全体の基本的なセッション情報を維持します。 [関連する推奨事項:

    Redis ビデオ チュートリアル ]

  • クライアントが初めてサーバーにアクセスすると、サーバーは sessionId で応答し、それをローカルに保存します。 Cookie を使用すると、その後のアクセスでは、サーバーにアクセスするためのリクエスト ヘッダーに Cookie の sessionId が挿入されます。

  • この sessionId で対応するデータが見つからない場合、サーバーは新しい sessionId がクライアントに応答されました。

2. 分散セッションにはどのような問題がありますか?

単一サーバーの Web アプリケーションでは、セッション情報はサーバーに保存するだけで済みます。これは、過去数年間でセッション情報にアクセスする最も一般的な方法です

しかし、近年、分散システムの普及により、単一のシステムでは何百万ものユーザーの増大するニーズに対応できなくなり、多くの企業でクラスターに展開されたサーバーが使用されています。同時実行性の高いリクエストがサーバーに到着し、クラスター内のサーバーに分散されたロード バランシングが使用されます。これにより、同じユーザーからの複数のリクエストがクラスター内の異なるサーバーに分散される可能性があり、セッション データが取得できないため、セッション共有が行われます。が一つの質問になります。

Redis での分散セッションの不整合についてどうすればよいか

3. サービスのクラスタリングは一般的にどのように行われますか?

SpringBoot プロジェクトを作成し、ポート番号を変更していくつかのポートを開始し、nginx を統合リバース プロキシとして使用します。

    SpringCloud マイクロサービス プロジェクトでは、この時点でリボン ローカル負荷分散を使用できます。
4. nginx 負荷分散とリボン負荷分散の違い

nginx 負荷分散はサーバー側の負荷です負荷分散アルゴリズムに基づいて、1 つのアドレスに均一にアクセスし、どのサーバーにアクセスするかを決定します。

    リボン ロード バランシング。これはローカル ロード バランシング (クライアント ロード バランシング) であり、サービスを提供するクライアント アドレスをキャッシュして記録し、ローカル アルゴリズムに基づいてロード バランシングを実装します。
  • #5. セッション整合性ソリューション

1. セッション レプリケーション (同期)

アイデア: 複数のサーバーが相互にセッションを同期し、各サーバーにすべてのセッションが含まれるようにします。

Redis での分散セッションの不整合についてどうすればよいか利点: サーバーによってサポートされる機能と、アプリケーションがサポートする機能コードを変更する必要はありません

欠点:

セッション同期にはデータ送信が必要であり、イントラネット帯域幅を占有し、遅延が発生します

すべて サーバーにはすべてのセッションが含まれますデータ量はメモリによって制限されており、水平方向に拡張することはできません
  • 2. クライアントの保存方法

アイデア: サーバーはすべてのユーザーのセッションを保存しますが、これは多くのメモリを消費します。セッションはブラウザの Cookie に保存できます。各クライアントは 1 人のユーザーのデータのみを保存する必要があります。

Redis での分散セッションの不整合についてどうすればよいか 利点: サーバーは保存する必要がありません

欠点:

各 http リクエストはセッションを伝送し、これにより外部ネットワーク帯域幅が考慮されます。データは端末内やネットワーク通信に保存されるため、漏洩、改ざん、盗難などのセキュリティリスクがあります。

セッションに保存されるデータサイズとドメイン名Cookieの数には制限があります
  • 注: この解決策は一般的には使用されませんが、確かに 1 つの考え方です。
  • 3. リバース プロキシ ハッシュの一貫性

アイデア: 高可用性を確保するために、サーバーには複数の冗長リバース プロキシがあります。同じユーザーからのリクエストが同じサーバーに送られるようにするための何かはありますか? オプション 1: 4 層プロキシ ハッシュ

リバース プロキシ層は、ユーザーの IP をハッシュに使用して、同じ IP を確保します。リクエストは同じサーバーに送信されます

オプション 2: 7 層プロキシ ハッシュRedis での分散セッションの不整合についてどうすればよいか

リバース プロキシは、ハッシュに http プロトコルの特定のビジネス属性 (sid、city_id、user_id など) を使用します。これにより、ハッシュ戦略をより柔軟に実装して、同じブラウザ ユーザーからのリクエストが同じサーバーに送られるようにすることができます。

利点:

  • nginx 構成を変更するだけで済み、アプリケーション コードを変更する必要はありません
  • ハッシュ属性が均一である限り、負荷分散が可能、複数のサーバーの負荷が分散されます
  • サーバーの水平拡張をサポートできます (セッション同期方法は不可能で、メモリ制限の影響を受けます)

欠点:

  • サーバーが再起動すると、セッションの一部が失われ、ビジネスに影響を及ぼします。たとえば、一部のユーザーが再度ログインすることになります。
  • サーバーが水平に拡張され、再ハッシュ後にセッションが再分散された場合, 一部のユーザーが正しいセッションにルーティングされない可能性があります。

セッションには通常、有効期間があります。この 2 つの欠点は、部分的なセッションの失敗に相当すると考えられます。通常、問題は大きくありません。

4 層ハッシュまたは 7 層ハッシュについては、個人的には前者をお勧めします: 専門的なソフトウェアに専門的なことをさせ、リバース プロキシが転送を担当します。 (ビジネス属性に応じて、複数のコンピュータ ルームおよび複数のサーバーを異なるコンピュータ ルームのサーバーにルーティングする必要がある場合など)。

4 層ロード バランシングと 7 層ロード バランシングの違い

4. バックエンドの統合集中ストレージ

Redis での分散セッションの不整合についてどうすればよいか

利点:

    #セキュリティリスクなし
  • 水平方向に拡張可能、データベース/キャッシュを水平方向に分割可能
  • サービス クライアントの再起動または拡張時にセッションが失われることはありません
欠点: ネットワーク呼び出しが追加され、アプリケーション コードを変更する必要があります

DB ストレージまたはキャッシュ。個人的には後者をお勧めします。 セッション 読み取りの頻度が非常に高く、データベースの負荷が比較的高くなります。セッションの高可用性が必要な場合は、キャッシュの可用性を高めることができますが、ほとんどの場合、セッションが失われる可能性があるため、一般に高可用性を考慮する必要はありません。

概要

セッションの一貫性を確保するためのアーキテクチャ設計の一般的な方法:

    セッションの同期方法: 複数のデバイスサーバーは相互にデータを同期します
  • クライアントの保存方法ユーザーは自分のデータのみを保存します
  • リバース プロキシ ハッシュの一貫性ユーザーのリクエストを保証するために 4 層ハッシュと 7 層ハッシュの両方を実行できます1 つのサーバーで発生する
  • バックエンド ユニファイド ストレージ サーバーは再起動および拡張され、セッションは失われません (バックエンド キャッシュ ユニファイド ストレージを推奨します)

# #6. ケースプラクティス: SpringSession redis は分散セッションの不整合の問題を解決します

##ステップ 1: SpringSession と redis の依存関係パッケージを追加します

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-redis</artifactId>
    <version>1.4.7.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

ステップ 2: 構成ファイル

# 为某个包目录下 设置日志
logging.level.com.ljw=debug

# 设置session的存储方式,采用redis存储
spring.session.store-type=redis
# session有效时长为10分钟
server.servlet.session.timeout=PT10M

## Redis 配置
## Redis数据库索引(默认为0)
spring.redis.database=0
## Redis服务器地址
spring.redis.host=127.0.0.1
## Redis服务器连接端口
spring.redis.port=6379
## Redis服务器连接密码(默认为空)
spring.redis.password=

ステップ 3: インターセプターの構成

@Configuration
public class SessionConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new SecurityInterceptor())
                //排除拦截的2个路径
                .excludePathPatterns("/user/login")
                .excludePathPatterns("/user/logout")
                //拦截所有URL路径
                .addPathPatterns("/**");
    }
}
@Configuration
public class SecurityInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
        HttpSession session = request.getSession();
        //验证当前session是否存在,存在返回true true代表能正常处理业务逻辑
        if (session.getAttribute(session.getId()) != null){
            log.info("session拦截器,session={},验证通过",session.getId());
            return true;
        }
        //session不存在,返回false,并提示请重新登录。
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        response.getWriter().write("请登录!!!!!");
        log.info("session拦截器,session={},验证失败",session.getId());
        return false;
    }
}
HandlerInterceptor

    preHandle: ビジネス プロセッサがリクエストを処理する前に呼び出されます。前処理では、エンコード、セキュリティ制御、権限検証などを実行できます;
    • postHandle: ビジネス プロセッサがリクエストの処理を完了した後、ビューを生成する前に実行されます。後処理 (サービスが呼び出され、ModelAndView が返されますが、ページはレンダリングされません)、ModelAndView
    • afterCompletion を変更する機会があります: DispatcherServlet がリクエストを完全に処理した後に呼び出され、クリーンアップに使用できます。リソースなどをアップします。処理に戻ります (ページがレンダリングされました)
ステップ 4: コントローラー

@RestController
@RequestMapping(value = "/user")
public class UserController {

    Map<String, User> userMap = new HashMap<>();

    public UserController() {
        //初始化2个用户,用于模拟登录
        User u1=new User(1,"user1","user1");
        userMap.put("user1",u1);
        User u2=new User(2,"user2","user2");
        userMap.put("user2",u2);
    }

    @GetMapping(value = "/login")
    public String login(String username, String password, HttpSession session) {
        //模拟数据库的查找
        User user = this.userMap.get(username);
        if (user != null) {
            if (!password.equals(user.getPassword())) {
                return "用户名或密码错误!!!";
            } else {
                session.setAttribute(session.getId(), user);
                log.info("登录成功{}",user);
            }
        } else {
            return "用户名或密码错误!!!";
        }
        return "登录成功!!!";
    }

    /**
     * 通过用户名查找用户
     */
    @GetMapping(value = "/find/{username}")
    public User find(@PathVariable String username) {
        User user=this.userMap.get(username);
        log.info("通过用户名={},查找出用户{}",username,user);
        return user;
    }

    /**
     *拿当前用户的session
     */
    @GetMapping(value = "/session")
    public String session(HttpSession session) {
        log.info("当前用户的session={}",session.getId());
        return session.getId();
    }

    /**
     * 退出登录
     */
    @GetMapping(value = "/logout")
    public String logout(HttpSession session) {
        log.info("退出登录session={}",session.getId());
        session.removeAttribute(session.getId());
        return "成功退出!!";
    }

}

ステップ 5: エンティティ クラス

@Data
public class User implements  Serializable{

    private int id;
    private String username;
    private String password;

    public User(int id, String username, String password) {
        this.id = id;
        this.username = username;
        this.password = password;
    }

}

ステップ 6: アクセス テスト

最初にログインします: http://127.0.0.1:8080/user/login?username=user1&password=user1

もう一度確認してくださいhttp://127.0.0.1:8080/user/find/user1

7. SpringSession の Redis 原理を分析する

ステップ 1: SpringSession の Redis データ構造を分析するSpringSession

127.0.0.1:6379> keys *
1) "spring:session:sessions:9889ccfd-f4c9-41e5-b9ab-a77649a7bb6a"
2) "spring:session:sessions:expires:d3434f61-4d0a-4687-9070-610bd7790f3b"
3) "spring:session:expirations:1635413520000"
4) "spring:session:sessions:expires:9889ccfd-f4c9-41e5-b9ab-a77649a7bb6a"
5) "spring:session:expirations:1635412980000"
6) "spring:session:sessions:d3434f61-4d0a-4687-9070-610bd7790f3b"
共通点: 3 つのキーはすべて SpringSession の Redis データを表す spring:session: で始まります。

クエリ タイプ

127.0.0.1:6379> type spring:session:sessions:d3434f61-4d0a-4687-9070-610bd7790f3b
hash
127.0.0.1:6379> hgetall spring:session:sessions:d3434f61-4d0a-4687-9070-610bd7790f3b
// session的创建时间
1) "creationTime"
2) "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01|\xc5\xdb\xecu"
// sesson的属性,存储了user对象
3) "sessionAttr:d3434f61-4d0a-4687-9070-610bd7790f3b"
4) "\xac\xed\x00\x05sr\x00\x1ecom.ljw.redis.controller.User\x16\"_m\x1b\xa0W\x7f\x02\x00\x03I\x00\x02idL\x00\bpasswordt\x00\x12Ljava/lang/String;L\x00\busernameq\x00~\x00\x01xp\x00\x00\x00\x01t\x00\x05user1q\x00~\x00\x03"
//最后的访问时间
5) "lastAccessedTime"
6) "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01|\xc5\xe1\xc7\xed"
//失效时间 100分钟
7) "maxInactiveInterval"
8) "\xac\xed\x00\x05sr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x17p"

ステップ 2: SpringSession の Redis 有効期限戦略を分析する

期限切れの削除には、通常 3 つのタイプがあります。データ戦略:

スケジュールされた削除。つまり、キーの有効期限を設定しながらタイマーを作成し、キーの有効期限が到来したらすぐに削除します。
  • 遅延削除。つまり、キーにアクセスするときに、キーの有効期限が切れているかどうかを判断し、有効期限が切れている場合は削除し、それ以外の場合はキーの値を返します。
  • 定期删除,即每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。

  • redis删除过期数据采用的是懒性删除+定期删除组合策略,也就是数据过期了并不会及时被删除。

  • 但由于redis是单线程,并且redis对删除过期的key优先级很低;如果有大量的过期key,就会出现key已经过期但是未删除。

  • 为了实现 session 过期的及时性,spring session 采用了定时删除+惰性删除的策略。

定时删除

127.0.0.1:6379> type spring:session:expirations:1635413520000
set
127.0.0.1:6379> smembers  spring:session:expirations:1635413520000
1) "\xac\xed\x00\x05t\x00,expires:d3434f61-4d0a-4687-9070-610bd7790f3b"

Redis での分散セッションの不整合についてどうすればよいか

2) "spring:session:sessions:expires:d3434f61-4d0a-4687-9070-610bd7790f3b" 
3) "spring:session:expirations:1635413520000" 
6) "spring:session:sessions:d3434f61-4d0a-4687-9070-610bd7790f3b"
  • 1635412980000 是时间戳,等于 2021-10-28 17:23:00,即是该可以在这个时刻过期
  • springsession 定时(1分钟)轮询,删除spring:session:expirations:[?] 的过期成员元素,例如:spring:session:expirations:1635413520000
  • springsesion 定时检测超时的key的值,根据值删除seesion,例如key:spring:session:expirations:1635413520000,值为(sessionId):d3434f61-4d0a-4687-9070-610bd7790f3b的seesion

惰性删除

127.0.0.1:6379> type spring:session:sessions:expires:d3434f61-4d0a-4687-9070-610bd7790f3b
string
127.0.0.1:6379> get spring:session:sessions:expires:d3434f61-4d0a-4687-9070-610bd7790f3b
""
127.0.0.1:6379> ttl spring:session:sessions:expires:d3434f61-4d0a-4687-9070-610bd7790f3b
(integer) 3143
127.0.0.1:6379>
  • 访问 spring:session:sessions:expires:d3434f61-4d0a-4687-9070-610bd7790f3b的时候,判断key是否过期,过期则删除,否则返回改进的值。
  • 例如 访问spring:session:sessions:expires:d3434f61-4d0a-4687-9070-610bd7790f3b的时候,判断 ttl 是否过期,过期就直接删除
2) "spring:session:sessions:expires:d3434f61-4d0a-4687-9070-610bd7790f3b" 
3) "spring:session:expirations:1635413520000" 
6) "spring:session:sessions:d3434f61-4d0a-4687-9070-610bd7790f3b"

更多编程相关知识,请访问:编程视频!!

以上がRedis での分散セッションの不整合についてどうすればよいかの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はjuejin.cnで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。