Spring Session - 使用Spring Session从零到一构建分布式session

简介: Spring Session - 使用Spring Session从零到一构建分布式session

20210215205331403.png

快速入门 Spring Session + Redis

官网指导

https://spring.io/projects/spring-session-data-redis#samples


20210216122249304.png


我们就用spring boot 来演示下吧

Demo

20210216171351219.png


pom 依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>boot2</artifactId>
        <groupId>com.artisan</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>springsession</artifactId>
    <dependencies>
        <!-- 实现对 Spring MVC 的自动化配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- redis  lettuce 需要使用-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <!-- 实现对 Spring Session 使用 Redis 作为数据源的自动化配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- 实现对 Spring Data Redis 的自动化配置 -->
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>


配置文件

server:
  port: 8888
spring:
  redis:
    host: 127.0.0.1
    port: 6379
    password: # Redis密码
    timeout: 5000ms
    lettuce:
      pool:
        max-active: 8
        max-wait: -1ms
        max-idle: 8
        min-idle: 0
  session:
    store-type: redis


max-active: 8 # 连接池最大连接数,默认为 8 。使用负数表示没有限制。


max-idle: 8 # 默认连接数最大空闲的连接数,默认为 8 。使用负数表示没有限制。


min-idle: 0 # 默认连接池最小空闲的连接数,默认为 0 。允许设置 0 和 正数。


max-wait: -1 # 连接池最大阻塞等待时间,单位:毫秒。默认为 -1 ,表示不限制。


session: store-type: redis 指定存储类型


配置类RedisHttpSessionConfiguration

package com.artisan.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
/**
 * @author 小工匠
 * @version 1.0
 * @description: TODO
 * @date 2021/2/16 14:12
 * @mark: show me the code , change the world
 */
@Configuration
@EnableRedisHttpSession
public class RedisHttpSessionConfiguration {
    @Bean(name = "springSessionDefaultRedisSerializer")
    public RedisSerializer springSessionDefaultRedisSerializer() {
        return RedisSerializer.json();
    }
}


添加 @EnableRedisHttpSession 注解,开启自动化配置 Spring Session 使用 Redis 作为数据 。

我们来下 EnableRedisHttpSession 注解

20210216172255872.png


maxInactiveIntervalInSeconds 属性,Session 不活跃后的过期时间,默认为 1800 秒。


redisNamespace 属性,在 Redis 的 key 的统一前缀,默认为 “spring:session” 。


flushMode 属性,Redis 会话刷新模式(RedisFlushMode)。支持两种,默认为RedisFlushMode.ON_SAVE


RedisFlushMode.ON_SAVE ,在请求执行完成时,统一写入 Redis 存储。

RedisFlushMode.IMMEDIATE ,在每次修改 Session 时,立即写入 Redis 存储。


cleanupCron 属性,清理 Redis Session 会话过期的任务执行 CRON 表达式,默认为 "0 * * * * *" 每分钟执行一次。虽然Redis 自带了 key 的过期,但是惰性删除策略,实际过期的 Session 还在 Redis 中占用内存。所以,Spring Session 通过定时任务,删除 Redis 中过期的 Session ,尽快释放 Redis 的内存。


默认情况下,采用 Java 自带的序列化方式 ,可读性很差。 所以在 springSessionDefaultRedisSerializer() 方法,定义了一个 Bean 名字为 springSessionDefaultRedisSerializer的 RedisSerializer Bean ,采用 JSON 序列化方式 。


好了,截止到目前,核心的框架已经搭建起来了,我们来测试下


package com.artisan.controller;
import com.artisan.common.CommonResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
 * @author 小工匠
 * @version 1.0
 * @description: TODO
 * @date 2021/2/16 14:42
 * @mark: show me the code , change the world
 */
@RestController
public class ArtisanController  extends BaseController {
    @GetMapping("/mockSet")
    public CommonResult set(@RequestParam("key") String key, @RequestParam("value") String value) {
        getHttpSession().setAttribute(key, value);
        return CommonResult.success("成功模拟登录");
    }
    @GetMapping("/mockGet") 
    public CommonResult get(@RequestParam("key") String key) {
        return CommonResult.success( getHttpSession().getAttribute(key));
    }
}

启动测试类,走一波

package com.artisan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringSeesionDemoApplication {
  public static void main(String[] args) {
    SpringApplication.run(SpringSeesionDemoApplication.class, args);
  }
}


访问 http://localhost:8888/mockSet?key=artisan &value=avalue

http://localhost:8888/mockGet?key=artisan

20210216174257475.png


Redis中的session数据解析

127.0.0.1:0>keys *
 1)  "spring:session:sessions:expires:e0dd90b9-9551-4e8a-9609-cde0758b88c2"
 2)  "spring:session:sessions:e0dd90b9-9551-4e8a-9609-cde0758b88c2"
 3)  "spring:session:expirations:1613470560000"


20210216175123775.png

每一个 Session 对应 Redis 二个 key-value 键值对


开头:以 spring:session 开头,可以通过 @EnableRedisHttpSession 注解的 redisNamespace 属性配置。

结尾:以对应 Session 的 sessionid 结尾。

中间:中间分别是 "session"、"expirations"、sessions:expires


一般情况下,只需要关注中间为 session 的 key-value 键值对即可,它负责真正存储 Session 数据20210216175621665.png


127.0.0.1:0>hgetall spring:session:sessions:ab7d40d8-cd3d-49d7-8b3a-d1ae71d40935
 1)  "lastAccessedTime"  # 最后访问时间
 2)  "1613469344975"
 3)  "maxInactiveInterval" # Session 允许最大不活跃时长,单位:秒。
 4)  "1800"
 5)  "creationTime"   # 创建时间
 6)  "1613469342207"
 7)  "sessionAttr:artisan" # 设置的属性值
 8)  ""avalue""
127.0.0.1:0>


对于中间为 sessions:expires和 expirations的两个来说,主要为了实现主动删除 Redis 过期的 Session 会话,解决 Redis 惰性删除的问题。


spring:session:expirations:{时间戳},是为了获得每分钟需要过期的 sessionid 集合,即 {时间戳} 是每分钟的时间戳


附 其他相关类

BaseController

package com.artisan.controller;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
public class BaseController  {
    public HttpServletRequest getRequest(){
        return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
    }
    public HttpServletResponse getResponse(){
        return ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getResponse();
    }
    public HttpSession getHttpSession(){
        return getRequest().getSession();
    }
}


统一返回结果相关的Code

【IErrorCode 】

package com.artisan.common;
public interface IErrorCode {
    long getCode();
    String getMessage();
}

【ResultCode 】

package com.artisan.common;
public enum ResultCode implements IErrorCode {
    SUCCESS(200, "操作成功"),
    FAILED(500, "操作失败"),
    VALIDATE_FAILED(404, "参数检验失败"),
    UNAUTHORIZED(401, "暂未登录或token已经过期"),
    FORBIDDEN(403, "没有相关权限");
    private long code;
    private String message;
    private ResultCode(long code, String message) {
        this.code = code;
        this.message = message;
    }
    public long getCode() {
        return code;
    }
    public String getMessage() {
        return message;
    }
}


【CommonResult】

package com.artisan.common;
public class CommonResult<T> {
    private long code;
    private String message;
    private T data;
    protected CommonResult() {
    }
    protected CommonResult(long code, String message, T data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }
    /**
     * 成功返回结果
     *
     * @param data 获取的数据
     */
    public static <T> CommonResult<T> success(T data) {
        return new CommonResult<T>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data);
    }
    /**
     * 成功返回结果
     *
     * @param data 获取的数据
     * @param  message 提示信息
     */
    public static <T> CommonResult<T> success(T data, String message) {
        return new CommonResult<T>(ResultCode.SUCCESS.getCode(), message, data);
    }
    /**
     * 失败返回结果
     * @param errorCode 错误码
     */
    public static <T> CommonResult<T> failed(IErrorCode errorCode) {
        return new CommonResult<T>(errorCode.getCode(), errorCode.getMessage(), null);
    }
    /**
     * 失败返回结果
     * @param message 提示信息
     */
    public static <T> CommonResult<T> failed(String message) {
        return new CommonResult<T>(ResultCode.FAILED.getCode(), message, null);
    }
    /**
     * 失败返回结果
     */
    public static <T> CommonResult<T> failed() {
        return failed(ResultCode.FAILED);
    }
    /**
     * 参数验证失败返回结果
     */
    public static <T> CommonResult<T> validateFailed() {
        return failed(ResultCode.VALIDATE_FAILED);
    }
    /**
     * 参数验证失败返回结果
     * @param message 提示信息
     */
    public static <T> CommonResult<T> validateFailed(String message) {
        return new CommonResult<T>(ResultCode.VALIDATE_FAILED.getCode(), message, null);
    }
    /**
     * 未登录返回结果
     */
    public static <T> CommonResult<T> unauthorized(T data) {
        return new CommonResult<T>(ResultCode.UNAUTHORIZED.getCode(), ResultCode.UNAUTHORIZED.getMessage(), data);
    }
    /**
     * 未授权返回结果
     */
    public static <T> CommonResult<T> forbidden(T data) {
        return new CommonResult<T>(ResultCode.FORBIDDEN.getCode(), ResultCode.FORBIDDEN.getMessage(), data);
    }
    /**
     * 请求异常返回结果#add by yangguo
     */
    public static <T> CommonResult<T> badResponse(IErrorCode errorCode) {
        return new CommonResult<T>(errorCode.getCode(), errorCode.getMessage(), null);
    }
    public long getCode() {
        return code;
    }
    public void setCode(long code) {
        this.code = code;
    }
    public String getMessage() {
        return message;
    }
    public void setMessage(String message) {
        this.message = message;
    }
    public T getData() {
        return data;
    }
    public void setData(T data) {
        this.data = data;
    }
}


Jedis的POM依赖及配置


Spring Boot 2 以上默认使用lettuce作为redis的客户端,如果想要用jedis ,我这里也给大家准备了一份,请参考

  <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <exclusions>
                <!-- 去掉对 Lettuce 的依赖, Spring Boot 优先使用 Lettuce 作为 Redis 客户端 -->
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- 引入 Jedis 的依赖 -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>
    </dependencies>

spring:
  # 对应 RedisProperties 类
  redis:
    host: 127.0.0.1
    port: 6379
    password: # Redis 服务器密码,默认为空。生产中,一定要设置 Redis 密码!
    database: 0 # Redis 数据库号,默认为 0 。
    timeout: 0 # Redis 连接超时时间,单位:毫秒。
    # 对应 RedisProperties.Jedis 内部类
    jedis:
      pool:
        max-active: 8 # 连接池最大连接数,默认为 8 。使用负数表示没有限制。
        max-idle: 8 # 默认连接数最大空闲的连接数,默认为 8 。使用负数表示没有限制。
        min-idle: 0 # 默认连接池最小空闲的连接数,默认为 0 。允许设置 0 和 正数。
        max-wait: -1 # 连接池最大阻塞等待时间,单位:毫秒。默认为 -1 ,表示不限制。


相关文章
|
5月前
|
人工智能 Java Nacos
基于 Spring AI Alibaba + Nacos 的分布式 Multi-Agent 构建指南
本文将针对 Spring AI Alibaba + Nacos 的分布式多智能体构建方案展开介绍,同时结合 Demo 说明快速开发方法与实际效果。
4207 85
|
6月前
|
存储 Kubernetes 微服务
Dapr:用于构建分布式应用程序的便携式事件驱动运行时
Dapr 是一个可移植、事件驱动的运行时,简化了分布式应用程序的开发。它支持多语言、多框架,适用于云和边缘计算环境,提供服务调用、状态管理、消息发布/订阅等构建模块。通过 sidecar 模式,Dapr 帮助开发者轻松应对微服务架构的复杂性,实现弹性、可扩展的应用部署。
440 9
Dapr:用于构建分布式应用程序的便携式事件驱动运行时
|
6月前
|
人工智能 Java API
构建基于Java的AI智能体:使用LangChain4j与Spring AI实现RAG应用
当大模型需要处理私有、实时的数据时,检索增强生成(RAG)技术成为了核心解决方案。本文深入探讨如何在Java生态中构建具备RAG能力的AI智能体。我们将介绍新兴的Spring AI项目与成熟的LangChain4j框架,详细演示如何从零开始构建一个能够查询私有知识库的智能问答系统。内容涵盖文档加载与分块、向量数据库集成、语义检索以及与大模型的最终合成,并提供完整的代码实现,为Java开发者开启构建复杂AI智能体的大门。
3310 58
|
5月前
|
缓存 监控 Java
《深入理解Spring》性能监控与优化——构建高性能应用的艺术
本文系统介绍了Spring生态下的性能监控与优化实践,涵盖监控体系构建、数据库调优、缓存策略、线程池配置及性能测试等内容,强调通过数据驱动、分层优化和持续迭代提升应用性能。
|
5月前
|
负载均衡 Java API
《深入理解Spring》Spring Cloud 构建分布式系统的微服务全家桶
Spring Cloud为微服务架构提供一站式解决方案,涵盖服务注册、配置管理、负载均衡、熔断限流等核心功能,助力开发者构建高可用、易扩展的分布式系统,并持续向云原生演进。
|
6月前
|
消息中间件 缓存 监控
中间件架构设计与实践:构建高性能分布式系统的核心基石
摘要 本文系统探讨了中间件技术及其在分布式系统中的核心价值。作者首先定义了中间件作为连接系统组件的&quot;神经网络&quot;,强调其在数据传输、系统稳定性和扩展性中的关键作用。随后详细分类了中间件体系,包括通信中间件(如RabbitMQ/Kafka)、数据中间件(如Redis/MyCAT)等类型。文章重点剖析了消息中间件的实现机制,通过Spring Boot代码示例展示了消息生产者的完整实现,涵盖消息ID生成、持久化、批量发送及重试机制等关键技术点。最后,作者指出中间件架构设计对系统性能的决定性影响,
|
6月前
|
NoSQL Java 调度
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
分布式锁是分布式系统中用于同步多节点访问共享资源的机制,防止并发操作带来的冲突。本文介绍了基于Spring Boot和Redis实现分布式锁的技术方案,涵盖锁的获取与释放、Redis配置、服务调度及多实例运行等内容,通过Docker Compose搭建环境,验证了锁的有效性与互斥特性。
543 0
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
|
7月前
|
存储 负载均衡 NoSQL
【赵渝强老师】Redis Cluster分布式集群
Redis Cluster是Redis的分布式存储解决方案,通过哈希槽(slot)实现数据分片,支持水平扩展,具备高可用性和负载均衡能力,适用于大规模数据场景。
499 2
|
7月前
|
存储 缓存 NoSQL
【📕分布式锁通关指南 12】源码剖析redisson如何利用Redis数据结构实现Semaphore和CountDownLatch
本文解析 Redisson 如何通过 Redis 实现分布式信号量(RSemaphore)与倒数闩(RCountDownLatch),利用 Lua 脚本与原子操作保障分布式环境下的同步控制,帮助开发者更好地理解其原理与应用。
453 6
|
8月前
|
存储 缓存 NoSQL
Redis核心数据结构与分布式锁实现详解
Redis 是高性能键值数据库,支持多种数据结构,如字符串、列表、集合、哈希、有序集合等,广泛用于缓存、消息队列和实时数据处理。本文详解其核心数据结构及分布式锁实现,帮助开发者提升系统性能与并发控制能力。

热门文章

最新文章