在大多数项目中,都会使用缓存中间件,例如Redis、Memcached等,一般会选择项目适配的中间件,能够提高数据的访问速度,减轻后端服务器和数据库的压力,但在使用缓存时,又不得不考虑其线程安全问题,特别是现在大多数项目都使用了分布式架构,因此就需要对中间件进行优化和加强。
Redisson是一个开源框架,它提供了一系列分布式数据结构和服务,如分布式锁、分布式集合、分布式对象等,使得在分布式环境中使用Redis变得更加简单和高效(后续会出一篇关于Redisson的原理和特性),下面就来说一说在系统中如何去使用Redisson分布式锁。
1.搭建一个Springboot项目,引入Redisson依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>shmily-demo-root</artifactId>
<groupId>alp.starcode</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>demo-redis</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--使用lettuce时,需要额外引入commons-pool2依赖包-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--redisson分布式锁-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
</dependency>
<!--Lombok依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
2.创建application.yml 、 application-multipart.yml、redisson.yml配置文件(application-multipart.yml配置文件创建是为了区分Redis集群和单节点的方式,Redisson的配置文件单独放在这个redisson中了)
spring:
profiles:
active: multipart #redis集群
#active: single #单redis节点
spring:
application:
name: demo-redis
redis:
pool:
max-idle: 100
min-idle: 1
max-active: 1000
max-wait: -1
timeout: 100000
cluster:
nodes:
#地址要和redis配置中bind地址一致
- 192.168.9.128:6379
- 192.168.9.128:6380
- 192.168.9.128:6381
- 192.168.9.128:6382
- 192.168.9.128:6383
- 192.168.9.128:6384
#jedis:
#springboot2.0以上的版本默认使用的是lettuce redis客户端,如果想使用jedis客户端,则需把lettuce依赖进行排除以及手动引入jedis依赖
lettuce:
pool:
max-active: 8 #连接池最大连接数(使用负值表示没有限制)
max-wait: -1 # 连接池中的最大空闲连接
max-idle: 500 # 连接池最大阻塞等待时间(使用负值表示没有限制)
min-idle: 0 # 连接池中的最小空闲连接
database: 0
port: 6379
#集群配置
clusterServersConfig:
# 连接空闲超时 如果当前连接池里的连接数量超过了最小空闲连接数,而同时有连接空闲时间超过了该数值,那么这些连接将会自动被关闭,并从连接池里去掉。时间单位是毫秒。
idleConnectionTimeout: 10000
pingTimeout: 1000
# 连接超时
connectTimeout: 10000
# 命令等待超时
timeout: 3000
# 命令失败重试次数
retryAttempts: 3
# 命令重试发送时间间隔
retryInterval: 1500
# 重新连接时间间隔
reconnectionTimeout: 3000
# failedAttempts
failedAttempts: 3
# 密码
#password: redis
password:
# 单个连接最大订阅数量
subscriptionsPerConnection: 5
# 客户端名称
clientName: null
#负载均衡算法类的选择 默认轮询调度算法RoundRobinLoadBalancer
loadBalancer: !<org.redisson.connection.balancer.RoundRobinLoadBalancer> {}
slaveSubscriptionConnectionMinimumIdleSize: 1
slaveSubscriptionConnectionPoolSize: 50
# 从节点最小空闲连接数
slaveConnectionMinimumIdleSize: 32
# 从节点连接池大小
slaveConnectionPoolSize: 64
# 主节点最小空闲连接数
masterConnectionMinimumIdleSize: 32
# 主节点连接池大小
masterConnectionPoolSize: 64
# 只在从服务节点里读取
readMode: "SLAVE"
# 主节点信息
nodeAddresses:
- "redis://192.168.9.128:6380"
- "redis://192.168.9.128:6381"
- "redis://192.168.9.128:6382"
- "redis://192.168.9.128:6383"
- "redis://192.168.9.128:6384"
- "redis://192.168.9.128:6379"
#集群扫描间隔时间 单位毫秒
scanInterval: 1000
threads: 0
nettyThreads: 0
codec: !<org.redisson.codec.JsonJacksonCodec> {}
3.添加配置类 RedisConfig 和工具类 RedisUtil、SpringContextUtils
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.io.IOException;
import java.io.Serializable;
@Configuration
@EnableCaching
public class RedisConfig {
/**
* 这是redis分布式锁的配置 1.有两个配置文件一个redisson.yml 和一个redisson-single.yml 前者是集群环境,后者是单个redis的配置(也就是没有配置集群的情况下使用)
* @return
* @throws IOException
*/
@Bean(value = "redisson",name = "redisson",destroyMethod="shutdown")
RedissonClient redisson() throws IOException {
//1、创建配置
// Config config = new Config();
// config.useSingleServer().setAddress("172.24.17.119:6371").setPassword(redisPassword);
Config config = Config.fromYAML(new ClassPathResource("redisson.yml").getInputStream());
//
//
// Config config = Config.fromYAML(new ClassPathResource(configurationFiles).getInputStream());
return Redisson.create(config);
}
/**
* 默认情况下的模板只能支持RedisTemplate<String, String>,也就是只能存入字符串,因此支持序列化
*/
@Bean
public RedisTemplate<String, Serializable> redisCacheTemplate(LettuceConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Serializable> template = new RedisTemplate<>();
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
//关联
template.setConnectionFactory(factory);
//设置key的序列化器
template.setKeySerializer(new StringRedisSerializer());
//设置value的序列器
template.setValueSerializer(new Jackson2JsonRedisSerializer<Object>(Object.class));
return template;
}
/**
* 配置使用注解的时候缓存配置,默认是序列化反序列化的形式,加上此配置则为 json 形式
*/
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
// 配置序列化
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
RedisCacheConfiguration redisCacheConfiguration = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())).serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(factory).cacheDefaults(redisCacheConfiguration).build();
}
}
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.CollectionUtils;
import java.util.concurrent.TimeUnit;
/**
* @author shmily
* @description
* @project shmily-demo-root
* @package alp.starcode.demo.tools.utils
* @clazz RedisUtil
* @since 2022/9/14 0:10
**/
@Slf4j
public class RedisUtil {
private static RedisTemplate<String, Object> redisTemplate = SpringContextUtils.getBean("redisTemplate", RedisTemplate.class);
//=============================common============================
/**
* 指定缓存失效时间
*
* @param key 键
* @param time 时间(秒)
* @return
*/
public static boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
log.error("设置redis指定key失效时间错误:", e);
return false;
}
}
/**
* 根据key 获取过期时间
*
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效 失效时间为负数,说明该主键未设置失效时间(失效时间默认为-1)
*/
public static long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
*
* @param key 键
* @return true 存在 false 不存在
*/
public static boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
log.error("redis判断key是否存在错误:", e);
return false;
}
}
/**
* 删除缓存
*
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public static void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
//============================String=============================
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
@SuppressWarnings("unchecked")
public static <T> T get(String key) {
return key == null ? null : (T) redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public static boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
log.error("设置redis缓存错误:", e);
return false;
}
}
/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public static boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增 此时value值必须为int类型 否则报错
*
* @param key 键
* @param delta 要增加几(大于0)
* @return
*/
public static long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
*
* @param key 键
* @param delta 要减少几(小于0)
* @return
*/
public static long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
}
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
/**
* @author shmily
* @description Spring Context 工具类
* @project shmily-demo-root
* @package alp.starcode.demo.tools.utils
* @clazz SpringContextUtils
* @since 2022/9/14 0:03
**/
@Component
public class SpringContextUtils implements ApplicationContextAware {
public static ApplicationContext applicationContext;
public static Object getBean(String name) {
return applicationContext.getBean(name);
}
public static <T> T getBean(Class<T> requiredType) {
return applicationContext.getBean(requiredType);
}
public static <T> T getBean(String name, Class<T> requiredType) {
return applicationContext.getBean(name, requiredType);
}
public static boolean containsBean(String name) {
return applicationContext.containsBean(name);
}
public static boolean isSingleton(String name) {
return applicationContext.isSingleton(name);
}
public static Class<? extends Object> getType(String name) {
return applicationContext.getType(name);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
SpringContextUtils.applicationContext = applicationContext;
}
}
4.测试Redisson分布式锁
import alp.starcode.demo.redis.util.RedisUtil;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.UUID;
@RestController
public class RedissonController {
public static final String REDIS_LOCK = "ticket_lock";
@Resource
Redisson redisson;
/**
* 测试分布式锁
*
* @throws InterruptedException
*/
@RequestMapping("/testRedisson")
public void testRedisson() throws InterruptedException{
for (int i = 0; i <100; i++) {
purchaseTicket();
}
}
/**
* 购买门票
* @throws InterruptedException
*/
@Async
public void purchaseTicket() throws InterruptedException {
RLock lock = redisson.getLock(REDIS_LOCK);
lock.lock();
// 每个人进来先要进行加锁,key值为"ticket_lock"
String value = UUID.randomUUID().toString().replace("-","");
try {
// Redis中存有ticket:001号门票,数量为100
String result = RedisUtil.get("ticket:001").toString();
int total = result == null ? 0 : Integer.parseInt(result);
if (total > 0) {
// 剩余门票数大于0 ,则进行扣减
int realTotal = total - 1;
// 将门票数回写数据库
RedisUtil.set("ticket:001", String.valueOf(realTotal));
System.out.println("线程:" + Thread.currentThread().getName() + "获得了锁");
System.out.println("购买门票成功,库存还剩:" + realTotal + "件, 服务端口为8080");
//System.out.println("购买门票成功,库存还剩:"+realTotal +"件, 服务端口为8080");
} else {
System.out.println("购买门票失败,服务端口为8080");
}
}catch (Exception e){
e.printStackTrace();
}finally {
if(lock.isLocked() && lock.isHeldByCurrentThread()){
lock.unlock();
System.out.println("线程:" + Thread.currentThread().getName() + "释放锁");
}
}
}
}
5.如下图,测试分布式锁是否生效
至此,SpringBoot+Redisson整合完毕,后续会单独出一版Redis单节点分布式锁的实现。