老鬼的博客 来都来啦,那就随便看看吧~
springboot整合redisson做分布式锁
发布于: 2023-02-28 更新于: 2023-02-28 分类于:  阅读次数: 

一:redisTemplate.opsForValue().setIfAbsent问题

1.1 说明

1
2
3
4
5
6
7
当自己直接使用redisTemplate.opsForValue().setIfAbsent进行获取锁的
方法,当并发量不是很大的时候,这里的还是能使用的,不过一般作为生产
的分布式锁不建议这么使用,一旦超高并发就会存在各种问题。主要是考虑
如下几点:
1.设置锁超时时间
2.设置clientId,保证谁锁定的谁去释放
3.finally 确保可以释放锁

1.2 测试代码

1
2
3
如何代码在超高并发的时候也是存在问题的,如果在timeout时间外第一个线程
尚未执行完毕,第二个线程拿到锁以后就可以继续往后面执行,如果timeout设置
的过长,如果一些特殊原因,比如应用重启等会导致一直被锁定。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
package com.tohours.miniprogram.controller;

import java.util.UUID;
import java.util.concurrent.TimeUnit;

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.tohours.miniprogram.exception.TohoursException;
import com.tohours.miniprogram.support.TohoursResponse;
import com.tohours.miniprogram.support.TohoursResult;

import lombok.extern.slf4j.Slf4j;

/**
* @desc redis相关
* @author RenJie
* @date 2023-02-27
*
*/
@RestController
@RequestMapping("/redis")
@SuppressWarnings("rawtypes")
@Slf4j
public class RedisController extends BaseController {

@Autowired
private StringRedisTemplate stringRedisTemplate;
private Boolean isFirst = true;

/**
* @desc redis lock测试
* @return
*/
@RequestMapping(value = "testLock")
public TohoursResult<Object> testLock() {

String prizeNumKey = "prizeNum1";
String lockKey = "prize_1";
String clientId = UUID.randomUUID().toString();
Long timeout = 30L;
// 获取锁
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, timeout, TimeUnit.SECONDS);
if (!lock) {// 未获取到锁
log.info("未获取到锁,结束");
throw new TohoursException("未获取到锁,结束");
}
try {
String tmp = stringRedisTemplate.opsForValue().get(prizeNumKey);
if (isFirst) {
tmp = "300";
isFirst = false;
}
int prize1Num = Integer.parseInt(tmp);
if (prize1Num > 0) {
prize1Num--;
stringRedisTemplate.opsForValue().set(prizeNumKey, String.valueOf(prize1Num));
log.info(Thread.currentThread().getName() + "剩余数量:" + prize1Num);
}

} finally {
// 释放锁
String cacheClientId = stringRedisTemplate.opsForValue().get(lockKey);
if (clientId.equals(cacheClientId)) {// 谁锁定得谁释放
stringRedisTemplate.delete(lockKey);
}
}
return TohoursResponse.success();
}

}

二:使用redssion做分布式锁

2.1 介绍

1
2
3
4
5
6
redis自动2.6版本以后支持了Lua语言,redission是在redis基础操作的基础上,
做了超时后自动延迟超时时间,并把基础的超时时间设置,线程ID设置,释放锁
等都统一做了处理,所以代码里面我们只要引入使用即可。

如果线程A获取到了锁,正在执行任务,尚未释放锁,此时线程B也去获取锁,
如果获取锁失败,则会等待锁的释放。

2.2 springboot引入redssion

  • pom.xml
1
2
3
4
5
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.19.3</version>
</dependency>
  • redisson配置类
1
在基于引入redis的基础上做的配置,此距离是单例模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.tohours.miniprogram.config;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* @desc redisson bean管理
* @author RenJie
* @date 2023-02-28
*/
@Configuration
public class RedissonConfig {

@Value("${spring.redis.host}")
private String host;

@Value("${spring.redis.port}")
private String port;

@Value("${spring.redis.password}")
private String password;

@Bean
public RedissonClient getRedisson() {
Config config = new Config();
config.useSingleServer().setAddress("redis://" + host + ":" + port).setPassword(password);
// 添加主从配置
// config.useMasterSlaveServers().setMasterAddress("").setPassword("").addSlaveAddress(new String[]{"",""});
return Redisson.create(config);
}
}

2.3 使用redisson分布式锁

  • 说明
1
2
3
4
1.redisson首先获取lock实例
2.加锁
3.释放锁
4.当线程未获取到锁的时候会等待
  • 案例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
package com.tohours.miniprogram.controller;

import java.util.UUID;
import java.util.concurrent.TimeUnit;

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.tohours.miniprogram.exception.TohoursException;
import com.tohours.miniprogram.support.TohoursResponse;
import com.tohours.miniprogram.support.TohoursResult;

import lombok.extern.slf4j.Slf4j;

/**
* @desc redis相关
* @author RenJie
* @date 2023-02-27
*
*/
@RestController
@RequestMapping("/redis")
@SuppressWarnings("rawtypes")
@Slf4j
public class RedisController extends BaseController {

@Autowired
private StringRedisTemplate stringRedisTemplate;
private Boolean isFirst = true;

/**
* @desc redis lock测试
* @return
*/
@RequestMapping(value = "testLock")
public TohoursResult<Object> testLock() {

String prizeNumKey = "prizeNum1";
String lockKey = "prize_1";
String clientId = UUID.randomUUID().toString();
Long timeout = 30L;
// 获取锁
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, timeout, TimeUnit.SECONDS);
if (!lock) {// 未获取到锁
log.info("未获取到锁,结束");
throw new TohoursException("未获取到锁,结束");
}
try {
String tmp = stringRedisTemplate.opsForValue().get(prizeNumKey);
if (isFirst) {
tmp = "300";
isFirst = false;
}
int prize1Num = Integer.parseInt(tmp);
if (prize1Num > 0) {
prize1Num--;
stringRedisTemplate.opsForValue().set(prizeNumKey, String.valueOf(prize1Num));
log.info(Thread.currentThread().getName() + "剩余数量:" + prize1Num);
}

} finally {
// 释放锁
String cacheClientId = stringRedisTemplate.opsForValue().get(lockKey);
if (clientId.equals(cacheClientId)) {// 谁锁定得谁释放
stringRedisTemplate.delete(lockKey);
}
}
return TohoursResponse.success();
}

@Autowired
private RedissonClient resissonClient;

/**
* @desc redisson Lock测试
* @return
*/
@RequestMapping(value = "redissonLock")
public TohoursResult<Object> redissonLock() {

String prizeNumKey = "prizeNum1";
String lockKey = "prize_1";
// 获取锁
RLock redissonLock = resissonClient.getLock(lockKey);
redissonLock.lock();// 锁定
try {
String tmp = stringRedisTemplate.opsForValue().get(prizeNumKey);
if (isFirst) {
tmp = "300";
isFirst = false;
}
int prize1Num = Integer.parseInt(tmp);
if (prize1Num > 0) {
prize1Num--;
stringRedisTemplate.opsForValue().set(prizeNumKey, String.valueOf(prize1Num));
log.info(Thread.currentThread().getName() + "剩余数量:" + prize1Num);
}

} finally {
// 释放锁
redissonLock.unlock();
}
return TohoursResponse.success();
}

}

2.4 读写锁

  • 介绍
1
2
3
读写锁: 保证一定能读到最新数据,修改期间,写锁是一个排他锁(互斥锁)。
读锁是一个共享锁。 写锁没释放,读锁就必须等待。
针对读数据时,只要当前正在修改数据,读数据就会等待写完成后,拿到最新数据。
  • 多场景测试
1
2
3
4
5
读 + 读 :相当于无所 n请求可以同时加锁成功 redis中会记录着进来锁的状态
写 + 读 :等待写锁释放 才能读到新数据(读被阻塞)
写 + 写 : 阻塞方式(后面写锁等待前面写锁释放)
读 + 写 :有读锁 写也需要等待(写锁:等你读完这一次 我再改写)
只要有写锁的存在,都必须等待。
  • 测试案例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//获取读写锁
RReadWriteLock readWriteLock = resissonClient.getReadWriteLock(lockKey);
//获取读锁
RLock rLock = readWriteLock.readLock();
//加锁
rLock.lock();
//释放锁
rLock.unlock();
//获取写锁
rLock = readWriteLock.writeLock();
//加锁
rLock.lock();
//释放锁
rLock.unlock();
*************感谢您的阅读*************