SpringBoot 操作Redis

一、Spring Boot 和 Redis 常用操作

Redis 是目前使用最广泛的缓存中间件,相比 Memcached,Redis 支持更多的数据结构和更丰富的数据操作,另外 Redis 有着丰富的集群方案和使用场景,这一课我们一起学习 Redis 的常用操作。

1、Redis 介绍

Redis 是一个速度非常快的非关系数据库(Non-Relational Database),它可以存储键(Key)与 5 种不同类型的值(Value)之间的映射(Mapping),可以将存储在内存的键值对数据持久化到硬盘,可以使用复制特性来扩展读性能,还可以使用客户端分片来扩展写性能。

为了满足高性能,Redis 采用内存(in-memory)数据集(Dataset),根据使用场景,可以通过每隔一段时间转储数据集到磁盘,或者追加每条命令到日志来持久化。持久化也可以被禁用,如果你只是需要一个功能丰富、网络化的内存缓存。

  • 数据模型

  • Redis 数据模型不仅与关系数据库管理系统(RDBMS)不同,也不同于任何简单的 NoSQL 键-值数据存储。Redis 数据类型类似于编程语言的基础数据类型,因此开发人员感觉很自然,每个数据类型都支持适用于其类型的操作,受支持的数据类型包括:

    • String(字符串)
    • Hash(哈希)
    • List(列表)
    • Set(集合)
    • Zset(Sorted Set:有序集合)
  • 关键优势

  • Redis 的优势包括它的速度、对富数据类型的支持、操作的原子性,以及通用性:

    • 性能极高,它每秒可执行约 100,000 个 Set 以及约 100,000 个 Get 操作;
    • 丰富的数据类型,Redis 对大多数开发人员已知的大多数数据类型提供了原生支持,这使得各种问题得以轻松解决;
    • 原子性,因为所有 Redis 操作都是原子性的,所以多个客户端会并发地访问一个 Redis 服务器,获取相同的更新值;
    • 丰富的特性,Redis 是一个多效用工具,有非常多的应用场景,包括缓存、消息队列(Redis 原生支持发布/订阅)、短期应用程序数据(比如 Web 会话、Web 页面命中计数)等

2、spring-boot-starter-data-redis

  • Spring Boot 提供了对 Redis 集成的组件包:spring-boot-starter-data-redis,它依赖于 spring-data-redis 和 lettuce。Spring Boot 1.0 默认使用的是 Jedis 客户端,2.0 替换成了 Lettuce,但如果你从 Spring Boot 1.5.X 切换过来,几乎感受不大差异,这是因为 spring-boot-starter-data-redis 为我们隔离了其中的差异性。

    • Lettuce:是一个可伸缩线程安全的 Redis 客户端,多个线程可以共享同一个 RedisConnection,它利用优秀 Netty NIO 框架来高效地管理多个连接。
    • Spring Data:是 Spring 框架中的一个主要项目,目的是为了简化构建基于 Spring 框架应用的数据访问,包括非关系数据库、Map-Reduce 框架、云数据服务等,另外也包含对关系数据库的访问支持。
    • Spring Data Redis:是 Spring Data 项目中的一个主要模块,实现了对 Redis 客户端 API 的高度封装,使对 Redis 的操作更加便捷。
  • 可以用以下方式来表达它们之间的关系:

Lettuce → Spring Data Redis → Spring Data → spring-boot-starter-data-redis

因此 Spring Data Redis 和 Lettuce 具备的功能,spring-boot-starter-data-redis 几乎都会有。

3、快速上手

  • 相关配置
  • 引入依赖包
    • 引入 commons-pool 2 是因为 Lettuce 需要使用 commons-pool 2 创建 Redis 连接池。
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>
  • application 配置
    • 从配置也可以看出 Spring Boot 默认支持 Lettuce 连接池。
# Redis 数据库索引(默认为 0)
spring.redis.database=0
# Redis 服务器地址
spring.redis.host=localhost
# Redis 服务器连接端口
spring.redis.port=6379  
# Redis 服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制) 默认 8
spring.redis.lettuce.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
spring.redis.lettuce.pool.max-wait=-1
# 连接池中的最大空闲连接 默认 8
spring.redis.lettuce.pool.max-idle=8
# 连接池中的最小空闲连接 默认 0
spring.redis.lettuce.pool.min-idle=0
  • 缓存配置
  • 在这里可以为 Redis 设置一些全局配置,比如配置主键的生产策略 KeyGenerator,如不配置会默认使用参数名作为主键。
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport{

    @Bean
    public KeyGenerator keyGenerator() {
        return new KeyGenerator() {
            @Override
            public Object generate(Object target, Method method, Object... params) {
                StringBuilder sb = new StringBuilder();
                sb.append(target.getClass().getName());
                sb.append(method.getName());
                for (Object obj : params) {
                    sb.append(obj.toString());
                }
                return sb.toString();
            }
        };
    }
}

注意,我们使用了注解:@EnableCaching 来开启缓存。

  • 测试使用
  • 在单元测试中,注入 RedisTemplate。String 是最常用的一种数据类型,普通的 key/value 存储都可以归为此类,value 其实不仅是 String 也可以是数字。
@RunWith(SpringRunner.class)
@SpringBootTest
public class TestRedisTemplate {
    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    public void testString()  {
        redisTemplate.opsForValue().set("neo", "ityouknow");
        Assert.assertEquals("ityouknow", redisTemplate.opsForValue().get("neo"));
    }
}
  • 在这个单元测试中,我们使用 redisTemplate 存储了一个字符串 "ityouknow",存储之后获取进行验证,多次进行 set 相同的 key,键对应的值会被覆盖

从上面的整个流程来看,使用 spring-boot-starter-data-redis 只需要三步就可以快速地集成 Redis 进行操作,下面介绍 Redis 如何操作各种数据类型。

  • 各类型实践

  • 我们知道 Redis 支持多种数据类型,实体、哈希、列表、集合、有序集合,那么在 Spring Boot 体系中都如何使用呢?

  • 实体

  • 先来看 Redis 对 Pojo 的支持,新建一个 User 对象,放到缓存中,再取出来。

@Test
public void testObj(){
    User user=new User("ityouknow@126.com", "smile", "youknow", "know","2020");
    ValueOperations<String, User> operations=redisTemplate.opsForValue();
    operations.set("com.neo", user);
    User u=operations.get("com.neo");
    System.out.println("user: "+u.toString());
}
  • 输出结果:
user: com.neo.domain.User@16fb356[id=<null>,userName=know,passWord=youknow,email=ityouknow@126.com,nickName=smile,regTime=2020]
  • 验证发现完美支持对象的存入和读取。

  • 超时失效

  • Redis 在存入每一个数据的时候都可以设置一个超时时间,过了这个时间就会自动删除数据,这种特性非常适合我们对阶段数据的缓存。

  • 新建一个 User 对象,存入 Redis 的同时设置 100 毫秒后失效,设置一个线程暂停 1000 毫秒之后,判断数据是否存在并打印结果。

@Test
public void testExpire() throws InterruptedException {
    User user=new User("ityouknow@126.com", "expire", "youknow", "expire","2020");
    ValueOperations<String, User> operations=redisTemplate.opsForValue();
    operations.set("expire", user,100,TimeUnit.MILLISECONDS);
    Thread.sleep(1000);
    boolean exists=redisTemplate.hasKey("expire");
    if(exists){
        System.out.println("exists is true");
    }else{
        System.out.println("exists is false");
    }
}
  • 输出结果:
exists is false
  • 从结果可以看出,Reids 中已经不存在 User 对象了,此数据已经过期,同时我们在这个测试的方法中使用了 hasKey("expire") 方法,可以判断 key 是否存在。

  • 删除数据

  • 有些时候,我们需要对过期的缓存进行删除,下面来测试此场景的使用。首先set 一个字符串“ityouknow”,紧接着删除此 key 的值,再进行判断。

@Test
public void testDelete() {
    ValueOperations<String, User> operations=redisTemplate.opsForValue();
    redisTemplate.opsForValue().set("deletekey", "ityouknow");
    redisTemplate.delete("deletekey");
    boolean exists=redisTemplate.hasKey("deletekey");
    if(exists){
        System.out.println("exists is true");
    }else{
        System.out.println("exists is false");
    }
}
  • 输出结果:
exists is false
  • 结果表明字符串“ityouknow”已经被成功删除。

  • Hash(哈希)

一般我们存储一个键,很自然的就会使用 get/set 去存储,实际上这并不是很好的做法。Redis 存储一个 key 会有一个最小内存,不管你存的这个键多小,都不会低于这个内存,因此合理的使用 Hash 可以帮我们节省很多内存

  • Hash Set 就在哈希表 Key 中的域(Field)的值设为 value。如果 Key 不存在,一个新的哈希表被创建并进行 Hset 操作;如果域(Field)已经存在于哈希表中,旧值将被覆盖。
@Test
public void testHash() {
    HashOperations<String, Object, Object> hash = redisTemplate.opsForHash();
    hash.put("hash","you","you");
    String value=(String) hash.get("hash","you");
    System.out.println("hash value :"+value);
}
  • 输出结果:
hash value :you
  • 根据上面测试用例发现,Hash set 的时候需要传入三个参数,第一个为 key,第二个为 Field,第三个为存储的值。一般情况下 Key 代表一组数据,Field 为 key 相关的属性,而 Value 就是属性对应的值。

  • List

Redis List 的应用场景非常多,也是 Redis 最重要的数据结构之一。 使用 List 可以轻松的实现一个队列,List 典型的应用场景就是消息队列,可以利用 List 的 Push 操作,将任务存在 List 中,然后工作线程再用 POP 操作将任务取出进行执行。

@Test
public void testList() {
    ListOperations<String, String> list = redisTemplate.opsForList();
    list.leftPush("list","it");
    list.leftPush("list","you");
    list.leftPush("list","know");
    String value=(String)list.leftPop("list");
    System.out.println("list value :"+value.toString());
}
  • 输出结果:
list value :know
  • 上面的例子我们从左侧插入一个 key 为 "list" 的队列,然后取出左侧最近的一条数据。其实 List 有很多 API 可以操作,比如从右侧进行插入队列从右侧进行读取,或者通过方法 range 读取队列的一部分。接着上面的例子我们使用 range 来读取。
List<String> values=list.range("list",0,2);
   for (String v:values){
       System.out.println("list range :"+v);
   }
  • 输出结果:
list range :know
list range :you
list range :it
  • range 后面的两个参数就是插入数据的位置,输入不同的参数就可以取出队列中对应的数据。

Redis List 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销,Redis 内部的很多实现,包括发送缓冲队列等也都是用的这个数据结构。

  • Set

Redis Set 对外提供的功能与 List 类似是一个列表的功能,特殊之处在于 Set 是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,Set 是一个很好的选择,并且 Set 提供了判断某个成员是否在一个 Set 集合内的重要接口,这个也是 List 所不能提供的。

@Test
public void testSet() {
    String key="set";
    SetOperations<String, String> set = redisTemplate.opsForSet();
    set.add(key,"it");
    set.add(key,"you");
    set.add(key,"you");
    set.add(key,"know");
    Set<String> values=set.members(key);
    for (String v:values){
        System.out.println("set value :"+v);
    }
}
  • 输出结果:
set value :it
set value :know
set value :you
  • 通过上面的例子我们发现,输入了两个相同的值“you”,全部读取的时候只剩下了一条,说明 Set 对队列进行了自动的排重操作。
  • Redis 为集合提供了求交集、并集、差集等操作,可以非常方便的使用。
  • 测试 difference
SetOperations<String, String> set = redisTemplate.opsForSet();
String key1="setMore1";
String key2="setMore2";
set.add(key1,"it");
set.add(key1,"you");
set.add(key1,"you");
set.add(key1,"know");
set.add(key2,"xx");
set.add(key2,"know");
Set<String> diffs=set.difference(key1,key2);
for (String v:diffs){
    System.out.println("diffs set value :"+v);
}
  • 输出结果:
diffs set value :it
diffs set value :you
  • 根据上面这个例子可以看出,difference() 函数会把 key 1 中不同于 key 2 的数据对比出来,这个特性适合我们在金融场景中对账的时候使用。

  • 测试 unions

SetOperations<String, String> set = redisTemplate.opsForSet();
String key3="setMore3";
String key4="setMore4";
set.add(key3,"it");
set.add(key3,"you");
set.add(key3,"xx");
set.add(key4,"aa");
set.add(key4,"bb");
set.add(key4,"know");
Set<String> unions=set.union(key3,key4);
for (String v:unions){
    System.out.println("unions value :"+v);
}
  • 输出结果:
unions value :know
unions value :you
unions value :xx
unions value :it
unions value :bb
unions value :aa
  • 根据例子我们发现,unions 会取两个集合的合集,Set 还有其他很多类似的操作,非常方便我们对集合进行数据处理。

Set 的内部实现是一个 Value 永远为 null 的 HashMap,实际就是通过计算 Hash 的方式来快速排重,这也是 Set 能提供判断一个成员是否在集合内的原因。

  • ZSet

Redis Sorted Set 的使用场景与 Set 类似,区别是 Set 不是自动有序的,而 Sorted Set 可以通过用户额外提供一个优先级(Score)的参数来为成员排序,并且是插入有序,即自动排序。

  • 在使用 Zset 的时候需要额外的输入一个参数 Score,Zset 会自动根据 Score 的值对集合进行排序,我们可以利用这个特性来做具有权重的队列,比如普通消息的 Score 为1,重要消息的 Score 为 2,然后工作线程可以选择按 Score 的倒序来获取工作任务。
@Test
public void testZset(){
    String key="zset";
    redisTemplate.delete(key);
    ZSetOperations<String, String> zset = redisTemplate.opsForZSet();
    zset.add(key,"it",1);
    zset.add(key,"you",6);
    zset.add(key,"know",4);
    zset.add(key,"neo",3);

    Set<String> zsets=zset.range(key,0,3);
    for (String v:zsets){
        System.out.println("zset value :"+v);
    }

    Set<String> zsetB=zset.rangeByScore(key,0,3);
    for (String v:zsetB){
        System.out.println("zsetB value :"+v);
    }
}
  • 输出结果:
zset value :it
zset value :neo
zset value :know
zset value :you
zsetB value :it
zsetB value :neo
  • 通过上面的例子我们发现插入到 Zset 的数据会自动根据 Score 进行排序,根据这个特性我们可以做优先队列等各种常见的场景。另外 Redis 还提供了 rangeByScore 这样的一个方法,可以只获取 Score 范围内排序后的数据。

Redis Sorted Set 的内部使用 HashMap 和跳跃表(SkipList)来保证数据的存储和有序,HashMap 里放的是成员到 Score 的映射,而跳跃表里存放的是所有的成员,排序依据是 HashMap 里存的 Score,使用跳跃表的结构可以获得比较高的查找效率,并且在实现上比较简单。

  • 封装

在我们实际的使用过程中,不会给每一个使用的类都注入 redisTemplate 来直接使用,一般都会对业务进行简单的包装,最后提供出来对外使用。

  • 首先定义一个 RedisService 服务,将 RedisTemplate 注入到类中。
@Service
public class RedisService {
    @Autowired
    private RedisTemplate redisTemplate;
}
  • 封装简单插入操作:
public boolean set(final String key, Object value) {
    boolean result = false;
    try {
        ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
        operations.set(key, value);
        result = true;
    } catch (Exception e) {
        logger.error("set error: key {}, value {}",key,value,e);
    }
    return result;
}
  • 会对其中出现的异常继续处理,反馈给调用方。
  • 比如我们想删除某一类的 Key 的值,使用 Redis 的 Pattern 来匹配出一批符合条件的缓存,然后批量进行删除。
public void removePattern(final String pattern) {
    Set<Serializable> keys = redisTemplate.keys(pattern);
    if (keys.size() > 0)
        redisTemplate.delete(keys);
}
  • 还有其他封装方法,比如删除的时候先判断 Key 是否存在等,这些简单的业务判断都应该封装在 RedisService,对外提供最简单的 API 调用即可。
@Autowired
private RedisService redisService;

@Test
public void testString() throws Exception {
    redisService.set("neo", "ityouknow");
    Assert.assertEquals("ityouknow", redisService.get("neo"));
}
  • 在其他服务使用的时候将 RedisService 注入其中,调用对应的方法来操作 Redis,这样会更优雅简单一些。

4、小结

Redis 是一款非常优秀的高性能缓存中间件,被广泛的使用在各互联网公司中,Spring Boot 对 Redis 的操作提供了很多支持,可以非常方便的去集成。Redis 拥有丰富的数据类型,方便我们在不同的业务场景中去使用,特别是提供了很多内置的高效集合操作,在业务中使用非常方便。

二、使用 Redis 实现 Session 共享

在微服务架构中,往往由多个微服务共同支撑前端请求,如果涉及到用户状态就需要考虑分布式 Session 管理问题,比如用户登录请求分发在服务器 A,用户购买请求分发到了服务器 B, 那么服务器就必须可以获取到用户的登录信息,否则就会影响正常交易。因此,在分布式架构或微服务架构下,必须保证一个应用服务器上保存 Session 后,其他应用服务器可以同步或共享这个 Session

1、目前主流的分布式 Session 管理有两种方案

  • Session 复制

部分 Web 服务器能够支持 Session 复制功能,如 Tomcat。用户可以通过修改 Web 服务器的配置文件,让 Web 服务器进行 Session 复制,保持每一个服务器节点的 Session 数据都能达到一致。

这种方案的实现依赖于 Web 服务器,需要 Web 服务器有 Session 复制功能。当 Web 应用中 Session 数量较多的时候,每个服务器节点都需要有一部分内存用来存放 Session,将会占用大量内存资源。同时大量的 Session 对象通过网络传输进行复制,不但占用了网络资源,还会因为复制同步出现延迟,导致程序运行错误。

在微服务架构中,往往需要 N 个服务端来共同支持服务,不建议采用这种方案。

  • Session 集中存储

在单独的服务器或服务器集群上使用缓存技术,如 Redis 存储 Session 数据,集中管理所有的 Session,所有的 Web 服务器都从这个存储介质中存取对应的 Session,实现 Session 共享。将 Session 信息从应用中剥离出来后,其实就达到了服务的无状态化,这样就方便在业务极速发展时水平扩充。

在微服务架构下,推荐采用此方案,接下来详细介绍。

2、 Session 共享

  • 什么是session

由于 HTTP 协议是无状态的协议,因而服务端需要记录用户的状态时,就需要用某种机制来识具体的用户。Session 是另一种记录客户状态的机制,不同的是 Cookie 保存在客户端浏览器中,而 Session 保存在服务器上。客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上,这就是 Session。客户端浏览器再次访问时只需要从该 Session 中查找该客户的状态就可以了。

  • 为什么需要session共享

    • 在互联网行业中用户量访问巨大,往往需要多个节点共同对外提供某一种服务,如下图:
  • 用户的请求首先会到达前置网关,前置网关根据路由策略将请求分发到后端的服务器,这就会出现第一次的请求会交给服务器 A 处理,下次的请求可能会是服务 B 处理,如果不做 Session 共享的话,就有可能出现用户在服务 A 登录了,下次请求的时候到达服务 B 又要求用户重新登录。

  • 前置网关我们一般使用 lvs、Nginx 或者 F5 等软硬件,有些软件可以指定策略让用户每次请求都分发到同一台服务器中,这也有个弊端,如果当其中一台服务 Down 掉之后,就会出现一批用户交易失效。在实际工作中我们建议使用外部的缓存设备来共享 Session,避免单个节点挂掉而影响服务,使用外部缓存 Session 后,我们的共享数据都会放到外部缓存容器中,服务本身就会变成无状态的服务,可以随意的根据流量的大小增加或者减少负载的设备。

  • Spring 官方针对 Session 管理这个问题,提供了专门的组件 Spring Session,使用 Spring Session 在项目中集成分布式 Session 非常方便

3、Spring Session

Spring Session 提供了一套创建和管理 Servlet HttpSession 的方案。Spring Session 提供了集群 Session(Clustered Sessions)功能,默认采用外置的 Redis 来存储 Session 数据,以此来解决 Session 共享的问题。

  • Spring Session 为企业级 Java 应用的 Session 管理带来了革新,使得以下的功能更加容易实现:

    • API 和用于管理用户会话的实现;
    • HttpSession,允许以应用程序容器(即 Tomcat)中性的方式替换 HttpSession;
    • 将 Session 所保存的状态卸载到特定的外部 Session 存储中,如 Redis 或 Apache Geode 中,它们能够以独立于应用服务器的方式提供高质量的集群;
    • 支持每个浏览器上使用多个 Session,从而能够很容易地构建更加丰富的终端用户体验;
    • 控制 Session ID 如何在客户端和服务器之间进行交换,这样的话就能很容易地编写 Restful API,因为它可以从 HTTP 头信息中获取 Session ID,而不必再依赖于 cookie;
    • 当用户使用 WebSocket 发送请求的时候,能够保持 HttpSession 处于活跃状态。
  • 需要说明的很重要的一点就是,Spring Session 的核心项目并不依赖于 Spring 框架,因此,我们甚至能够将其应用于不使用 Spring 框架的项目中。

  • Spring 为 Spring Session 和 Redis 的集成提供了组件:spring-session-data-redis,接下来演示如何使用。

4、快速集成

  • 引入依赖包
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
  • 添加配置文件
# 数据库配置
spring.datasource.url=jdbc:mysql://localhost:3306/test?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# JPA 配置
spring.jpa.properties.hibernate.hbm2ddl.auto=create
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.show-sql= true
# Redis 配置
# Redis 数据库索引(默认为0)
spring.redis.database=0  
# Redis 服务器地址
spring.redis.host=localhost
# Redis 服务器连接端口
spring.redis.port=6379  
# Redis 服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1
spring.redis.lettuce.shutdown-timeout=100
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0

整体配置分为三块:数据库配置、JPA 配置、Redis 配置,具体配置项在前面课程都有所介绍。在项目中创建 SessionConfig 类,使用注解配置其过期时间。

  • session配置
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 86400*30)
public class SessionConfig {
}

maxInactiveIntervalInSeconds: 设置 Session 失效时间,使用 Redis Session 之后,原 Spring Boot 中的 server.session.timeout 属性不再生效。

  • 仅仅需要这两步 Spring Boot 分布式 Session 就配置完成了。

5、测试验证

  • 我们在 Web 层写两个方法进行验证。
@RequestMapping(value = "/setSession")
public Map<String, Object> setSession (HttpServletRequest request){
    Map<String, Object> map = new HashMap<>();
    request.getSession().setAttribute("message", request.getRequestURL());
    map.put("request Url", request.getRequestURL());
    return map;
}
  • 上述方法中获取本次请求的请求地址,并把请求地址放入 Key 为 message 的 Session 中,同时结果返回页面。
@RequestMapping(value = "/getSession")
public Object getSession (HttpServletRequest request){
    Map<String, Object> map = new HashMap<>();
    map.put("sessionId", request.getSession().getId());
    map.put("message", request.getSession().getAttribute("message"));
    return map;
}
  • getSession() 方法获取 Session 中的 Session Id 和 Key 为 message 的信息,将获取到的信息封装到 Map 中并在页面展示。

  • 在测试前我们需要将项目 spring-boot-redis-session 复制一份,改名为 spring-boot-redis-session-1 并将端口改为:9090(server.port=9090)。修改完成后依次启动两个项目。

  • 首先访问 8080 端口的服务,浏览器输入网址 http://localhost:8080/setSession, 返回:{"request Url":"http://localhost:8080/setSession"};浏览器栏输入网址 http://localhost:8080/getSession,返回信息如下:

{"sessionId":"432765e1-049e-4e76-980c-d7f55a232d42","message":"http://localhost:8080/setSession"}
  • 说明 Url 地址信息已经存入到 Session 中。

  • 访问 9090 端口的服务,浏览器栏输入网址 http://localhost:9090/getSession,返回信息如下:

{"sessionId":"432765e1-049e-4e76-980c-d7f55a232d42","message":"http://localhost:8080/setSession"}
  • 通过对比发现,8080 和 9090 服务返回的 Session 信息完全一致,说明已经实现了 Session 共享。

6、模拟登录

在实际中作中常常使用共享 Session 的方式去保存用户的登录状态,避免用户在不同的页面多次登录。我们来简单模拟一下这个场景,假设有一个 index 页面,必须是登录的用户才可以访问,如果用户没有登录给出请登录的提示。在一台实例上登录后,再次访问另外一台的 index 看它是否需要再次登录,来验证统一登录是否成功。

  • 添加登录方法,登录成功后将用户信息存放到 Session 中。
@RequestMapping(value = "/login")
public String login (HttpServletRequest request,String userName,String password){
    String msg="logon failure!";
    User user= userRepository.findByUserName(userName);
    if (user!=null && user.getPassword().equals(password)){
        request.getSession().setAttribute("user",user);
        msg="login successful!";
    }
    return msg;
}
  • 通过 JPA 的方式查询数据库中的用户名和密码,通过对比判断是否登录成功,成功后将用户信息存储到 Session 中。

  • 在添加一个登出的方法,清除掉用户的 Session 信息。

@RequestMapping(value = "/loginout")
public String loginout (HttpServletRequest request){
    request.getSession().removeAttribute("user");
    return "loginout successful!";
}
  • 定义 index 方法,只有用户登录之后才会看到:index content ,否则提示请先登录。
@RequestMapping(value = "/index")
public String index (HttpServletRequest request){
    String msg="index content";
    Object user= request.getSession().getAttribute("user");
    if (user==null){
        msg="please login first!";
    }
    return msg;
}
  • 和上面一样我们需要将项目复制为两个,第二个项目的端口改为 9090,依次启动两个项目。在 test 数据库中的 user 表添加一个用户名为 neo,密码为 123456 的用户,脚本如下:
INSERT INTO `user` VALUES ('1', 'ityouknow@126.com', 'smile', '123456', '2018', 'neo');

也可以利用 Spring Data JPA 特性在应用启动时完成数据初始化:当配置 spring.jpa.hibernate.ddl-auto : create-drop,在应用启动时,自动根据 Entity 生成表,并且执行 classpath 下的 import.sql。

  • 首先测试 8080 端口的服务,直接访问网址 http://localhost:8080/index,返回:please login first!提示请先登录。我们将验证用户名为 neo,密码为 123456 的用户登录。访问地址 http://localhost:8080/login?userName=neo&password=123456 模拟用户登录,返回:login successful!,提示登录成功。我们再次访问地址 http://localhost:8080/index,返回 index content 说明已经可以查看受限的资源。

  • 再来测试 9090 端口的服务,直接访问网址 http://localhost:9090/index,页面返回 index content,并没有提示请先进行登录,这说明 9090 服务已经同步了用户的登录状态,达到了统一登录的目的。

  • 我们在 8080 服务上测试用户退出系统,再来验证 9090 的用户登录状态是否同步失效。首先访问地址 http://localhost:8080/loginout 模拟用户在 8080 服务上退出,访问网址 http://localhost:8080/index,返回 please login first!说明用户在 8080 服务上已经退出。再次访问地址 http://localhost:9090/index,页面返回:please login first!,说明 9090 服务上的退出状态也进行了同步。

注意,本次实验只是简单模拟统一登录,实际生产中我们会以 Filter 的方式对登录状态进行校验,在本课程的最后一节课中也会讲到这方面的内容。

  • 我们最后来看一下,使用 Redis 作为 Session 共享之后的示意图:

  • 从上图可以看出,所有的服务都将 Session 的信息存储到 Redis 集群中,无论是对 Session 的注销、更新都会同步到集群中,达到了 Session 共享的目的。

7、小结

在微服务架构下,系统被分割成大量的小而相互关联的微服务,因此需要考虑分布式 Session 管理,方便平台架构升级时水平扩充。通过向架构中引入高性能的缓存服务器,将整个微服务架构下的 Session 进行统一管理。

Spring Session 是 Spring 官方提供的 Session 管理组件,集成到 Spring Boot 项目中轻松解决分布式 Session 管理的问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值