文章内容
本文使用windows基于主从复制的方式搭建redsi集群,使用springboot和redisson包实现分布式锁,并进行了模拟测试。
Redis集群环境搭建
redis版本5.0.14.1
将zip包解压缩,然后复制六份。这里不能在一个文件夹里面启动多次,因为这样会导致redis的数据会持久化到同一个文件当中,所以需要复制六个文件夹。当然,如果你不觉得麻烦可以配置六次配置文件,修改他们持久化的文件。
修改redsi.window.conf文件中的相关配置,如下
这个是节点相关的配置
cluster-config-file nodes-6375.conf
超时的设置
cluster-node-timeout 15000
端口号
port 6375
如果需要密码的话可以放开
requirepass foobared
然后依次启动六个节点,注意要先把这六个节点全部启动起来,并且刚开始配置的时候里面不能有数据,必须保持一致。
启动命令:redis-server.exe [你的配置文件]
然后下面是最关键的一步,部署集群,也就是将所有的节点关联起来而不是向刚才那样各自独立:
redis-cli --cluster create 127.0.0.1:6379 127.0.0.1:6378 127.0.0.1:6377 127.0.0.1:6376 127.0.0.1:6375 127.0.0.1:6374 --cluster-replicas 1 -a foobared
然后我们就可以登录某一个节点了
登录命令:redis-cli -h localhost -p 6374 -a foobared -c
,注意这个-c参数是集群环境下要加的,不然后面的操作会报错。
使用cluster nodes
可以看到所有的节点的主从状态。这六个节点是相互关联的,其中一个节点加入数据在其他的节点都能够查询到数据。其中一个节点删除其他节点的数据一样会删除。这里相关的原理暂不做过多的分析。
Redis实现分布式锁方案
首先项目中添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.4.0</version>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.9.1</version>
</dependency>
<dependency>
自动配置引入下面的依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.17.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.redisson/redisson-spring-data-20 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-data-26</artifactId>
<version>3.17.0</version>
</dependency>
配置文件的内容
spring:
redis:
# host: localhost
cluster:
nodes: localhost:6374,localhost:6375,localhost:6376,localhost:6377,localhost:6378,localhost:6379
password: foobared
port: 6379
database: 0
lettuce:
pool:
max-active: 8 #最大连接数据库连接数,设 -1 为没有限制
max-idle: 8 #最大等待连接中的数量,设 0 为没有限制
max-wait: -1ms #最大建立连接等待时间。如果超过此时间将接到异常。设为-1表示无限制。
min-idle: 0 #最小等待连接中的数量,设 0 为没有限制
shutdown-timeout: 100ms
timeout: 100000
这个配置文件的内容会被spring
读取并以RedisProperties
进行封装。所以我们这里进行Redis相关的配置如下:
@Component
@Data
@ToString(callSuper = true)
public class RedisConfig {
@Resource
RedisProperties redisProperties;
/**
* 配置redisson
* @return
*/
@Bean
public RedissonClient redisson() {
RedisProperties.Cluster cluster = redisProperties.getCluster();
String[] redisNodes = cluster.getNodes().stream().map(value -> ("redis://" + value)).toArray(String[]::new);
Config config = new Config();
ClusterServersConfig clusterServersConfig = config.useClusterServers()
.addNodeAddress(redisNodes);
clusterServersConfig.setPassword(redisProperties.getPassword());
return Redisson.create(config);
}
@Bean
@SuppressWarnings(value = {"unchecked", "rawtypes"})
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
serializer.setObjectMapper(mapper);
// 使用StringRedisSerializer来序列化和反序列化redis的key值
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(serializer);
// Hash的key也采用StringRedisSerializer的序列化方式
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(serializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
return new StringRedisTemplate(redisConnectionFactory);
}
}
下面我们对加锁和解锁进行封装,这里只是简单的演示,项目中还要自己进行封装。
@Component
public class RedisExcutor {
private static final String LOCK_PREFIX = "*******";
//这里注入的就是前面配置的bean,不然会注入失败
@Autowired
RedissonClient redissonClient;
/**
* 最终加强分布式锁
* @return 是否获取到
*/
public boolean lock() throws SQLException {
RLock lock = redissonClient.getLock(LOCK_PREFIX);
return lock.tryLock();
}
public void unlock() {
RLock lock = redissonClient.getLock(LOCK_PREFIX);
lock.unlock();
}
}
分布式锁测试
我们这里使用单机模拟多线程并发操作看看分布式锁是否生效,验证的代码如下
@RestController
@RequestMapping("/redis")
public class RedisController {
@Resource
RedisExcutor redisExcutor;
public volatile int a = 0;
public volatile int b = 0;
private int threadNum = 100;
@GetMapping("/testunlock")
public String testUnlock() throws SQLException, InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(threadNum);
CyclicBarrier cyclicBarrier = new CyclicBarrier(threadNum);
for (int i = 0; i < threadNum; i++) {
new Thread(() -> {
try {
cyclicBarrier.await();
for (int j = 0; j < 1000; j++) {
a++;
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (BrokenBarrierException e) {
throw new RuntimeException(e);
} finally {
countDownLatch.countDown();
}
}).start();
}
countDownLatch.await();
return "finally ,the value of a is " + a;
}
@GetMapping("/testlock")
public String testLock() throws SQLException, InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(threadNum);
CyclicBarrier cyclicBarrier = new CyclicBarrier(threadNum);
for (int i = 0; i < threadNum; i++) {
new Thread(()->{
try {
cyclicBarrier.await();
if (redisExcutor.lock()) {
try {
for (int j = 0; j < 1000; j++) {
b++;
}
Thread.sleep(1000);
} finally {
redisExcutor.unlock();
}
}
} catch (SQLException e) {
throw new RuntimeException(e);
} catch (BrokenBarrierException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
countDownLatch.countDown();
}
}).start();
}
countDownLatch.await();
return "finally ,the value of b is " + b;
}
@PostMapping("/clear")
public String zero() {
a = 0;
b = 0;
return "a = " + a + " and b = "+b;
}
}
这里我们模拟一百个请求并发执行,每次请求循环操作1000次。
首先执行清零把a和b的值清零
测试无锁请求,可以发现存在问题,数据并不是预期的数据100000.
测试加锁请求,由于代码中加了一个sleep因此基本上只有一个线程可以获取到锁,最后的值应该是1000。
这里遇到一个问题,有点时候会出现2000,但是这里并没有问题,而是我电脑性能太低导致实际上各个线程并没有同时执行,而是其中有一个线程获取到锁并释放了然后另一个线程获取到了导致的。实验验证如下,我们修改了原来的代码,把并发的请求数量增加到500
:
@GetMapping("/testlock")
public String testLock() throws SQLException, InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(threadNum);
CyclicBarrier cyclicBarrier = new CyclicBarrier(threadNum);
for (int i = 0; i < threadNum; i++) {
new Thread(()->{
try {
cyclicBarrier.await();
if (redisExcutor.lock()) {
Timestamp timestamp = new Timestamp(new Date().getTime());
System.out.println(timestamp+" 线程"+ Thread.currentThread().getName()+ "获取到了锁");
try {
for (int j = 0; j < 1000; j++) {
b++;
}
Thread.sleep(1000);
} finally {
redisExcutor.unlock();
}
timestamp = new Timestamp(new Date().getTime());
System.out.println(timestamp+" 线程"+ Thread.currentThread().getName()+ "获取到了锁");
}
} catch (SQLException e) {
throw new RuntimeException(e);
} catch (BrokenBarrierException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
countDownLatch.countDown();
}
}).start();
}
countDownLatch.await();
return "finally ,the value of b is " + b;
}
打印的结果如下:
这里说明请求是顺序执行的,是其中一个线程释放锁之后另一个线程才获取的。
countdownlatch和cyclicbarrier的区别
countdownlatch是所有线程执行完了之后才让主线程继续执行,这样我们上面代码中输出的a和b就是最终所有线程操作结束之后的值,而不是获取到线程执行过程中的值。
cyclicbarrier是为了让我们所有的线程同时执行,而不是随着我们主程序的执行一个个启动。
Redisson如何避免死锁
Redission
为了避免锁未被释放,采用了一个特殊的解决方案,若未设置过期时间的话,redission
默认的过期时间是30s,同时未避免锁在业务未处理完成之前被提前释放,Redisson
在获取到锁且默认过期时间的时候,会在当前客户端内部启动一个定时任务,每隔internalLockLeaseTime/3
的时间去刷新key
的过期时间,这样既避免了锁提前释放,同时如果客户端宕机的话,这个锁最多存活30s
的时间就会自动释放(刷新过期时间的定时任务进程也宕机)。
但是如果仅仅是锁过期了但所没有释放,那么只有当前线程可以继续获取锁,其他线程永远获取不到锁。