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

分布式Session解决方案

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

1.保存Session
1.编写工具类
1.MD5Util.java
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
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的逻辑
4.测试,用户票据成功保存到cookie中
2.访问到商品列表页面
1.编写GoodsController.java 验证用户登录后进入商品列表页
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
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.测试登录成功后进入商品列表页

2.分布式session解决方案

1.session绑定/粘滞(不常用)
2.session复制
3.前端存储
4.后端集中存储

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

1.安装使用redis-desktop-manager
1.一直下一步,安装到D盘
2.首先要确保redis集群的端口是开放的并使其支持远程访问(之前配置过)
3.使用telnet指令测试某个服务是否能够连接成功
sh 复制代码
telnet 140.143.164.206 7489
4.连接Redis,先测试连接然后确定
5.在redis命令行设置两个键
6.在可视化工具查看
2.项目整合Redis并配置分布式session
1.pom.xml引入依赖
xml 复制代码
        <!--spring data redis 依赖, 即 spring 整合 redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <version>2.4.5</version>
        </dependency>
        <!--pool2 对象池依赖-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
            <version>2.9.0</version>
        </dependency>
        <!--实现分布式 session, 即将 Session 保存到指定的 Redis-->
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>
2.application.yml配置Redis
yaml 复制代码
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:
        - 
        - 
3.启动测试
1.登录
2.Redis可视化工具发现session成功存到redis

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

1.修改pom.xml,去掉分布式springsession的依赖
2.将用户信息放到Redis
1.添加Redis配置类 com/sxs/seckill/config/RedisConfig.java
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
2.修改doLogin方法,将用户信息放到Redis中
3.启动测试
1.登录
2.可视化工具查看用户信息
3.实现使用Redis + Cookie实现登录,可以访问商品列表页面
1.刚才已经实现了Redis记录信息的功能,但是校验还没实现,修改GoodsController.java完成校验
1.注入RedisTemplate
2.从Redis中获取校验信息,进行校验
2.测试
1.登录成功后访问商品列表页面

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

1.修改 com/sxs/seckill/controller/GoodsController.java 使参数直接为User
java 复制代码
    // 进入到商品首页
    @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
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失效了,用户就要重新登陆
java 复制代码
    @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
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 才能生效
java 复制代码
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.在登录之后,可以正常访问商品列表页面
相关推荐
王磊鑫9 分钟前
重返JAVA之路-初识JAVA
java·开发语言
半兽先生31 分钟前
WebRtc 视频流卡顿黑屏解决方案
java·前端·webrtc
南星沐2 小时前
Spring Boot 常用依赖介绍
java·前端·spring boot
代码不停2 小时前
Java中的异常
java·开发语言
Justice link3 小时前
部署redis cluster
数据库·redis·缓存
何似在人间5753 小时前
多级缓存模型设计
java·jvm·redis·缓存
多云的夏天3 小时前
ubuntu24.04-MyEclipse的项目导入到 IDEA中
java·intellij-idea·myeclipse
Fanxt_Ja3 小时前
【数据结构】红黑树超详解 ---一篇通关红黑树原理(含源码解析+动态构建红黑树)
java·数据结构·算法·红黑树
Aphelios3803 小时前
TaskFlow开发日记 #1 - 原生JS实现智能Todo组件
java·开发语言·前端·javascript·ecmascript·todo
程序猿阿伟3 小时前
《解锁分布式软总线:构建智能设备统一管理平台》
分布式