• 高并发项目-分布式Session解决方案


    分布式Session解决方案

    1.保存Session,进入商品列表页面

    1.保存Session
    1.编写工具类
    1.MD5Util.java
    package com.sxs.seckill.utils;
    
    import org.apache.commons.codec.digest.DigestUtils;
    
    /**
     * Description: MD5加密工具类
     *
     * @Author sun
     * @Create 2024/5/5 14:23
     * @Version 1.0
     */
    public class MD5Util {
        /**
         * 将一个字符串转换为MD5
         * @param src
         * @return
         */
        public static String md5(String src) {
            return DigestUtils.md5Hex(src);
        }
    
        // 固定的salt
        public static final String SALT = "4tIY5VcX";
        // 第一次加密加盐
        public static String inputPassToMidPass(String inputPass) {
            // 加盐
            String str = SALT.charAt(0) + inputPass + SALT.charAt(6);
            return md5(str);
        }
    
        /**
         * 第二次加密加盐
         * @param midPass
         * @param salt
         * @return
         */
        public static String midPassToDBPass(String midPass, String salt) {
            String str = salt.charAt(0) + midPass + salt.charAt(5);
            return md5(str);
        }
    
        /**
         * 两次加密
         * @param input
         * @param saltDB
         * @return
         */
        public static String inputPassToDBPass(String input, String saltDB) {
            String midPass = inputPassToMidPass(input);
            String dbPass = midPassToDBPass(midPass, saltDB);
            return dbPass;
        }
    }
    
    
    2.CookieUtil.java
    package com.sxs.seckill.utils;
    
    import javax.servlet.http.Cookie;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.UnsupportedEncodingException;
    import java.net.URLDecoder;
    import java.net.URLEncoder;
    
    /**
     * Description: Cookie工具类
     *
     * @Author sun
     * @Create 2024/5/6 16:04
     * @Version 1.0
     */
    
    
    public class CookieUtil {
        /**
         * 得到 Cookie 的值, 不编码
         *
         * @param request
         * @param cookieName
         * @return
         */
        public static String getCookieValue(HttpServletRequest request, String
                cookieName) {
            return getCookieValue(request, cookieName, false);
        }
    
        /**
         * 得到 Cookie 的值, *
         *
         * @param request
         * @param cookieName
         * @return
         */
        public static String getCookieValue(HttpServletRequest request, String
                cookieName, boolean isDecoder) {
            Cookie[] cookieList = request.getCookies();
            if (cookieList == null || cookieName == null) {
                return null;
            }
            String retValue = null;
            try {
                for (int i = 0; i < cookieList.length; i++) {
                    if (cookieList[i].getName().equals(cookieName)) {
                        if (isDecoder) {
                            retValue = URLDecoder.decode(cookieList[i].getValue(), "UTF-8");
                        } else {
                            retValue = cookieList[i].getValue();
                        }
                        break;
                    }
                }
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
            return retValue;
        }
    
        /**
         * 得到 Cookie 的值, *
         *
         * @param request
         * @param cookieName
         * @param encodeString
         * @return
         */
        public static String getCookieValue(HttpServletRequest request, String
                cookieName, String encodeString) {
            Cookie[] cookieList = request.getCookies();
            if (cookieList == null || cookieName == null) {
                return null;
            }
            String retValue = null;
            try {
                for (int i = 0; i < cookieList.length; i++) {
                    if (cookieList[i].getName().equals(cookieName)) {
                        retValue = URLDecoder.decode(cookieList[i].getValue(), encodeString);
                        break;
                    }
                }
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
            return retValue;
        }
    
        /**
         * 设置 Cookie 的值 不设置生效时间默认浏览器关闭即失效,也不编码
         */
        public static void setCookie(HttpServletRequest request, HttpServletResponse
                response, String cookieName, String cookieValue) {
            setCookie(request, response, cookieName, cookieValue, -1);
        }
    
        /**
         * 设置 Cookie 的值 在指定时间内生效,但不编码
         */
        public static void setCookie(HttpServletRequest request, HttpServletResponse
                response, String cookieName, String cookieValue, int cookieMaxage) {
            setCookie(request, response, cookieName, cookieValue, cookieMaxage,
                    false);
        }
    
        /**
         * 设置 Cookie 的值 不设置生效时间,但编码
         */
        public static void setCookie(HttpServletRequest request, HttpServletResponse
                response, String cookieName, String cookieValue, boolean isEncode) {
            setCookie(request, response, cookieName, cookieValue, -1, isEncode);
        }
    
        /**
         * 设置 Cookie 的值 在指定时间内生效, 编码参数
         */
        public static void setCookie(HttpServletRequest request, HttpServletResponse
                response, String cookieName, String cookieValue, int cookieMaxage, boolean
                                             isEncode) {
            doSetCookie(request, response, cookieName, cookieValue, cookieMaxage,
                    isEncode);
        }
    
        /**
         * 设置 Cookie 的值 在指定时间内生效, 编码参数(指定编码)
         */
        public static void setCookie(HttpServletRequest request, HttpServletResponse
                response, String cookieName, String cookieValue, int cookieMaxage, String
                                             encodeString) {
            doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, encodeString);
        }
    
        /**
         * 删除 Cookie 带 cookie 域名
         */
        public static void deleteCookie(HttpServletRequest request, HttpServletResponse response, String cookieName) {
            doSetCookie(request, response, cookieName, "", -1, false);
        }
    
        /**
         * 设置 Cookie 的值,并使其在指定时间内生效
         *
         * @param cookieMaxage cookie 生效的最大秒数
         */
        private static final void doSetCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue,
                                              int cookieMaxage, boolean isEncode) {
            try {
                if (cookieValue == null) {
                    cookieValue = "";
                } else if (isEncode) {
                    cookieValue = URLEncoder.encode(cookieValue, "utf-8");
                }
                Cookie cookie = new Cookie(cookieName, cookieValue);
                if (cookieMaxage > 0) {
                    cookie.setMaxAge(cookieMaxage);
                }
                // if (null != request) {// 设置域名的 cookie
                // String domainName = getDomainName(request);
                // if (!"localhost".equals(domainName)) {
                // cookie.setDomain(domainName);
                // }
                // }
                cookie.setPath("/");
                response.addCookie(cookie);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        /**
         * 设置 Cookie 的值,并使其在指定时间内生效
         *
         * @param cookieMaxage cookie 生效的最大秒数
         */
        private static final void doSetCookie(HttpServletRequest request,
                                              HttpServletResponse response, String cookieName, String cookieValue,
                                              int cookieMaxage, String encodeString) {
            try {
                if (cookieValue == null) {
                    cookieValue = "";
                } else {
                    cookieValue = URLEncoder.encode(cookieValue, encodeString);
                }
                Cookie cookie = new Cookie(cookieName, cookieValue);
                if (cookieMaxage > 0) {
                    cookie.setMaxAge(cookieMaxage);
                }
                if (null != request) {// 设置域名的 cookie
                    String domainName = getDomainName(request);
                    System.out.println(domainName);
                    if (!"localhost".equals(domainName)) {
                        cookie.setDomain(domainName);
                    }
                }
                cookie.setPath("/");
                response.addCookie(cookie);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        /**
         * 得到 cookie 的域名
         */
        private static final String getDomainName(HttpServletRequest request) {
            String domainName = null;
            // 通过 request 对象获取访问的 url 地址
            String serverName = request.getRequestURL().toString();
            if ("".equals(serverName)) {
                domainName = "";
            } else {
                // 将 url 地下转换为小写
                serverName = serverName.toLowerCase();
                // 如果 url 地址是以 http://开头 将 http://截取
                if (serverName.startsWith("http://")) {
                    serverName = serverName.substring(7);
                }
                int end = serverName.length();
                // 判断 url 地址是否包含"/"
                if (serverName.contains("/")) {
                    // 得到第一个"/"出现的位置
                    end = serverName.indexOf("/");
                }
                // 截取
                serverName = serverName.substring(0, end);
                // 根据"."进行分割
                final String[] domains = serverName.split("\\.");
                int len = domains.length;
                if (len > 3) {
                    // www.abc.com.cn
                    domainName = domains[len - 3] + "." + domains[len - 2] + "." +
                            domains[len - 1];
                } else if (len > 1) {
                    // abc.com or abc.cn
                    domainName = domains[len - 2] + "." + domains[len - 1];
                } else {
                    domainName = serverName;
                }
            }
            if (domainName.indexOf(":") > 0) {
                String[] ary = domainName.split("\\:");
                domainName = ary[0];
            }
            return domainName;
        }
    }
    
    
    2.关于session和cookie关系的回顾
    • 当浏览器请求到服务端时cookie会携带sessionid
    • 然后在服务端getSession时会得到当前用户的session
    • cookie-sessionid 连接到session
    3.修改UserServiceImpl.java的doLogin方法,增加保存信息到session的逻辑

    image-20240506180654918

    4.测试,用户票据成功保存到cookie中

    image-20240506164400785

    2.访问到商品列表页面
    1.编写GoodsController.java 验证用户登录后进入商品列表页
    package com.sxs.seckill.controller;
    
    import com.sxs.seckill.pojo.User;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.CookieValue;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    import javax.servlet.http.HttpSession;
    
    /**
     * Description:
     *
     * @Author sun
     * @Create 2024/5/6 18:16
     * @Version 1.0
     */
    @Controller
    @Slf4j
    @RequestMapping("/goods")
    public class GoodsController {
        // 进入到商品首页
        @RequestMapping("/toList")
        public String toList(HttpSession session, Model model, @CookieValue("userTicket") String ticket) {
            // 首先判断是否有票据
            if (null == ticket) {
                return "login";
            }
            // 根据票据来获取用户信息
            User user = (User) session.getAttribute(ticket);
            if (null == user) {
                return "login";
            }
            // 将用户信息存入model中,返回到前端
            model.addAttribute("user", user);
            return "goodsList";
        }
    }
    
    
    2.商品列表页goodsList.html
    DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>商品列表title>
    head>
    <body>
    <h1>商品列表h1>
    <p th:text="'hi: ' + ${user.nickname}">p>
    body>
    html>
    
    3.测试登录成功后进入商品列表页

    image-20240506182533204

    image-20240506182619668

    2.分布式session解决方案

    1.session绑定/粘滞(不常用)

    image-20240507084426254

    image-20240507084410603

    2.session复制

    image-20240507084606158

    image-20240507084657719

    3.前端存储

    image-20240507084727017

    4.后端集中存储

    image-20240507084749944

    3.方案一:SpringSession实现分布式Session

    1.安装使用redis-desktop-manager
    1.一直下一步,安装到D盘

    image-20240507085744926

    2.首先要确保redis集群的端口是开放的并使其支持远程访问(之前配置过)
    3.使用telnet指令测试某个服务是否能够连接成功
    telnet 140.143.164.206 7489
    

    image-20240507090655656

    4.连接Redis,先测试连接然后确定

    image-20240507091103602

    5.在redis命令行设置两个键

    image-20240507091252851

    6.在可视化工具查看

    image-20240507091332860

    2.项目整合Redis并配置分布式session
    1.pom.xml引入依赖
            
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-data-redisartifactId>
                <version>2.4.5version>
            dependency>
            
            <dependency>
                <groupId>org.apache.commonsgroupId>
                <artifactId>commons-pool2artifactId>
                <version>2.9.0version>
            dependency>
            
            <dependency>
                <groupId>org.springframework.sessiongroupId>
                <artifactId>spring-session-data-redisartifactId>
            dependency>
    
    2.application.yml配置Redis
    spring:
      redis:
        password:  # Redis服务器密码
        database: 0 # 默认数据库为0号
        timeout: 10000ms # 连接超时时间是10000毫秒
        lettuce:
          pool:
            max-active: 8 # 最大活跃连接数,使用负值表示没有限制,最佳配置为核数*2
            max-wait: 10000ms # 最大等待时间,单位为毫秒,使用负值表示没有限制,这里设置为10秒
            max-idle: 200 # 最大空闲连接数
            min-idle: 5 # 最小空闲连接数
        cluster:
          nodes:
            - 
            - 
    

    image-20240507094324857

    3.启动测试
    1.登录

    image-20240507094643899

    2.Redis可视化工具发现session成功存到redis

    image-20240507094628773

    image-20240507095339215

    4.方案二:统一存放用户信息到Redis

    1.修改pom.xml,去掉分布式springsession的依赖

    image-20240507103646670

    2.将用户信息放到Redis
    1.添加Redis配置类 com/sxs/seckill/config/RedisConfig.java
    package com.sxs.seckill.config;
    
    import com.fasterxml.jackson.annotation.JsonAutoDetect;
    import com.fasterxml.jackson.annotation.JsonTypeInfo;
    import com.fasterxml.jackson.annotation.PropertyAccessor;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
    import org.springframework.cache.CacheManager;
    import org.springframework.cache.annotation.CachingConfigurerSupport;
    import org.springframework.cache.annotation.EnableCaching;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.cache.RedisCacheConfiguration;
    import org.springframework.data.redis.cache.RedisCacheManager;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
    import org.springframework.data.redis.serializer.RedisSerializationContext;
    import org.springframework.data.redis.serializer.RedisSerializer;
    import org.springframework.data.redis.serializer.StringRedisSerializer;
    
    import java.time.Duration;
    
    /**
     * Description:
     *
     * @Author sun
     * @Create 2024/4/29 21:29
     * @Version 1.0
     */
    @EnableCaching
    @Configuration
    public class RedisConfig extends CachingConfigurerSupport {
        @Bean
        public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
            RedisTemplate<String, Object> template =
                    new RedisTemplate<>();
            System.out.println("template=>" + template);
            RedisSerializer<String> redisSerializer =
                    new StringRedisSerializer();
            Jackson2JsonRedisSerializer jackson2JsonRedisSerializer =
                    new Jackson2JsonRedisSerializer(Object.class);
            ObjectMapper om = new ObjectMapper();
            om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
            om.activateDefaultTyping(
                    LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);
            jackson2JsonRedisSerializer.setObjectMapper(om);
            template.setConnectionFactory(factory);
            // key 序列化方式
            template.setKeySerializer(redisSerializer);
            // value 序列化
            template.setValueSerializer(jackson2JsonRedisSerializer);
            // value hashmap 序列化
            template.setHashValueSerializer(jackson2JsonRedisSerializer);
            return template;
        }
    
        @Bean
        public CacheManager cacheManager(RedisConnectionFactory factory) {
            RedisSerializer<String> redisSerializer =
                    new StringRedisSerializer();
            Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new
                    Jackson2JsonRedisSerializer(Object.class);
            // 解决查询缓存转换异常的问题
            ObjectMapper om = new ObjectMapper();
            om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
            om.activateDefaultTyping(
                    LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);
            jackson2JsonRedisSerializer.setObjectMapper(om);
            // 配置序列化(解决乱码的问题),过期时间 600 秒
            RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                    .entryTtl(Duration.ofSeconds(600))
                    .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                    .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                    .disableCachingNullValues();
            RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                    .cacheDefaults(config)
                    .build();
            return cacheManager;
        }
    }
    
    2.修改 com/sxs/seckill/service/impl/UserServiceImpl.java
    1.注入RedisTemplate

    image-20240507104718538

    2.修改doLogin方法,将用户信息放到Redis中

    image-20240507104748489

    3.启动测试
    1.登录

    image-20240507105110074

    2.可视化工具查看用户信息

    image-20240507105302615

    3.实现使用Redis + Cookie实现登录,可以访问商品列表页面
    1.刚才已经实现了Redis记录信息的功能,但是校验还没实现,修改GoodsController.java完成校验
    1.注入RedisTemplate

    image-20240507110303645

    2.从Redis中获取校验信息,进行校验

    image-20240507110342489

    2.测试
    1.登录成功后访问商品列表页面

    image-20240507110534647

    5.扩展:自定义参数解析器,直接获取User

    1.修改 com/sxs/seckill/controller/GoodsController.java 使参数直接为User
        // 进入到商品首页
        @RequestMapping("/toList")
        public String toList(Model model, User user) {
            // 判断是否有用户信息
            if (null == user) {
                return "login";
            }
            // 将用户信息存入model中,返回到前端
            model.addAttribute("user", user);
            return "goodsList";
        }
    
    
    2.service层添加方法,通过票据从Redis中获取User对象
    1.UserService.java
        /**
         * 根据cookie获取用户
         * @param userTicket
         * @param request
         * @param response
         * @return
         */
        public User getUserByCookie(String userTicket, HttpServletRequest request, HttpServletResponse response);
    
    
    2.UserServiceImpl.java
    • 这里需要注意,每次获取完User,需要重新设置Cookie,来刷新Cookie的时间
    • 原因是,调用这个的目的是为了校验,而用户访问每个页面都要进行校验,如果每次校验之后都不刷新Cookie的时间,一旦Cookie失效了,用户就要重新登陆
        @Override
        public User getUserByCookie(String userTicket, HttpServletRequest request, HttpServletResponse response) {
            // 判断是否有票据
            if (null == userTicket) {
                return null;
            }
            // 根据票据来获取用户信息,从redis中获取
            User user = (User) redisTemplate.opsForValue().get("user:" + userTicket);
            if (null == user) {
                return null;
            }
            // 重新设置cookie的有效时间
            CookieUtil.setCookie(request, response, "userTicket", userTicket);
            return user;
        }
    
    
    3.编写自定义参数解析器对User类型参数进行解析 config/UserArgumentResolver.java
    package com.sxs.seckill.config;
    
    import com.sxs.seckill.pojo.User;
    import com.sxs.seckill.service.UserService;
    import com.sxs.seckill.utils.CookieUtil;
    import org.springframework.core.MethodParameter;
    import org.springframework.stereotype.Component;
    import org.springframework.web.bind.support.WebDataBinderFactory;
    import org.springframework.web.context.request.NativeWebRequest;
    import org.springframework.web.method.support.HandlerMethodArgumentResolver;
    import org.springframework.web.method.support.ModelAndViewContainer;
    
    import javax.annotation.Resource;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    /**
     * Description: 自定义参数解析器
     *
     * @Author sun
     * @Create 2024/5/7 14:39
     * @Version 1.0
     */
    @Component
    public class UserArgumentResolver implements HandlerMethodArgumentResolver {
        @Resource
        private UserService userService;
        /*
         * 判断是否支持要转换的参数类型,简单来说,就是在这里设置要解析的参数类型
         */
        @Override
        public boolean supportsParameter(MethodParameter methodParameter) {
            // 如果参数类型是User,则进行解析
            Class<?> parameterType = methodParameter.getParameterType();
            if (parameterType == User.class) {
                return true;
            }
            return false;
        }
    
        /**
         * 编写参数解析的逻辑
         *
         * @param methodParameter
         * @param modelAndViewContainer
         * @param nativeWebRequest
         * @param webDataBinderFactory
         * @return
         * @throws Exception
         */
        @Override
        public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
            // 首先获取request和response
            HttpServletRequest request = nativeWebRequest.getNativeRequest(HttpServletRequest.class);
            HttpServletResponse response = nativeWebRequest.getNativeResponse(HttpServletResponse.class);
            // 从cookie中获取票据
            String userTicket = CookieUtil.getCookieValue(request, "userTicket");
            // 如果票据为空,则返回null
            if (null == userTicket) {
                return null;
            }
            // 如果票据不为空,则根据票据获取用户信息
            User user = this.userService.getUserByCookie(userTicket, request, response);
            return user;
        }
    }
    
    
    4.编写config/WebConfig.java 将自定义参数解析器放到 resolvers 才能生效
    package com.sxs.seckill.config;
    
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.method.support.HandlerMethodArgumentResolver;
    import org.springframework.web.servlet.config.annotation.EnableWebMvc;
    import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    import javax.annotation.Resource;
    import java.util.List;
    
    /**
     * Description:
     *
     * @Author sun
     * @Create 2024/5/7 14:53
     * @Version 1.0
     */
    @Configuration
    // @EnableWebMvc 使用这个注解会导致SpringBoot的自动配置失效,一切都要自己配置,所以建议不要使用这个注解
    public class WebConfig implements WebMvcConfigurer {
        // 注入自定义参数解析器
        @Resource
        private UserArgumentResolver userArgumentResolver;
        /**
         * 静态资源加载,静态资源放在哪里就怎么配置
         * @param registry
         */
        @Override
        public void addResourceHandlers(ResourceHandlerRegistry registry) {
            registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
        }
    
        /**
         * 添加自定义参数解析器到resolvers中,才能生效
         * @param resolvers
         */
        @Override
        public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
            resolvers.add(userArgumentResolver);
        }
    }
    
    
    5.测试
    1.在登录之后,可以正常访问商品列表页面

    image-20240507151255032

  • 相关阅读:
    【Linux】第三篇——Linux环境下的工具(一)(yum + vim + gcc/g++ +gdb)
    云可观测性:提升云环境中应用程序可靠性
    照片生成数字人解决方案
    Java进阶(8)——抽象类与接口
    Success! kcat is now built
    数据结构:平衡二叉树
    【SwiftUI项目】0009、SwiftUI项目-费用跟踪-记账App项项目-第1/3部分 - 本地数据
    一文带你学会Linux vi/vim
    如何用 Tana AI 一站式批量润色整理音频笔记?
    Spring AOP基础之代理模式.静态代理和动态代理
  • 原文地址:https://blog.csdn.net/m0_64637029/article/details/139361945