单个应用的session应用
集群应用的Session共享
项目内引入spring-session-data-redis
,配合spring-boot-starter-data-redis
<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>
</dependency>
Redis
的httpSession
在启动类上方加上注解,启动SpringSession
管理应用的session
,并设置session
数据的有效期30分钟
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 30 * 60 * 1000)
spring:
redis:
database: 0
host: 192.168.161.3
password: 4rfv$RFV
port: 6379
@Controller
public class SessionController {
@RequestMapping(value="/uid",method = RequestMethod.GET)
public @ResponseBody
String uid(HttpSession session) {
return "sessionId:" + session.getId();
}
}
-Dserver.port=8889 -Dserver.httpPort=89 -Dspring.profiles.active=dev -Ddebug
-Dserver.port=8888 -Dserver.httpPort=88 -Dspring.profiles.active=dev -Ddebug
依次访问,看看效果.通过返回值session.getId()
即:sessionid
来判断,如果sessionid
一致,则证明session
共享成功了。
用浏览器访问下面的地址,自己看一下效果,再理解一下。
因为我们在同一台机器上启动多个实例,ip相同所以session是共享的。如果你在不同的服务器上启动多个实例(IP)不同,你需要在应用前方加上负载均衡逆向代理才可以实现session共享。
spring-session简介、使用及实现原理
【第一篇】Spring-Session实现Session共享入门教程
在我们写Java程序的时候,多线程争取同一个资源的时候,经常会使用到诸如syncchronize或Lock来实现锁操作,这种锁通常被称为“本地锁”。但是本地锁只能适用于在同一个进程内(同一个应用内的线程之间锁定资源),如果应用是分布式部署的,彼此之间是独立的进程,进程之间又存在需要争夺的资源,那么该如何对资源进行锁定?这就需要使用到分布式锁。
其实分布式锁和本地锁的基本原理是一样的,举个例子:上厕所
上面的逻辑可以使用下面的代码来体现。
@Resource
RedisTemplate<String, String> redisTemplate;
public void updateUserWithRedisLock(SysUser sysUser) throws InterruptedException {
// 占分布式锁,去redis占坑
Boolean lock = redisTemplate.opsForValue()
.setIfAbsent("SysUserLock" + sysUser.getId(),
"value");
if(lock) {
//加锁成功... 执行业务
redisTemplate.delete("SysUserLock" + sysUser.getId()); //删除key,释放锁
} else {
Thread.sleep(100); // 加锁失败,重试
updateUserWithRedisLock(sysUser);
}
}
setIfAbsent
方法的作用是在某一个lock key
不存在的时候,才能返回true
;如果这个key
已经存在了就返回false
,返回false
就是获取锁失败。setIfAbsent
函数功能类似于redis
命令行setnx
。
这个问题形成的原因就是程序在获取到锁之后,执行业务的过程中出现了异常,导致锁没有被释放。通俗的话说:上厕所的人死在了厕所里面,导致“坑位”资源死锁无法被释放。(当然这种情况出现的概率很小,但概率小不等于不存在。)
解决方案: 为redis的key设置过期时间,程序异常导致的死锁,在到达过期时间之后锁自动释放。也就说厕所门是电子锁,锁定的最长时间是有限制的,超过时长锁就会自动打开释放"坑位"资源。
// 设置过期时间
redisTemplate.expire("SysUserLock" + sysUser.getId(), timeout: 30, TimeUnit.SECONDS) ;
上文中我们虽然获取到锁,也设置了过期时间,看似完美。但是在高并发的场景下仍然会出问题,因为“获取锁”与“设置过期时间”是两个redis操作,两个redis操作不是原子性的。
可能出现这种情况:就在获取锁之后,设置过期时间之前程序宕机了。锁被获取到了但没有设置过期时间,最后又成为死锁。
解决方案
: 获取锁的同时设置过期时间
// 1. 分布式锁占坑
Boolean lock = redisTemplate.opsForValue().setIfAbsent("SysUserLock" + sysUser.getId(), "value", 30, TimeUnit.SECONDS);
这个问题出现的场景是:假如某个应用集群化部署存在多个进程实例,实例A、实例B。实例A获取到锁,但是执行过程超时了(数据库层面或其他层面导致操作执行超时)。超时之后锁被自动释放了,实例B获取到锁,并执行业务程序,执行完成之后把锁删除了。
解决方案: 在释放锁之前判断一下,这把锁是不是自己的那一把,如果是别人的锁你就不要动。怎么判断这把锁是不是自己的?加锁时为value赋随机值,加锁的随机值等于解锁时的获取到的值,才能证明这把锁是你的。代码如下:
大家仔细看代码,锁的释放时三个操作,这三个操作不是原子性的。也就是说在高并发的场景下,你刚get到的redis key有可能也被别的线程get了,你刚要删除别的线程可能已经把这个key删除了。
为了解决这个问题,我们可以使用redis lua脚本(lua脚本是在一个事务里面执行的,可以保证原子性)。在Java代码中可以以字符串的形式存在。
String script =
"if redis.call('get', KEYS[1]) == ARGV[1]
then return redis.call('del', KEYS[1])
else
return 0
end";
上面我们分析了很多使用redis实现分布式锁可能出现的问题及解决方案,其实在实际的开发应用中还会有更多的问题。比如:
所以实现一个分布式锁,不是我们想的那么简单,在高并发的环境下需要考虑的问题会复杂得多。怎么办?实际上分布式锁的细节时间有很多的现成的解决方案,不用我们去自己实现。比较完整优秀的分布式锁实现包括:
对比:
前提项目里面已经正确的集成了spring-boot-starter-data-redis
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-redis</artifactId>
</dependency>
@Configuration
public class RedisLockConfig {
@Bean
public RedisLockRegistry redisLockRegistry(RedisConnectionFactory redisConnectionFactory) {
//第一个参数redisConnectionFactory
//第二个参数registryKey,分布式锁前缀,设置为项目名称会好些
//该构造方法对应的分布式锁,默认有效期是60秒.可以自定义
return new RedisLockRegistry(redisConnectionFactory, "boot-launch");
//return new RedisLockRegistry(redisConnectionFactory, "boot-launch",60);
}
}
@Resource
private RedisLockRegistry redisLockRegistry;
public void updateUser(String userId) {
String lockKey = "config" + userId;
Lock lock = redisLockRegistry.obtain(lockKey); //获取锁资源
try {
lock.lock(); //加锁
//这里写需要处理业务的业务代码
} finally {
lock.unlock(); //释放锁
}
}
org.springframework.integration.redis.util.RedisLockRegistry的核心源码非常简单,就RedisLockRegistry这一个类。源码我就不贴在这里了,我给大家总结一下要点:
现在有很多的博文里面给出了一种非常简单的实现,就是在方法上面加注解,比如:
@RedisLock("lock-key")
public void save(){
}
这种实现使用上非常简单,但是笔者不建议使用这种方式,有几个原因
Redisson是Redis官方推荐的Java版的Redis客户端(Jedis、letture也是官方推荐的java版本redis客户端程序)。它提供的功能非常多,也非常强大,特别是它默认提供的分布式锁支持功能。其github源码仓库地址:https://github.com/redisson/redisson,包含多个子项目,对于我们本节比较有用的是
也就是说我们可以使用redisson无缝、无损的替换Spring Boot 2.x官方默认支持的redis客户端letture。也就是说我们之前学过的RedisTemplate、Redis Repository、Cache缓存该怎么用还怎么用,不受影响。
但是需要说明的是Redisson并不在Spring Boot官方默认支持的redis客户端的范围之内,所以redisson向Spring Boot 或者 Spring Data的集成方案,都是由redisson自己来维护的。
先从IDEA-maven管理Tab中查看,要确保自己的项目里面已经引入了下图所示的spring-boot-starter-data-redis。如何集成spring-boot-starter-data-redis
如上所示,我们使用的是spring data 2.2.4版本,所以artifactId为redisson-spring-data-22。如果你使用的其他的版本,以此类推。
<dependency>
<groupId>org.redisson</groupId>
<!-- for Spring Data Redis v.2.2.x -->
<artifactId>redisson-spring-data-22</artifactId>
<version>3.15.0</version>
</dependency>
除此之外,还要加入核心jar包redisson-spring-boot-starter,我们使用的是3.15.0版本,其默认包含redisson-spring-data-23,和我们Spring Data Redis v.2.2.x不匹配,所以我们用exclusion把它排除掉。
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.15.0</version>
<exclusions>
<exclusion>
<groupId>org.redisson</groupId>
<!-- 默认是 Spring Data Redis v.2.3.x ,所以排除掉-->
<artifactId>redisson-spring-data-23</artifactId>
</exclusion>
</exclusions>
</dependency>
下面的这一步从我的实验来看,可做可不做,不影响。但是既然我们使用redisson替换lettuce,就不要把lettuce的jar留在项目里面了,把它也排除掉。
redisson-spring-boot-starter默认支持application全局配置文件,redis配置以前怎么配置,现在还怎么配置,把lettuce段的配置去掉就可以了。
首先把全局配置文件中spring.redis下面的配置全都删除掉,然后加上redisson独立配置文件的指向位置及文件名称
spring:
redis:
redisson:
file: classpath:redisson.yaml
在resource目录下新建一个文件redisson.yaml,比如:redis单例模式的配置方法如下:
singleServerConfig:
idleConnectionTimeout: 10000
connectTimeout: 10000
timeout: 3000
retryAttempts: 3
retryInterval: 1500
password: 123456
subscriptionsPerConnection: 5
clientName: null
address: "redis://192.168.161.3:6379"
subscriptionConnectionMinimumIdleSize: 1
subscriptionConnectionPoolSize: 50
connectionMinimumIdleSize: 32
connectionPoolSize: 64
database: 0
dnsMonitoringInterval: 5000
threads: 0
nettyThreads: 0
codec: !<org.redisson.codec.JsonJacksonCodec> {}
transportMode: "NIO"
大家可以看到第二种配置方案比第一种配置方案,多出很多细节方面的配置,更适合有经验的高手进行性能优化使用。
仍然是老套路,获取锁、上锁锁定、业务代码执行完成释放锁。
@Resource
private RedissonClient redissonClient;
public void updateUser(String userId) {
String lockKey = "config" + userId;
RLock lock = redissonClient.getLock(lockKey); //获取锁资源
try {
lock.lock(10, TimeUnit.SECONDS); //加锁,可以指定锁定时间
//这里写需要处理业务的业务代码
} finally {
lock.unlock(); //释放锁
}
}
上面的用法可以满足你绝大部分的分布式锁的业务场景,更多的用法参考官方wiki:
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.csdn.net/m0_53157173/article/details/121707857
内容来源于网络,如有侵权,请联系作者删除!