Distributed session inconsistency solution

“This is the 11th day of my participation in the Gwen Challenge in November. See details: The Last Gwen Challenge in 2021”

What does Session do?

  • Session is a communication Session tracking technology between the client and the server. The server and the client maintain the basic information of the entire communication Session.
  • When the client accesses the server for the first time, the server responds with a sessionId and stores it in the local cookie. In subsequent accesses, the sessionId in the cookie is put in the request header to access the server.
  • If no data is found using the sessionId, the server creates a new sessionId and responds to the client.

What are the problems with distributed sessions?

In single-server Web applications, session information only needs to be stored on that server, which was the most common approach we encountered a few years ago

However, with the popularity of distributed system in recent years, single system can not meet the needs of millions of users, cluster deployment server has been used in many companies

When the requests with high concurrency arrive at the server, they are distributed to a server in the cluster through load balancing. In this way, multiple requests from the same user may be distributed to different servers in the cluster, and session data cannot be obtained. Therefore, session sharing becomes a problem.

Graph of TD client - > nginx nginx - > server have the session nginx - > | | request server session

Three, services do cluster is generally done?

  • SpringBoot project, so just change the port number to start a few, and then use Nginx unified reverse proxy.
  • The SpringCloud microservices project, at this time, can use the ribbon local load balancing.

Nginx load Balancing and ribbon load balancing

  • Nginx does load balancing on the server side. It accesses the same address uniformly and decides which server to access according to the load balancing algorithm.
  • Ribbon Load Balancer. This is a local load balancer (client load balancer). The Ribbon caches the addresses of clients that provide services and implements load balancing based on local algorithms.

5. Session consistency solution

1. Session replication (synchronization)

Graph LR client - > nginx nginx nginx > server 1 -- - > server 2 server 2 -- - > | | synchronization server server 1 - > | | synchronization server 2

Multiple servers synchronize sessions with each other, so that each server contains all sessions

Advantages: Functionality supported by the server without code modification

Disadvantages:

  • Session synchronization requires data transmission, occupies Intranet bandwidth, and has latency
  • All servers contain all session data. The amount of data is limited by memory and cannot be horizontally expanded

2. Client storage

Graph LR client save seesion --> nginx nginx --> server 1 nginx --> server 2

The server saves all users’ sessions in the cookie of the browser. Each server only needs to store one user’s data

Advantages: The server does not need storage

Disadvantages:

  • Each HTTP request carries a session and occupies the bandwidth of the Internet
  • Data is stored on the host and transmitted over the network, causing security risks such as leakage, tampering, and theft
  • The size of the data stored in the session and the number of domain cookies are limited

Note: although this scheme is not commonly used, it is indeed a way of thinking.

3. Reverse proxy hash consistency

Can the reverse proxy layer do something to ensure that requests from the same user are placed on the same server?

Scheme 1: Four-layer hash proxy

Graph LR client - > | 127.0.0.1 | nginx (nginx ip_hsah) nginx - > | | request 1 nginx server - > server 2

The reverse proxy layer hashes the user’s IP address to ensure that requests from the same IP address are sent to the same server

Scheme 2: Layer 7 hash proxy

Graph LR client - > | sid = 12345 | nginx (nginx session_id hsah) nginx - > | | request 1 nginx server - > server 2

The reverse proxy uses certain service attributes in THE HTTP protocol to hash, such as SID, city_id, and user_id. In this way, the reverse proxy can flexibly implement hash policies to ensure that requests from users in the same browser are sent to the same server

Advantages:

  • You only need to change the nginx configuration, not the application code
  • Load balancing: As long as the hash attribute is uniform, the load on multiple servers is balanced
  • Can support server level scaling (session synchronization method is not available, limited by memory)

Disadvantages:

  • If the server restarts, some sessions are lost, which affects services. For example, some users log in again
  • If the server extends horizontally and the session is redistributed after rehash, some users may not be routed to the correct session

Sessions generally have a validity period. Two of these deficiencies can be regarded as the failure of some sessions, which is not a big problem.

I recommend the four-tier hash versus the seven-tier hash: let professional software do professional things and the reverse proxy takes care of forwarding. Try not to introduce application-layer business attributes unless you have to (for example, sometimes multiple machines with multiple lives need to be routed to servers in different machines based on business attributes).

Difference between layer 4 and Layer 7 load balancing

4. Back-end centralized storage

Graph TD subgraph DB client 1(client)-->nginx1(nginx) --> server 1 Nginx1 (nginx) --> Server 2 Server 1--> DB1 (DB seesion) server 2 -->db1(db seesion) end subgraph Redis Client 2(client)-->nginx2(nginx) --> Server 3 Nginx2 (nginx) --> Server 4 Server 4 --> DB2 (Redis Seesion) server 3 --> DB2 (Redis Seesion) end

Store sessions in the server storage layer, database or cache

Advantages:

  • No safety risks
  • Can scale horizontally, database/cache horizontal shard
  • Sessions will not be lost during server restart or expansion

Disadvantages: Added a network call and required application code changes

As for DB storage or cache, I recommend the latter: session reading frequency is high, and database pressure is high. If session high availability is required, the cache can implement high availability. However, sessions can be lost in most cases and high availability is not considered.

conclusion

Common architectural design methods to ensure session consistency:

  • Session synchronization: Data is synchronized between multiple servers
  • Client-side storage a user stores only his or her own data
  • Reverse proxy hash Consistency Both 4-layer hash and 7-layer hash can be implemented to ensure that a user’s request is sent to the same server
  • Back-end unified storageServer restart and capacity expansion, session is not lost (Unified back-end cache storage is recommended)

Case: SpringSession+ Redis to solve the problem of distributed session inconsistency

Step 1: Add the dependency packages of SpringSession and 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>
Copy the code

Step 2: Configuration files

Set logging for a package directory
logging.level.com.ljw=debug

Set session storage mode, use Redis storage
spring.session.store-type=redis
The session duration is 10 minutes
server.servlet.session.timeout=PT10M

# # Redis configuration
## Redis database index (default 0)
spring.redis.database=0
# Redis server address
Spring. Redis. Host = 127.0.0.1
Redis server connection port
spring.redis.port=6379
## Redis server connection password (default null)
spring.redis.password=
Copy the code

Step 3: Configure interceptors

@Configuration
public class SessionConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new SecurityInterceptor())
                // Exclude two blocked paths
                .excludePathPatterns("/user/login")
                .excludePathPatterns("/user/logout")
                // Intercept all URL paths
                .addPathPatterns("/ * *"); }}Copy the code
@Configuration
public class SecurityInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
        HttpSession session = request.getSession();
        // Check whether the current session exists. True is returned if the current session exists. True indicates that service logic can be processed normally
        if(session.getAttribute(session.getId()) ! =null){
            log.info("Session interceptor, session={}, authenticated",session.getId());
            return true;
        }
        // Session does not exist, returns false and prompts you to log in again.
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        response.getWriter().write("Go to !!!!!");
        log.info("Session interceptor, session={}, authentication failed",session.getId());
        return false; }}Copy the code
  • HandlerInterceptor
    • PreHandle: Called before the request is processed by the business processor. Preprocessing, coding, security control, authority check and other processing;
    • PostHandle: Executed after the business processor completes the execution of the request and before the view is generated. Post-processing (the Service is called and ModelAndView is returned, but the page is not rendered) gives you an opportunity to modify the ModelAndView
    • AfterCompletion: Invoked after the DispatcherServlet has fully processed the request and can be used to clean up resources, etc. Return to processing (page already rendered)

Step 4: Controller

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

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

    public UserController(a) {
        // Initialize two users to simulate login
        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) {
        // simulate database lookup
        User user = this.userMap.get(username);
        if(user ! =null) {
            if(! password.equals(user.getPassword())) {return "Wrong username or password!!";
            } else {
                session.setAttribute(session.getId(), user);
                log.info("Login successful {}",user); }}else {
            return "Wrong username or password!!";
        }
        return "Login successful!!";
    }

    /** * Find user */ by user name
    @GetMapping(value = "/find/{username}")
    public User find(@PathVariable String username) {
        User user=this.userMap.get(username);
        log.info("Find user {} by username ={}",username,user);
        return user;
    }

    /** * take the current user's session */
    @GetMapping(value = "/session")
    public String session(HttpSession session) {
        log.info("Current user session={}",session.getId());
        return session.getId();
    }

    /** * Log out */
    @GetMapping(value = "/logout")
    public String logout(HttpSession session) {
        log.info("Exit login session={}",session.getId());
        session.removeAttribute(session.getId());
        return "Successful exit!!"; }}Copy the code

Step 5: Entity classes

@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; }}Copy the code

Step 6: Access tests

First login: http://127.0.0.1:8080/user/login? username=user1&password=user1

Query again http://127.0.0.1:8080/user/find/user1

Analyze the Redis principle of SpringSession

Step 1: Analyze the Redis data structure of SpringSession

127.0. 01.: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"
Copy the code

Common: All three keys start with Spring: Session: and represent redis data for SpringSession.

Types of queries

127.0. 01.:6379> type spring:session:sessions:d3434f61-4d0a-4687-9070-610bd7790f3b
hash
Copy the code
127.0. 01.:6379> hgetall spring:session:sessions:d3434f61-4d0a-4687-9070-610bd7790f3b
// The session creation time
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\x0 0xp\x00\x00\x01|\xc5\xdb\xecu"
// Sesson property, which stores the user object
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"
// Last access time
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\x0 0xp\x00\x00\x01|\xc5\xe1\xc7\xed"
// The failure time is 100 minutes
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.N umber\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x17p"
Copy the code

Step 2: Analyze the Redis expiration policy of SpringSession

There are generally three deletion strategies for expired data:

  1. Time to deleteThat is, you can create a timer when setting the expiration time of a key. When the expiration time of a key arrives, the timer will be deleted immediately.
  2. Lazy to delete“Is used to check whether the key is expired. If the key expires, it is deleted. Otherwise, the key value is returned.
  3. Periodically deleteIn other words, every once in a while, the program checks the database and removes expired keys. It is up to the algorithm to decide how many expired keys to delete and how many databases to check.
  • redisThe expired data is deletedLazy delete + delete regularlyCombinatorial policy, where data expires and is not deleted in time.
  • However, because Redis is single-threaded, and Redis has a low priority to delete expired keys; If there are a large number of expired keys, the key is expired but not deleted.
  • In order to achieve timeliness of session expiration,spring session Using theScheduled deletion + Lazy deletionThe strategy.

Time to delete

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

2) "spring:session:sessions:expires:d3434f61-4d0a-4687-9070-610bd7790f3b" 
3) "spring:session:expirations:1635413520000" 
6) "spring:session:sessions:d3434f61-4d0a-4687-9070-610bd7790f3b"
Copy the code
  • 1635412980000 is the time stamp, equal to 2021-10-28 17:23:00, that is, it can expire at this time
  • Springsession (1 minute) regularly polling, remove the spring: session: expirations: [?] Members expired elements, such as: spring: session: expirations: 1635413520000
  • Springsesion timing detection timeout of key values, according to the value delete seesion, such as key: spring: session: expirations: 1635413520000, a value of (sessionId) : D0a d3434f61-4-4687-9070-610 bd7790f3b seesion

Lazy to delete

127.0. 01.:6379> type spring:session:sessions:expires:d3434f61-4d0a-4687-9070-610bd7790f3b
string
127.0. 01.:6379> get spring:session:sessions:expires:d3434f61-4d0a-4687-9070-610bd7790f3b
""
127.0. 01.:6379> ttl spring:session:sessions:expires:d3434f61-4d0a-4687-9070-610bd7790f3b
(integer) 3143
127.0. 01.:6379>
Copy the code
  • Access spring: session: sessions: expires: d0a d3434f61-4-4687-9070-610 bd7790f3b, judge whether the key expired, expired, deleted, or return the value of improvement.
  • Such as access to the spring: session: sessions: expires: d0a d3434f61-4-4687-9070-610 bd7790f3b, whether TTL expired, overdue will delete directly
2) "spring:session:sessions:expires:d3434f61-4d0a-4687-9070-610bd7790f3b" 
3) "spring:session:expirations:1635413520000" 
6) "spring:session:sessions:d3434f61-4d0a-4687-9070-610bd7790f3b"
Copy the code

Redis distributed cache family

  • Redis Distributed Cache (1) – Redis Installation (Linux and Docker)
  • Redis Distributed cache (ii) – RDB and AOF
  • SpringBoot integrates Mybatis-Plus,Redis and Swagger
  • Redis distributed cache (4) – SpringCache integrates redis
  • Redis Distributed Cache (5) — Common command (String)
  • Redis Distributed Cache (6) — Article read volume PV Solution (String)
  • Redis Distributed Cache (7) — Distributed Global ID Solution (String)
  • Redis Distributed Cache (8) — Highly Concurrent atomic Operations (Redis+Lua)
  • Redis Distributed Cache (9) — Hacker Anti-brush Attack Solution (String)
  • Redis Distributed Cache (10) common Commands (Hash)
  • Redis Distributed Cache (11) store Java Bean object Hash
  • Redis Distributed Cache (12) – Short Link Solution (Hash)
  • Redis Distributed Cache (13) — JD Shopping cart solution
  • The article continues to be updated…