赞
踩
在大多数项目中,都会使用缓存中间件,例如Redis、Memcached等,一般会选择项目适配的中间件,能够提高数据的访问速度,减轻后端服务器和数据库的压力,但在使用缓存时,又不得不考虑其线程安全问题,特别是现在大多数项目都使用了分布式架构,因此就需要对中间件进行优化和加强。
Redisson是一个开源框架,它提供了一系列分布式数据结构和服务,如分布式锁、分布式集合、分布式对象等,使得在分布式环境中使用Redis变得更加简单和高效(后续会出一篇关于Redisson的原理和特性),下面就来说一说在系统中如何去使用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>
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> {}
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; } }
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() + "释放锁"); } } } }
至此,SpringBoot+Redisson整合完毕,后续会单独出一版Redis单节点分布式锁的实现。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。