Redis工具类
package xxxxx; import cn.hutool.core.util.BooleanUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import com.jdkj.sysschool.entity.RedisData; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.*; import org.springframework.stereotype.Service; import java.time.LocalDateTime; import java.util.*; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.function.Function; /** * @author ailuti */ @Service public class RedisService { @Autowired public StringRedisTemplate stringRedisTemplate; //定义线程库:10个线程 private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10); /** * 缓存基本的对象,Inteage、String等 * @param key 缓存的键值 * @param value 缓存的值 * @param timeout 时间 * @param timeUnit 时间颗粒度 */ public void set(String key, Object value, Long timeout, TimeUnit timeUnit) { stringRedisTemplate.opsForValue().set(key, value.toString(), timeout, timeUnit); } /** * 缓存Bean对象、实体类 * @param key 缓存的键值 * @param value 缓存的值 * @return 缓存的对象 */ public <T> void setCacheObject(String key, T value) { String v = JSONUtil.toJsonStr(value); stringRedisTemplate.opsForValue().set(key,v); } /** * 缓存Bean对象、实体类 * @param key 缓存的键值 * @param value 缓存的值 * @param timeout 时间 * @param timeUnit 时间颗粒度 * @return 缓存的对象 */ public <T> void setCacheObject(String key, T value, Integer timeout, TimeUnit timeUnit) { String v = JSONUtil.toJsonStr(value); stringRedisTemplate.opsForValue().set(key, v, timeout, timeUnit); } /** * 缓存List数据 * @param key 缓存的键值 * @param dataList 待缓存的List数据 * @return 缓存的对象 */ public <T> ListOperations<String, T> setCacheList(String key, List<T> dataList) { ListOperations listOperation = stringRedisTemplate.opsForList(); if (null != dataList) { int size = dataList.size(); for (int i = 0; i < size; i++) { listOperation.leftPush(key, dataList.get(i)); } } return listOperation; } /** * 获得缓存基本的对象,Inteage、String等 * @param key 缓存的键值 */ public String get(String key) { String s = stringRedisTemplate.opsForValue().get(key); return s; } /** * 获得缓存Bean对象、实体类 * @param key 缓存键值 * @return 缓存键值对应的数据 */ public <R> R getCacheObject(String key,Class<R> clazz) { String jsonStr = stringRedisTemplate.opsForValue().get(key); //判断是否存在 if(StrUtil.isNotBlank(jsonStr)){ //直接返回 return JSONUtil.toBean(jsonStr, clazz); } return null; } /** * 获得缓存Bean对象、实体类List * @param key 缓存键值 * @return 缓存键值对应的数据 */ public <R> List<R> getCacheList(String key, Class<R> clazz) { String jsonList = stringRedisTemplate.opsForValue().get(key); //判断是否存在 if(StrUtil.isNotBlank(jsonList)){ //直接返回 return JSONUtil.toList(jsonList, clazz); } return null; } /** * 删除单个对象 * @param key */ public void deleteObject(String key) { stringRedisTemplate.delete(key); } /** * 删除缓存的基本对象列表 * @param keys 缓存的前缀 */ public void deleteKeys(String keys) { Set rediskeys = stringRedisTemplate.keys(keys); stringRedisTemplate.delete(rediskeys); } /** * 获得缓存的keys * @param pattern 字符串前缀 * @return 对象列表 */ public Collection<String> keys(String pattern) { return stringRedisTemplate.keys(pattern); } /** * 缓存基本的对象,设置逻辑过期 * @param key 缓存键值 * @param value 缓存对象 * @param timeout 时间 * @param timeUnit 时间颗粒度 */ public void setWithLogicalExpire(String key, Object value, Long timeout, TimeUnit timeUnit) { // 设置逻辑过期 RedisData redisData = new RedisData(); redisData.setData(value); redisData.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(timeout))); // 写入Redis stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData)); } /** * 缓存穿透解决方法 * * 缓存穿透 :缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。 * 缓存空对象思路分析: * 当我们客户端访问不存在的数据时,先请求redis,但是此时redis中没有数据,此时会访问到数据库, * 但是数据库中也没有数据,这个数据穿透了缓存,直击数据库,数据库能够承载的并发不如redis这么高, * 如果大量的请求同时过来访问这种不存在的数据,这些请求就都会访问到数据库, * 简单的解决方案就是哪怕这个数据在数据库中也不存在,我们也把这个数据存入到redis中去, * 这样,下次用户过来访问这个不存在的数据,那么在redis中也能找到这个数据就不会进入到缓存了 * * @param keyPrefix 缓存键值Key的前缀 * @param id id * @param type 返回的类型 * @param dbFallback 数据查询函数 * @param timeout 时间 * @param timeUnit 时间颗粒度 * @param <R> * @param <ID> * @return */ public <R,ID> R queryWithPassThrough( String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long timeout, TimeUnit timeUnit){ String key = keyPrefix + id; // 1.从redis查询缓存 String json = stringRedisTemplate.opsForValue().get(key); // 2.判断是否存在 if (StrUtil.isNotBlank(json)) { // 3.存在,直接返回 return JSONUtil.toBean(json, type); } // 判断命中的是否是空值 if (json != null) { // 返回一个错误信息 return null; } // 4.不存在,根据id查询数据库 R r = dbFallback.apply(id); // 5.不存在,返回错误 if (r == null) { // 将空值写入redis stringRedisTemplate.opsForValue().set(key, "", 200, TimeUnit.MINUTES); // 返回错误信息 return null; } // 6.存在,写入redis this.set(key, r, timeout, timeUnit); return r; } /** * 缓存击穿解决方法 * * 缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了, * 无数的请求访问会在瞬间给数据库带来巨大的冲击。 * 我们之所以会出现这个缓存击穿问题,主要原因是在于我们对key设置了过期时间, * 假设我们不设置过期时间,其实就不会有缓存击穿的问题,但是不设置过期时间,这样数据不就一直占用我们内存了吗, * 我们可以采用逻辑过期方案。 * 我们把过期时间设置在 redis的value中,注意:这个过期时间并不会直接作用于redis, * 而是我们后续通过逻辑去处理。假设线程1去查询缓存,然后从value中判断出来当前的数据已经过期了, * 此时线程1去获得互斥锁,那么其他线程会进行阻塞,获得了锁的线程他会开启一个 线程去进行 以前的重构数据的逻辑, * 直到新开的线程完成这个逻辑后,才释放锁, 而线程1直接进行返回,假设现在线程3过来访问, * 由于线程线程2持有着锁,所以线程3无法获得锁,线程3也直接返回数据,只有等到新开的线程2把重建数据构建完后, * 其他线程才能走返回正确的数据。 * 这种方案巧妙在于,异步的构建缓存,缺点在于在构建完缓存之前,返回的都是脏数据。 * * @param keyPrefix 缓存键值Key的前缀 * @param id id * @param type 返回的类型 * @param dbFallback 数据查询函数 * @param timeout 时间 * @param timeUnit 时间颗粒度 * @param <R> * @param <ID> * @return */ public <R, ID> R queryWithLogicalExpire( String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long timeout, TimeUnit timeUnit) { String key = keyPrefix + id; // 1.从redis查询商铺缓存 String json = stringRedisTemplate.opsForValue().get(key); // 2.判断是否存在 if (StrUtil.isBlank(json)) { // 3.存在,直接返回 return null; } // 4.命中,需要先把json反序列化为对象 RedisData redisData = JSONUtil.toBean(json, RedisData.class); R r = JSONUtil.toBean((JSONObject) redisData.getData(), type); LocalDateTime expireTime = redisData.getExpireTime(); // 5.判断是否过期 if(expireTime.isAfter(LocalDateTime.now())) { // 5.1.未过期,直接返回店铺信息 return r; } // 5.2.已过期,需要缓存重建 // 6.缓存重建 // 6.1.获取互斥锁 String lockKey = "LOCK_KEY:"+ keyPrefix + ":" + id; boolean isLock = tryLock(lockKey); // 6.2.判断是否获取锁成功 if (isLock){ // 6.3.成功,开启独立线程,实现缓存重建 CACHE_REBUILD_EXECUTOR.submit(() -> { try { // 查询数据库 R newR = dbFallback.apply(id); // 重建缓存 this.setWithLogicalExpire(key, newR, timeout, timeUnit); } catch (Exception e) { throw new RuntimeException(e); }finally { // 释放锁 unlock(lockKey); } }); } // 6.4.返回过期的商铺信息 return r; } /** * 将key锁定10秒 * @param key * @return */ private boolean tryLock(String key) { Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS); return BooleanUtil.isTrue(flag); } /** * 将key解锁 * @param key * @return */ private void unlock(String key) { stringRedisTemplate.delete(key); } }
RedisData
package com.jdkj.sysschool.entity; import lombok.Data; import java.time.LocalDateTime; @Data public class RedisData { /** * 过期时间 */ private LocalDateTime expireTime; /** * 缓存数据 */ private Object data; }
文章评论