diff --git a/pom.xml b/pom.xml index 73137127..74eb7086 100644 --- a/pom.xml +++ b/pom.xml @@ -52,6 +52,7 @@ 0.7.5 2.2.0.RELEASE 7.12.1 + 3.12.0 @@ -114,6 +115,13 @@ ${elasticsearch.version} + + + org.redisson + redisson + ${redisson.version} + + com.xjs xjs-business-common diff --git a/ruoyi-common/ruoyi-common-redis/pom.xml b/ruoyi-common/ruoyi-common-redis/pom.xml index 005be306..ac52ff11 100644 --- a/ruoyi-common/ruoyi-common-redis/pom.xml +++ b/ruoyi-common/ruoyi-common-redis/pom.xml @@ -9,26 +9,42 @@ 4.0.0 公共模块-Redis模块 - + ruoyi-common-redis - + ruoyi-common-redis缓存服务 - - + + org.springframework.boot spring-boot-starter-data-redis + + + io.lettuce + lettuce-core + + - + + redis.clients + jedis + + + + org.redisson + redisson + + + com.ruoyi ruoyi-common-core - + - \ No newline at end of file + diff --git a/ruoyi-common/ruoyi-common-redis/src/main/java/com/ruoyi/common/redis/configure/RedisConfig.java b/ruoyi-common/ruoyi-common-redis/src/main/java/com/ruoyi/common/redis/configure/RedisConfig.java index 6f28066c..da96f0fa 100644 --- a/ruoyi-common/ruoyi-common-redis/src/main/java/com/ruoyi/common/redis/configure/RedisConfig.java +++ b/ruoyi-common/ruoyi-common-redis/src/main/java/com/ruoyi/common/redis/configure/RedisConfig.java @@ -1,10 +1,14 @@ package com.ruoyi.common.redis.configure; +import cn.hutool.core.util.RandomUtil; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.cache.CacheProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cache.annotation.CachingConfigurerSupport; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; @@ -27,7 +31,12 @@ import java.time.Duration; */ @Configuration @EnableCaching +@EnableConfigurationProperties(CacheProperties.class) public class RedisConfig extends CachingConfigurerSupport { + + @Autowired + private CacheProperties cacheProperties; + @Bean @SuppressWarnings(value = {"unchecked", "rawtypes"}) public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { @@ -66,10 +75,24 @@ public class RedisConfig extends CachingConfigurerSupport { om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jacksonSeial.setObjectMapper(om); // 定制缓存数据序列化方式及时效 - RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofDays(1)) //时效1天 + + RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(strSerializer)) - .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jacksonSeial)) - .disableCachingNullValues(); + .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jacksonSeial)); + + CacheProperties.Redis redisProperties = cacheProperties.getRedis(); + //将配置文件中所有的配置都生效 + if (redisProperties.getTimeToLive() != null) { + //定义随机时间值,防止缓存大量过期 + long salt = RandomUtil.randomLong(10, 100); + Duration time = redisProperties.getTimeToLive(); + Duration plusSeconds = time.plusSeconds(salt); + config = config.entryTtl(plusSeconds); + } + if (!redisProperties.isCacheNullValues()) { + config = config.disableCachingNullValues(); + } + RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(config).build(); return cacheManager; } diff --git a/ruoyi-common/ruoyi-common-redis/src/main/java/com/ruoyi/common/redis/configure/RedissonConfig.java b/ruoyi-common/ruoyi-common-redis/src/main/java/com/ruoyi/common/redis/configure/RedissonConfig.java new file mode 100644 index 00000000..fae4ea26 --- /dev/null +++ b/ruoyi-common/ruoyi-common-redis/src/main/java/com/ruoyi/common/redis/configure/RedissonConfig.java @@ -0,0 +1,44 @@ +package com.ruoyi.common.redis.configure; + +import lombok.Data; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Redisson分布式锁配置 + * + * @author xiejs + * @since 2022-04-08 + */ +@Configuration +@ConfigurationProperties(prefix = "redisson") +@Data +public class RedissonConfig { + + private String ip; + + private Integer port; + + + /** + * 所有对 Redisson 的使用都是通过 RedissonClient + * + * @return RedissonClient + */ + @Bean(destroyMethod = "shutdown",name = "redissonClient") + public RedissonClient redissonClient() { + // 1、创建配置 + Config config = new Config(); + // Redis url should start with redis:// or rediss:// + config.useSingleServer().setAddress("redis://" + ip + ":" + port + ""); + + // 2、根据 Config 创建出 RedissonClient 实例 + return Redisson.create(config); + } + + +} diff --git a/xjs-business/xjs-business-common/src/main/java/com/xjs/consts/RedisConst.java b/xjs-business/xjs-business-common/src/main/java/com/xjs/consts/RedisConst.java index 7ea12ed0..78ec0eac 100644 --- a/xjs-business/xjs-business-common/src/main/java/com/xjs/consts/RedisConst.java +++ b/xjs-business/xjs-business-common/src/main/java/com/xjs/consts/RedisConst.java @@ -13,17 +13,17 @@ public class RedisConst { /** * 翻译字典常量key */ - public static final String TRAN_DICT = "tianxing:tran_dict"; + public static final String TRAN_DICT = "bussiness:tianxing:tran_dict"; /** * 英语一言常量key */ - public static final String ONE_ENGLISH = "tianxing:one_english"; + public static final String ONE_ENGLISH = "bussiness:tianxing:one_english"; /** * 热搜常量key */ - public static final String HOT = "tianxing:hot"; + public static final String HOT = "bussiness:tianxing:hot"; /** * websocket常量key @@ -33,32 +33,32 @@ public class RedisConst { /** * ip信息常量key */ - public static final String IP_INFO = "ip_info"; + public static final String IP_INFO = "bussiness:ip_info"; /** * 实时天气常量信息key */ - public static final String NOW_WEATHER = "weather:now"; + public static final String NOW_WEATHER = "bussiness:weather:now"; /** * 预报天气常量信息key */ - public static final String FORECAST_WEATHER = "weather:forecast"; + public static final String FORECAST_WEATHER = "bussiness:weather:forecast"; /** * 爬虫记录循环次数常量信息:_36wallpaper */ - public static final String REPTILE_36_WALLPAPER_COUNT = "reptile:_36wallpaper.count"; + public static final String REPTILE_36_WALLPAPER_COUNT = "bussiness:reptile:_36wallpaper.count"; /** * 爬虫记录循环次数常量信息:weixin.sougou */ - public static final String REPTILE_WEIXIN_SOUGOU_COUNT = "reptile:weixin.sougou.count"; + public static final String REPTILE_WEIXIN_SOUGOU_COUNT = "bussiness:reptile:weixin.sougou.count"; /** * 爬虫记录循环次数常量信息:weixin.link */ - public static final String REPTILE_WEIXIN_LINK_COUNT = "reptile:weixin.link.count"; + public static final String REPTILE_WEIXIN_LINK_COUNT = "bussiness:reptile:weixin.link.count"; //--------------------------mall-key----------------------------------- @@ -78,6 +78,11 @@ public class RedisConst { */ public static final String CATALOG_BEFORE = MALL_PREFIX + "catalog:before"; + /** + * Redis分布式锁key + */ + public static final String LOCK = "lock"; + //-------------------有效时间----------------------- public static final Integer TRAN_DICT_EXPIRE = 1; //小时 @@ -92,5 +97,7 @@ public class RedisConst { public static final Long FORECAST_WHEATHER_EXPIRE = 10L; //分钟 + public static final Long LOCK_EXPIRE = 30L; //秒 + } diff --git a/xjs-business/xjs-business-openapi/src/main/java/com/xjs/copywriting/service/impl/CopyWritingServiceImpl.java b/xjs-business/xjs-business-openapi/src/main/java/com/xjs/copywriting/service/impl/CopyWritingServiceImpl.java index 4bb119cb..dddb12f2 100644 --- a/xjs-business/xjs-business-openapi/src/main/java/com/xjs/copywriting/service/impl/CopyWritingServiceImpl.java +++ b/xjs-business/xjs-business-openapi/src/main/java/com/xjs/copywriting/service/impl/CopyWritingServiceImpl.java @@ -46,7 +46,7 @@ public class CopyWritingServiceImpl extends ServiceImpl categoryEntityList=categoryService.getLevel1Categorys(); + List categoryEntityList = categoryService.getLevel1Categorys(); model.addAttribute("categorys", categoryEntityList); return "index"; } @GetMapping("/index/json/catalog.json") @ResponseBody - public Map> getCatalogJson() { + public Map> getCatalogJson() { return categoryService.getCatalogJson(); } + @ResponseBody + @GetMapping("hello") + public String hello() { + //获取一把锁 + RLock lock = redissonClient.getLock("my-lock"); + + //加锁 + lock.lock(); //阻塞时等待,默认假的锁是30s时间 + //锁的自动续期,如果业务超长,运行期间自动给锁续期30s,不用担心业务时间长,锁自动过期被删除 + //加锁的业务只要 运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30s以后过期 + + + try { + System.out.println("加锁成功" + Thread.currentThread().getName()); + Thread.sleep(10000); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + System.out.println("释放锁" + Thread.currentThread().getName()); + lock.unlock(); + + } + return "hello"; + + } } diff --git a/xjs-business/xjs-project-mall/mall-product/src/main/java/com/xjs/mall/product/service/impl/CategoryServiceImpl.java b/xjs-business/xjs-project-mall/mall-product/src/main/java/com/xjs/mall/product/service/impl/CategoryServiceImpl.java index 21125d57..868ddf48 100644 --- a/xjs-business/xjs-project-mall/mall-product/src/main/java/com/xjs/mall/product/service/impl/CategoryServiceImpl.java +++ b/xjs-business/xjs-project-mall/mall-product/src/main/java/com/xjs/mall/product/service/impl/CategoryServiceImpl.java @@ -17,21 +17,28 @@ import com.xjs.mall.product.service.AttrService; import com.xjs.mall.product.service.CategoryBrandRelationService; import com.xjs.mall.product.service.CategoryService; import com.xjs.mall.product.vo.Catelog2Vo; +import lombok.extern.log4j.Log4j2; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.util.*; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import static com.xjs.consts.RedisConst.CATALOG_AFTER; -import static com.xjs.consts.RedisConst.CATALOG_BEFORE; +import static com.xjs.consts.RedisConst.*; @Service("categoryService") @Transactional +@Log4j2 public class CategoryServiceImpl extends ServiceImpl implements CategoryService { @Resource @@ -49,6 +56,9 @@ public class CategoryServiceImpl extends ServiceImpl listWithTree() { @@ -111,7 +121,7 @@ public class CategoryServiceImpl extends ServiceImpl getLevel1Categorys() { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(CategoryEntity::getParentCid, 0); @@ -146,14 +158,19 @@ public class CategoryServiceImpl extends ServiceImpl> getCatalogJson() { + /** + * 1、空结果缓存,解决缓存穿透 + * 2、设置过期时间加随机值,解决缓存雪崩 + * 3、加锁,解决缓存击穿 + */ + + //先查缓存 String catalogJSON = stringRedisTemplate.opsForValue().get(CATALOG_BEFORE); if (StringUtils.isEmpty(catalogJSON)) { - //缓存没有查数据库并且放入 - Map> catalogJsonFormDb = this.getCatalogJsonFormDb(); - String jsonString = JSON.toJSONString(catalogJsonFormDb); - stringRedisTemplate.opsForValue().set(CATALOG_BEFORE, jsonString); - return catalogJsonFormDb; + + return this.getCatalogJsonFormDbWithRedissonLock(); + } else { return JSON.parseObject(catalogJSON, new TypeReference>>() { }); @@ -161,13 +178,19 @@ public class CategoryServiceImpl extends ServiceImpl> getCatalogJsonFormDb() { + //获取锁之后再去缓存确认,如果没有就继续查询 + String catalogJSON = stringRedisTemplate.opsForValue().get(CATALOG_BEFORE); + if (StringUtils.isNotEmpty(catalogJSON)) { + return JSON.parseObject(catalogJSON, new TypeReference>>() { + }); + } + //查出所有1级分类 List level1Categorys = getLevel1Categorys(); //封装数据 - return level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> { + Map> collect = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> { //每一个的一级分类,查到这个一级分类的二级分类 List categoryEntities = super.baseMapper.selectList(new LambdaQueryWrapper().eq(CategoryEntity::getParentCid, v.getCatId())); @@ -195,6 +218,83 @@ public class CategoryServiceImpl extends ServiceImpl> getCatalogJsonFormDbWithRedissonLock() { + + RLock lock = redissonClient.getLock(LOCK + ":catalog"); + lock.lock(); + + log.info("获取分布式锁成功"); + Map> catalogJsonFormDb; + try { + + catalogJsonFormDb = getCatalogJsonFormDb(); + + } finally { + lock.unlock(); + } + + return catalogJsonFormDb; + } + + //使用Java本地可重入锁synchronized + private Map> getCatalogJsonFormDbWithSynLock() { + + //加锁,解决缓存击穿 (本地锁/单机锁 分布式服务锁不住) + synchronized (this) { + return this.getCatalogJsonFormDb(); + } + } + + //使用redis分布式锁 + private Map> getCatalogJsonFormDbWithRedisLock() { + + //加锁,解决缓存击穿 分布式锁 去redis占坑 + String uuid = UUID.randomUUID().toString(); //添加唯一标识符 + Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(LOCK, uuid, LOCK_EXPIRE, TimeUnit.SECONDS); + if (Boolean.TRUE.equals(lock)) { + log.info("获取分布式锁成功:{}", uuid); + Map> catalogJsonFormDb = null; + try { + catalogJsonFormDb = this.getCatalogJsonFormDb(); + } finally { + //lua脚本解锁 + String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; + Long ret = stringRedisTemplate.execute(new DefaultRedisScript(script, Long.class), Arrays.asList(LOCK), uuid); + } + + //加锁成功 + //设置过期时间必须和加锁同步(原子性) + //stringRedisTemplate.expire(LOCK, LOCK_EXPIRE, TimeUnit.SECONDS); + + //删除锁(判断是否是自己的锁) + /*String lockValue = stringRedisTemplate.opsForValue().get(LOCK); + if (uuid.equals(lockValue)) { + stringRedisTemplate.delete(LOCK); + }*/ + + + return catalogJsonFormDb; + } else { + log.warn("获取分布式锁失败:{}", uuid); + //加锁失败...重试 自旋 + try { + Thread.sleep(100); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return this.getCatalogJsonFormDbWithRedisLock(); + + } + } //225,25,2