工作中Redis最常用的两个场景:一是数据缓存,另一是Session共享。Spring Boot针对这两场景都做了一些定制及,使得在实际项目中使用非常的方便。

一、数据缓存

使用Redis做为数据缓存是最常用的场景。绝大多数的网站/系统,最先遇到的一个性能瓶颈就是数据库,使用Redis做数据库的前置缓存,可以有效的降低数据库的压力,从而提升整个系统的响应效率和并发量。Spring Boot也提供了非常简单的解决方案。

A、相关依赖



<dependency>



spring-boot-starter-cache会进行缓存的自动化配置和识别,Spring Boot为Redis自动配置了RedisCacheConfiguration等信息,具体redis配置参看上篇,《SpringBoot & Redis》。

B、RedisConfig配置

需要使用@EnableCaching启用缓存配置,否则出现注解不生效的情况;对于存储对象,使用Jackson进行序列化。



@Configuration



二、缓存注解

关于Spring Cache最核心的三个注解:@Cacheable、@CachePut、@CacheEvict。

A、@Cacheable

@Cacheable用来声明方法是可缓存的,将结果存储到缓存中以便后续使用相同参数调用时不需执行实际的方法,直接从缓存中取值。注解可以标记在一个方法上,也可以标记在一个类上,当标记在一个方法上时表示该方法是支持缓存的,当标记在一个类上时则表示该类所有的方法都是支持缓存的。



@RequestMapping



启动项目后访问http://localhost:8080/getUser?id=1,第一次访问会输出log,但是再次访问相同URL,则不会输出log,说明这次请求内容直接由缓存返回。




redis缓存刷新js刷新页面 redis缓存前端页面_服务器


@Cacheable(value ="userCache")注解的含义是,当调用这个方法的时候,会从一个key为userCache:id的缓存中查询,如果没有,则执行实际的方法,并将返回的结果存入缓存中;否则返回缓存中的对象。

从Redis中可以看到对象被缓存。


redis缓存刷新js刷新页面 redis缓存前端页面_缓存_02


B、注解参数

@Cacheable 支持以下参数:

  • value:缓存的名称。
  • key:缓存的key,可以为空,如果指定要按照SpEL表达式编写,如果不指定,则缺省按照方法的所有参数进行组合。
  • condition:触发条件,只有满足条件的情况才会加入缓存,默认为空,既表示全部都加入缓存,支持SpEL。
@RequestMapping


启动后浏览器调用相关方法,第一次输出栏输出log,再次执行无输出表明,已经走缓存。

将用户名长度缩短,反复执行,一直输出log,说明条件condition生效。


redis缓存刷新js刷新页面 redis缓存前端页面_Redis_03


C、@CachePut

项目运行中会对数据库的信息进行更新,如果仍然使用@Cacheable就会导致数据库的信息和缓存的信息不一致。项目中,一般更新完数据库后,再手动删除掉Redis中对应的缓存,以保证数据的一致性。Spring Boot提供了另外的一种解决方案,可以优雅的去更新缓存。

与@Cacheable不同的是使用@CachePut注解的方法在执行前,不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中


@RequestMapping


先执行save方法,查看redis中,已经有该缓存,再调用获取用户方法,没有log输出,说明执行在方法上声明@CachePut会自动执行方法,并将结果存入缓存。

@CachePut的参数和使用方法基本和@Cacheable一致。

D、@CacheEvict

@CacheEvict是用来标注在需要清除缓存元素的方法或类上的,当标记在一个类上时表示其中所有的方法的执行都会触发缓存的清除操作。

@CacheEvict可以指定的参数有value、key、condition、allEntries 和 beforeInvocation。其中value、key 和 condition的语义与@Cacheable对应的属性类似。

1、allEntries属性

allEntries是boolean类型,表示是否需要清除缓存中的所有元素。默认为false,表示不需要,当指定了allEntries为true时,Spring Cache将忽略指定的key。有的时候需要一次清除Cache中所有的元素,比一个一个清除元素更有效率。


@RequestMapping


2、beforeInvocation属性

清除操作默认是在对应方法成功执行之后触发的,即方法如果因为抛出异常而未能成功返回时也不会触发清除操作。使用beforeInvocation可以改变触发清除操作的时间,当指定该属性值为true时,Spring会在调用该方法之前清除缓存中的指定元素。

@Cacheable、@CacheEvict、@CachePut三个注解非常灵活,满足了我们对数据缓存的绝大多数使用场景,并且使用起来非常的简单而又强大,在实际工作中我们可以灵活搭配使用。

三、Session 共享

A、Session简介

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

在互联网行业中用户量访问巨大,往往需要多个节点共同对外提供某一种服务。


redis缓存刷新js刷新页面 redis缓存前端页面_Redis_04


用户的请求首先会到达前置网关,前置网关根据负载均衡策略将请求分发到后端的服务器,就会出现第一次的请求会交给服务器A处理,下次的请求可能会是服务B处理,如果不做Session共享的话,就有可能出现用户在服务器A登录了,下次请求的时候到达服务器B又要求用户重新登录。

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

一般Tomcat有自带的Session共享功能,但是使用起来并不是很便利,官方也不推荐大规模的使用,最常见的方式就是使用Redis来实现后端服务的Session共享。

B、Spring Session

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

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

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

PS:Spring Session核心项目并不依赖于Spring框架,所以能够将其应用于不使用Spring框架的项目中。

四、Spring Session使用

A、相关依赖


<dependency>


B、Session配置


@Configuration


maxInactiveIntervalInSeconds,设置Session失效时间。仅仅需要以上两步Spring Boot分布式Session配置完成了。

C、测试验证

获取页面的请求地址,并把请求地址放入到Session中,并将结果返回。


@RequestMapping


获取Session中的请求中的Session Id和Key为message的信息封装返回。


@RequestMapping


使用Maven打包Spring Boot应用,mvn clean package -Dmaven.test.skip=true

通过java -jar redis-session-0.0.1.jar --server.port=9090,运行两份实例。


redis缓存刷新js刷新页面 redis缓存前端页面_redis缓存路由为空_05


redis缓存刷新js刷新页面 redis缓存前端页面_redis缓存刷新js刷新页面_06


1、访问8080应用


# 1、访问8080端口服务setSession
http://localhost:8080/setSession
返回
{
    "request Url":"http://localhost:8080/setSession"
}

# 2、访问8080端口服务getSession
http://localhost:8080/getSession
返回
{
    "sessionId": "5c4bc710-6f45-4c51-b519-ad2ba0fd52af",
    "message": "http://localhost:8080/setSession"
}

# 3、访问8080端口服务setSession
http://localhost:8080/setSession
返回
{
    "sessionId": "5c4bc710-6f45-4c51-b519-ad2ba0fd52af",
    "message": "http://localhost:8080/setSession"
}


说明url地址信息已经存入到Session中。


redis缓存刷新js刷新页面 redis缓存前端页面_Redis_07


2、访问9090应用(同浏览器)


# 1、访问9090端口服务getSession
http://localhost:9090/getSession
返回
{
    "sessionId": "5c4bc710-6f45-4c51-b519-ad2ba0fd52af",
    "message": "http://localhost:9090/setSession"
}


redis缓存刷新js刷新页面 redis缓存前端页面_服务器_08


通过对比发现,8080和9090服务返回的Session信息完全一致,说明已经实现了Session共享。

D、模拟登录

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

定义index方法,只有用户登录之后才能看到index content这条信息,否则提示请先登录。


@RequestMapping


添加login方法,登录成功后将用户信息存放到Session中。


@RequestMapping


1、访问8080应用


# 1、访问8080端口服务index
http://localhost:8080/index
返回
please login first

# 2、访问8080端口服务login
http://localhost:8080/login?userName=wish&password=pass
返回
login successful

# 3、访问8080端口服务index
http://localhost:8080/index
返回
index content


通过登录流程,说明已经可以查看到受限的资源了。

2、访问9090应用


# 1、访问9090端口服务index
http://localhost:9090/index
返回
index content


redis缓存刷新js刷新页面 redis缓存前端页面_缓存_09


直接返回了index content,并没有提示进行登录,这表明9090服务已经同步了用户的登录状态,达到了统一登录的目的。

PS:此测试只是简单模拟统一登录,实际生产中会以Filter的方式对登录状态进行校验。

E、Seesion说明

使用Redis作为Session共享之后的示意图。


redis缓存刷新js刷新页面 redis缓存前端页面_缓存_10


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

PS:https://github.com/spring-projects/spring-boot/issues/16871,2.1.5版本存在Bug,请使用其他版本。


redis缓存刷新js刷新页面 redis缓存前端页面_redis缓存刷新js刷新页面_11


五、总结

使用Spring Boot集成Redis非常的简单,仅需要几个配置即可完成,Spring Boot针对Redis的常用的场景都提供了相应的支持,满足了数据缓存的绝大多数使用场景。Spring Session提供很多丰富的功能来管理Session,结合Spirng Boot使用可以很简单就实现Session共享。