当前位置:网站首页>Distributed session solution

Distributed session solution

2022-06-27 15:45:00 ·wangweijun

Consider a scenario , The background needs to verify whether the user logs in before placing an order , If you are not logged in, you are not allowed to submit an order , This is very easy to implement in traditional monomer applications , Just judge before submitting the order Session Whether the user information in is logged in , But in distributed applications , This is obviously a problem to be solved .

Under distributed application Session The problem is

In a distributed architecture , An application is often divided into several sub modules , such as : Login registration module and order module , When the application is split , Then comes the problem of data sharing :

Generally, in the login registration module, we save the login status of the user to Session in , However, when the user places an order , Since the order module is independent , It cannot get the stored in the login registration module Session, So the order module cannot judge whether the user logs in .

In order to ensure the high availability of the system , A module is often deployed in multiple copies to form a cluster , Data sharing between these modules is also a problem :

After the user successfully logs in a module , It is likely that the request will be load balanced to other cluster modules during the next access , This will result in failure to read Session, Make the user have to log in to the system again .

Session Case demonstration of shared problems

Here is a case to demonstrate , First create a SpringBoot application , Realize the login module :

@RestController
public class LoginController {

    @Autowired
    private ServiceOrderClient serviceOrderClient;

    @GetMapping("/login")
    public Result login(User user, HttpSession session) {
        String username = user.getUsername();
        String password = user.getPassword();
        Result result = new Result();
        if ("admin".equals(username) && "admin".equals(password)) {
            result.setCode(200);
            result.setMessage(" Login successful ");
            session.setAttribute("user", user);
        } else {
            result.setCode(-1);
            result.setMessage(" Login failed ");
        }
    }
}

Create another SpringBoot application , Implement the order module :

@RestController
public class OrderController {

    @GetMapping("/order/test")
    public String order(@CookieValue("JSESSIONID") String jSessionId) {
        return "success";
    }
}

The code is very simple , We mainly observe Session The problem of , Write the remote call interface in the login module :

@FeignClient("service-order")
public interface ServiceOrderClient {

    @GetMapping("/order/test")
    String order();
}

Register both applications to Nacos in , I will not post other codes , It's all pretty simple .

Start the two projects separately , And access http://localhost:8080/test , You will find that the visit was unsuccessful :

The result of the console output :

2021-09-21 16:51:43.155  WARN 20908 --- [nio-9000-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MissingRequestCookieException: Missing cookie 'JSESSIONID' for method parameter of type String]

Can't find the name JSESSIONID Of Cookie, We know , The server is through JSESSIONID To find the corresponding Session The information of , since JSESSIONID You can't get , Not to mention user information , This is it. Session The problem of not sharing .

Redis solve Session Sharing issues

For distributed applications Session problem , In fact, it's very simple , It's just that you can't share it Session, therefore , We can analogize the idea of caching , take Session Put in cache , Other services want to get Session Also get from the cache , That's it Session The share of .

Improve the login module :

@GetMapping("/login")
public Result login(User user, HttpSession session) {
    String username = user.getUsername();
    String password = user.getPassword();
    Result result = new Result();
    if ("admin".equals(username) && "admin".equals(password)) {
        result.setCode(200);
        result.setMessage(" Login successful ");
        String json = JSONObject.toJSONString(user);
        redisTemplate.opsForValue().set("session", json);
    } else {
        result.setCode(-1);
        result.setMessage(" Login failed ");
    }
    return result;
}

When we access the login interface http://localhost:8080/login?username=admin&password=admin when , Will go to Redis Keep a journal of Session Value :

At this time, if other services are required Session, As long as Redis It can be read from the , Modify the order module :

@RestController
public class OrderController {

    @GetMapping("/order/test")
    public String order() {
        return "success";
    }
}

Add a login interceptor in the order module :

public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //  Manual access StringRedisTemplate object 
        StringRedisTemplate redisTemplate = SpringBeanOperator.getBean(StringRedisTemplate.class);
        String json = redisTemplate.opsForValue().get("session");
        User user = JSONObject.parseObject(json, User.class);
        System.out.println(user);
        if (user == null) {
            System.out.println(" The user is not logged in ......");
            return false;
        } else {
            System.out.println(" User logged in ......");
            return true;
        }
    }
}

Register the interceptor :

@Configuration
public class MyWebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .addPathPatterns("/**");
    }
}

Restart project , visit http://localhost:8080/test , Output results :

User(username=admin, password=admin)
 User logged in ......

SpringSession solve Session Sharing issues

We used it ourselves just now Redis Tried to solve it Session The problem of sharing , However, this method has many defects , First , What we keep is just a User object , Not at all Session, So we can't identify the user , This will cause the user to access the information of other users , Make the system chaotic . Of course we can use JSESSIONID To identify different users , But in fact ,Spring A component has been provided for us to solve this problem , That's it SpringSession.

\

In both modules SpringSession Dependence :

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

stay application.yml Middle configuration Session The storage method of is Redis:

spring:  session:
    store-type: redis

Finally, add... To the startup class @EnableRedisHttpSession annotation , such SpringSession The integration of .

We modify the code of the login module :

@GetMapping("/login")
public Result login(User user, HttpSession session) {
    String username = user.getUsername();
    String password = user.getPassword();
    Result result = new Result();
    if ("admin".equals(username) && "admin".equals(password)) {
        result.setCode(200);
        result.setMessage(" Login successful ");
        session.setAttribute("user",user);
    } else {
        result.setCode(-1);
        result.setMessage(" Login failed ");
    }
    return result;
}

Follow the normal process to User Objects in Session, Restart the project and access the login interface , Let's see Redis What's the change in :

here Redis User information has been saved in , And there is also the creation time 、 Lifetime and other configurations , Other modules want to get Session User information in , You just need to write code according to the normal process :

public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HttpSession session = request.getSession();
        User user = (User) session.getAttribute("user");
        System.out.println(user);
        if (user == null) {
            System.out.println(" The user is not logged in ......");
            return false;
        } else {
            System.out.println(" User logged in ......");
            return true;
        }
    }
}

Note that the login module stores User Objects need to be read out with other modules User Object package names are consistent , So it's better to User Classes are extracted into common modules , Available to all modules .

Come here SpringSession It's settled. Session The problem of sharing , You can run the project to test , visit http://localhost:8080/test

The results were unexpected , The result of the console is :

null
 The user is not logged in ......

This is strange , Is it SpringSession Didn't work ? Let's write a test method to test :

@GetMapping("/test")
public String test(HttpSession session) {
    User user = (User) session.getAttribute("user");
    System.out.println(user);
    return "test";
}

visit http://localhost:9000/test , Get the results :

User(username=admin, password=admin)

obviously SpringSession There is no problem , So what's the problem ?

OpenFeign Remote call pit

Just now we had a test , Find direct access to... In the order module Session Can get User object , However, through remote calls ,User You can't get it , We can guess that this is OpenFeign There is a problem ,Debug Debug the project , This is the code for remote calls :

Let's go in and have a look :

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if ("equals".equals(method.getName())) {
        try {
            Object otherHandler =
                args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
            return equals(otherHandler);
        } catch (IllegalArgumentException e) {
            return false;
        }
    } else if ("hashCode".equals(method.getName())) {
        return hashCode();
    } else if ("toString".equals(method.getName())) {
        return toString();
    }

    return dispatch.get(method).invoke(args);
}

Some judgments are made in this method , Will eventually call dispatch.get() Method :

@Override
public Object invoke(Object[] argv) throws Throwable {
    RequestTemplate template = buildTemplateFromArgs.create(argv);
    Options options = findOptions(argv);
    Retryer retryer = this.retryer.clone();
    while (true) {
        try {
            return executeAndDecode(template, options);
        } catch (RetryableException e) {
            try {
                retryer.continueOrPropagate(e);
            } catch (RetryableException th) {
                Throwable cause = th.getCause();
                if (propagationPolicy == UNWRAP && cause != null) {
                    throw cause;
                } else {
                    throw th;
                }
            }
            if (logLevel != Logger.Level.NONE) {
                logger.logRetry(metadata.configKey(), logLevel);
            }
            continue;
        }
    }
}

This method will call executeAndDecode():

This method encapsulates a request template as the target request for remote invocation , However, we observe that there are no parameters or request headers in the request template , And we know that ,Session Depend on JSESSIONID For identification , stay SpringSession in ,Session Depend on SESSIONID Identification of the :

From this we come to the conclusion that , because OpenFeign The remote call lost the request header , Lead to SESSIONID The loss of , As a result, the order module cannot obtain User object . After knowing the problem , The solution is very simple , We can create a request filter , It will process the request before the request template is generated :

@Configuration
public class MyFeignConfig {

    @Bean
    public RequestInterceptor requestInterceptor() {
        return requestTemplate -> {
            System.out.println(" Call this method before remote call -->requestInterceptor......");
            ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            HttpServletRequest request = requestAttributes.getRequest();
            String cookie = request.getHeader("Cookie");
            requestTemplate.header("Cookie", cookie);
        };
    }
}

The original Request Object Cookie Set the request header information to the request template , such OpenFeign The created request has Cookie Content , Restart the project test , The problem is solved with ease .

原网站

版权声明
本文为[·wangweijun]所创,转载请带上原文链接,感谢
https://yzsam.com/2022/178/202206271524329804.html