一、介绍:
在web应用中,我们经常会用session来保存已登录用户的相关信息,在单机应用中,由于所有的用户都访问同一个应用,而session都保存在此单机应用中所以并无不妥。但是随着用户并发量的上升,分布式系统势在必行,这就导致一个用户的访问请求可能会分发到不同的集群部署应用上处理,此时在某个应用上创建session存储信息可能换一个应用就找不到了。解决方法:
1、当创建一份session时,给集群内所有应用都复制一份,很显然这种方法是很占用网络带宽和内存的;
2、利用负载均衡策略中的一致性hash,将同一个客户的请求都分发到同一个应用中,因此应用总能找到此次用户连接对应的session,例如nginx就可以配置ip_hash实现此功能
3、基于分布式全局缓存的全局session,例如我们可以利用redis、memcached全局缓存来存储session信息,以sessionId为key,当应用需要用到session数据时统一从全局缓存中获取。这种方法是比较实用的。
二、代码实现:
1、配置文件配置:这种方式有个缺点,当项目中也需要使用redis时,既要配置redis信息又要配置sesison共享的redis信息,相当于需要配置两次redis信息。
(1)pom:
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session</artifactId>
</dependency>
(2)application.properties配置文件加上以下配置:
单机模式
spring.redis.host=xxx.xx.xxx.xx
spring.redis.port=你的redis端口号
spring.redis.password=xxxxxx
#以下为redis的其他配置
spring.redis.jedis.pool.maxTotal=500
spring.redis.jedis.pool.maxIdle=10
spring.redis.jedis.pool.maxWaitMillis=5000
spring.redis.jedis.pool.min-idle=5
spring.redis.timeout=5000
redis集群模式:
#session共享
spring.redis.sentinel.master = my-master
spring.redis.sentinel.nodes = xxx.xx.xxx:19601,xxx.xx.xxx:19601,xxx.xx.xxx:19601
spring.redis.password = ******
#不指定默认是redis第0个数据库
spring.redis.database = 6
spring.redis.timeout = 15000
spring.redis.jedis.pool.max-active = 8
2、代码配置:session存储的转换成redis存储。pom不需要依赖了。
(1)配置文件:
server.port=9999
server.context-path=/redisgroup
my.redis.server.jedis.pool.maxTotal=500
my.redis.server.jedis.pool.maxIdle=10
my.redis.server.jedis.pool.maxWaitMillis=5000
my.redis.server.jedis.pool.min-idle=5
my.redis.server.timeout=5000
my.redis.sentinel.nodes=xxx.xx.xxx:19601,xxx.xx.xxx:19601,xxx.xx.xxx:19601
my.redis.sentinel.password=******
my.redis.sentinel.master-name=master-test
my.redis.sentinel.database=10
my.redis.sentinel.pool.max-total=10
my.redis.sentinel.pool.max-idle=5
my.redis.sentinel.pool.min-idle=5
(2)redis配置
package com.demo.config;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.client.codec.Codec;
import org.redisson.codec.JsonJacksonCodec;
import org.redisson.config.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.support.collections.RedisProperties;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.JedisSentinelPool;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
@Configuration
public class JedisConfig {
private Logger logger = LoggerFactory.getLogger(JedisConfig.class);
/*@Value("${my.redis.server.host}")
private String host;
@Value("${my.redis.server.port}")
private int port;
@Value("${my.redis.server.password}")
private String password;*/
@Value("${my.redis.server.jedis.pool.maxTotal}")
private int maxTotal;
@Value("${my.redis.server.jedis.pool.maxIdle}")
private int maxIdle;
@Value("${my.redis.server.jedis.pool.maxWaitMillis}")
private int maxWaitMillis;
@Value("${my.redis.server.timeout}")
private int timeout;
@Value("${my.redis.sentinel.nodes}")
private String redisSentinelNodes;
@Value("${my.redis.sentinel.pool.max-total}")
private int redisSentinelMaxTotal;
@Value("${my.redis.sentinel.pool.max-idle}")
private int redisSentinelMaxIdle;
@Value("${my.redis.sentinel.pool.min-idle}")
private int redisSentinelMinIdle;
@Value("${my.redis.sentinel.master-name}")
private String redisSentinelMasterName;
@Value("${my.redis.sentinel.password}")
private String redisSentinelPassword;
@Value("${my.redis.sentinel.database}")
private int dataBase;
@Bean(name = "jedisPool")
public JedisSentinelPool jedisPool() {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(maxTotal);
jedisPoolConfig.setMaxIdle(maxIdle);
jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
//sentinel
String[] hosts = redisSentinelNodes.split(",");
Set<String> sentinels = new HashSet<>(Arrays.asList(hosts));
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
poolConfig.setMaxTotal(redisSentinelMaxTotal);
poolConfig.setMaxIdle(redisSentinelMaxIdle);
poolConfig.setMinIdle(redisSentinelMinIdle);
JedisSentinelPool jedisSentinelPool = new JedisSentinelPool(redisSentinelMasterName,
sentinels, jedisPoolConfig,timeout,redisSentinelPassword,dataBase);
return jedisSentinelPool;
}
/* @Bean
public RedissonClient redissonClient(){
Config config = new Config();
// 使用json序列化方式
Codec codec = new JsonJacksonCodec();
config.setCodec(codec);
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}*/
}
(3)redis客户端:
package com.demo.config;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisSentinelPool;
import redis.clients.jedis.Tuple;
@SuppressWarnings("unused")
@Component
public class RedisClient {
private static boolean BORROW = true; // 在borrow一个事例时是否提前进行validate操作
private static Logger logger = Logger.getLogger(RedisClient.class);
@Autowired
private JedisSentinelPool pool;
/**
* 获取连接
*/
public synchronized Jedis getJedis() {
try {
if (pool != null) {
return pool.getResource();
} else {
return null;
}
} catch (Exception e) {
logger.info("连接池连接异常");
return null;
}
}
/**
* @Description: 关闭连接
* @param @param jedis
* @return void 返回类型
*/
public static void getColse(Jedis jedis) {
if (jedis != null) {
jedis.close();
}
}
/**
* 格式化Key
*/
public static String format(String formatKey, String... keyValues) {
if (keyValues == null || keyValues.length == 0) {
return formatKey;
}
StringBuilder key = new StringBuilder();
char[] chars = formatKey.toCharArray();
int index = -1;
boolean inmark = false;
boolean firstinmark = false;
for (int i = 0; i < chars.length; i++) {
char ch = chars[i];
if (ch == '{') {
index++;
inmark = true;
firstinmark = true;
} else if (ch == '}') {
inmark = false;
} else if (inmark) {
if (firstinmark) {
firstinmark = false;
key.append(keyValues[index]);
}
} else {
key.append(chars[i]);
}
}
return key.toString();
}
/********************************** 省略其他操作************************************/
}
(4)session操作:
package com.demo.config;
import com.alibaba.fastjson.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;
@SuppressWarnings("unused")
@Component
public class SessionClient {
@Autowired
private RedisClient jedis;
/**
* 根据key获取session中的值
* @param sessionId
* @param key
* @return
*/
public Object getSessionValue(String sessionId,String key){
if(jedis.exists(sessionId)){
String sessionStr = jedis.get(sessionId);
JSONObject session = JSONObject.parseObject(sessionStr);
return session.get(key);
}
return null;
}
/**
* 往session中放入entry
* @param sessionId
* @param key
* @param value
* @param expireTime 超时时间
*/
public void setSessionValue(String sessionId,String key,String value,int expireTime){
if(jedis.exists(sessionId)){
String sessionStr = jedis.get(sessionId);
JSONObject session = JSONObject.parseObject(sessionStr);
session.put(key, value);
jedis.set(sessionId, session.toString());
jedis.expire(sessionId, expireTime);
}else{
JSONObject session = new JSONObject();
session.put(key, value);
jedis.set(sessionId, session.toString());
jedis.expire(sessionId, expireTime);
}
}
/**
* 根据sessionid查询全局session是否存在
* @param sessionId
* @return
*/
public boolean isExistSession(String sessionId){
try{
return jedis.exists(sessionId);
}finally{
}
}
/**
* 根据sessionId删除session
* @param sessionId
*/
public void removeSession(String sessionId){
try{
jedis.del(sessionId);
}finally {
}
}
public void createSession(){
}
/**
* 获取JsessionId
* @param request
* @return
*/
public static String getJsessionId(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
// 从Cookie数据中遍历查找 并取CSESSIONID
if (null != cookies && cookies.length > 0) {
for (Cookie cookie : cookies) {
if ("JSESSIONID".equals(cookie.getName())) {
// 有 直接返回
return cookie.getValue().toString();
}
}
}
return null;
}
public static String addCookie(HttpServletRequest request, HttpServletResponse response, Integer KEY_EXPIRE_TIME) {
String jsessionId = getJsessionId(request);
if (jsessionId == null || "".equals(jsessionId)) {
jsessionId = UUID.randomUUID().toString().replaceAll("-", "").toUpperCase();
}
Cookie cookie = new Cookie("CSESSIONID", jsessionId);
cookie.setPath("/");
cookie.setMaxAge(KEY_EXPIRE_TIME);
response.addCookie(cookie);
return jsessionId;
}
}
(5)接口:
package com.demo.controller;
import com.demo.config.RedisClient;
import com.demo.config.SessionClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
@RestController
@RequestMapping("index")
public class IndexController {
@Autowired
private RedisClient redisClient;
@Autowired
private SessionClient sessionClient;
private String nameFormat = "redis:name:{name}";
//key为常量+userId
private String sessionKey = "login:sessionId:%s";
@RequestMapping("/setName")
public void setName(String key,String value){
redisClient.set(nameFormat,value,key);
}
@RequestMapping("/getName")
public String getName(String key){
return redisClient.get(nameFormat,key);
}
@RequestMapping("/sessionSetName")
public void sessionSetName(HttpServletRequest request,String key,String value){
String userId = "zhangsan";
String sessionId = String.format(sessionKey,userId);
sessionClient.setSessionValue(sessionId,key,value,300);
}
@RequestMapping("/sessionGetName")
public String sessionGetName(HttpServletRequest request,String key){
String userId = "zhangsan";
String sessionId = String.format(sessionKey,userId);
return (String)sessionClient.getSessionValue(sessionId,key);
}
}
注意:和mysql、redis一样,如果没有配置指定地址,则会默认连接到本地(官方默认配置),如redis会连接本地的localhost:6379的redis。
三、demo:
1、配置文件配置:新建两个工程,一个部署在本地8080端口,一个部署在本地9090端口,两个配置到一个redis上。
8080端口工程:
pom:
<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">
<modelVersion>4.0.0</modelVersion>
<groupId>com.demo</groupId>
<artifactId>sessionRedis</artifactId>
<version>0.0.1-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.4.1.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.21</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.33</version>
</dependency>
</dependencies>
</project>
controller接口
package com.demo.controller;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import com.demo.module.Message;
import com.demo.module.User;
import com.demo.service.UserService;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/login")
public Message login(String name, String pwd,HttpServletRequest req) {
User user = userService.select(name, pwd);
Message message = new Message();
if (user != null) {
message.setCode(200);
message.setRes(true);
message.setMsg("登录成功");
HttpSession session = req.getSession();
session.setAttribute("user", user.toString());
} else {
message.setCode(500);
message.setRes(false);
message.setMsg("登录失败");
}
System.out.println("message"+message);
return message;
}
@RequestMapping("/main")
public String main(HttpServletRequest req){
HttpSession session = req.getSession();
System.out.println("请求"+session.getId());
String user = (String) session.getAttribute("user");
System.out.println("8080当前登录用户为"+user);
return session.getId();
}
// @RequestMapping(value = "/first", method = RequestMethod.GET)
// public Map<String, Object> firstResp (HttpServletRequest request){
// Map<String, Object> map = new HashMap<>();
// request.getSession().setAttribute("request Url", request.getRequestURL());
// map.put("request Url", request.getRequestURL());
// return map;
// }
//
// @RequestMapping(value = "/sessions", method = RequestMethod.GET)
// public Object sessions (HttpServletRequest request){
// Map<String, Object> map = new HashMap<>();
// map.put("sessionId", request.getSession().getId());
// map.put("message", request.getSession().getAttribute("map"));
// return map;
// }
}
9090端口工程:
pom:
<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">
<modelVersion>4.0.0</modelVersion>
<groupId>com.demo</groupId>
<artifactId>sessionRedis1</artifactId>
<version>0.0.1-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.4.1.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.21</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.33</version>
</dependency>
</dependencies>
</project>
两个项目的配置文件只有端口号不一样。
package com.demo.controller;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/test")
public class TestController {
@RequestMapping("/test")
public String test(HttpServletRequest request){
HttpSession session = request.getSession();
String user = (String) session.getAttribute("user");
System.out.println("9090当前登录用户为"+user);
return request.getSession().getId();
}
}
测试:
8080上登录:
8080上获取用户:
9090上获取用户:
可以看到,两个sessonId是一样的,获取的session是一致的。
2、代码配置:修改端口号分别为7777、9999,各启动一次。
访问localhost:9999/redisgroup/index/sessionSetName?value=wtyy999&key=mytest
查看redis
分别访问localhost:7777/redisgroup/index/sessionGetName?key=mytest、
localhost:9999/redisgroup/index/sessionGetName?key=mytest返回的结果一致: