緩存穿透
緩存穿透是指用戶查詢數據,在數據庫沒有,自然在緩存中也不會有。這樣就導致用戶查詢的時候,在緩存中找不到,每次都要去數據庫再查詢一遍,然后返回空。這樣請求就繞過緩存直接查數據庫,這也是經常提的緩存命中率問題。
解決的辦法就是:如果查詢數據庫也為空,直接設置一個默認值存放到緩存,這樣第二次到緩沖中獲取就有值了,而不會繼續訪問數據庫,這種辦法最簡單粗暴。
把空結果,也給緩存起來,這樣下次同樣的請求就可以直接返回空了,即可以避免當查詢的值為空時引起的緩存穿透。同時也可以單獨設置個緩存區域存儲空值,對要查詢的key進行預先校驗,然后再放行給后面的正常緩存處理邏輯。
查詢查不到的數據,在緩存中沒有,而直接走了數據庫! 反反復復的去這么做就崩潰了哦
Redis穿透:
4沒有,redis中沒有,然后去DB查詢,會導致雪崩效應。稱之為 穿透效應。
穿透 產生的原因:客戶端隨機生成不同的key,在redis緩存中沒有該數據,數據庫也沒有該數據。這樣的話可能導致一直發生jdbc連接
解決方案:
1、通過網關判斷客戶端傳入對應key的規則,不符合數據庫查詢規則,直接返回空
2、如果使用的key數據庫查詢不到的話,直接在redis中存一份null結果。
在存入id為4的數據庫的時候,直接清除對應redis為4的緩存(此時是空哈)
廢話不多說,上代碼:
pom:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.toov5.architect</groupId> <artifactId>architect</artifactId> <version>0.0.1-SNAPSHOT</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.0.RELEASE</version> </parent> <dependencies> <!-- SpringBoot 對lombok 支持 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <!-- SpringBoot web 核心組件 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </dependency> <!-- SpringBoot 外部tomcat支持 --> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-jasper</artifactId> </dependency> <!-- springboot-log4j --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j</artifactId> <version>1.3.8.RELEASE</version> </dependency> <!-- springboot-aop 技術 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <!-- https://mvnrepository.com/artifact/commons-lang/commons-lang --> <dependency> <groupId>commons-lang</groupId> <artifactId>commons-lang</artifactId> <version>2.6</version> </dependency> <!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient --> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> </dependency> <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.47</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> </dependency> <dependency> <groupId>taglibs</groupId> <artifactId>standard</artifactId> <version>1.1.2</version> </dependency> <!--開啟 cache 緩存 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <!-- ehcache緩存 --> <dependency> <groupId>net.sf.ehcache</groupId> <artifactId>ehcache</artifactId> <version>2.9.1</version><!--$NO-MVN-MAN-VER$ --> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.1.1</version> </dependency> <!-- mysql 依賴 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- redis 依賴 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> </dependencies> </project>
service:
package com.toov5.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.ehcache.EhCacheCacheManager; import org.springframework.stereotype.Component; import net.sf.ehcache.Cache; import net.sf.ehcache.Element; @Component public class EhCacheUtils { // @Autowired // private CacheManager cacheManager; @Autowired private EhCacheCacheManager ehCacheCacheManager; // 添加本地緩存 (相同的key 會直接覆蓋) public void put(String cacheName, String key, Object value) { Cache cache = ehCacheCacheManager.getCacheManager().getCache(cacheName); Element element = new Element(key, value); cache.put(element); } // 獲取本地緩存 public Object get(String cacheName, String key) { Cache cache = ehCacheCacheManager.getCacheManager().getCache(cacheName); Element element = cache.get(key); return element == null ? null : element.getObjectValue(); } public void remove(String cacheName, String key) { Cache cache = ehCacheCacheManager.getCacheManager().getCache(cacheName); cache.remove(key); } }
package com.toov5.service; import java.util.Set; import java.util.concurrent.TimeUnit; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; @Component public class RedisService { @Autowired private StringRedisTemplate stringRedisTemplate; //這樣該方法支持多種數據類型 public void set(String key , Object object, Long time){ //開啟事務權限 stringRedisTemplate.setEnableTransactionSupport(true); try { //開啟事務 stringRedisTemplate.multi(); String argString =(String)object; //強轉下 stringRedisTemplate.opsForValue().set(key, argString); //成功就提交 stringRedisTemplate.exec(); } catch (Exception e) { //失敗了就回滾 stringRedisTemplate.discard(); } if (object instanceof String ) { //判斷下是String類型不 String argString =(String)object; //強轉下 //存放String類型的 stringRedisTemplate.opsForValue().set(key, argString); } //如果存放Set類型 if (object instanceof Set) { Set<String> valueSet =(Set<String>)object; for(String string:valueSet){ stringRedisTemplate.opsForSet().add(key, string); //此處點擊下源碼看下 第二個參數可以放好多 } } //設置有效期 if (time != null) { stringRedisTemplate.expire(key, time, TimeUnit.SECONDS); } } //做個封裝 public void setString(String key, Object object){ String argString =(String)object; //強轉下 //存放String類型的 stringRedisTemplate.opsForValue().set(key, argString); } public void setSet(String key, Object object){ Set<String> valueSet =(Set<String>)object; for(String string:valueSet){ stringRedisTemplate.opsForSet().add(key, string); //此處點擊下源碼看下 第二個參數可以放好多 } } public String getString(String key){ return stringRedisTemplate.opsForValue().get(key); } }
package com.toov5.service; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.bind.annotation.RequestMapping; import com.toov5.entity.Users; import com.toov5.mapper.UserMapper; import io.netty.util.internal.StringUtil; @Service public class SnowslideService { @Autowired private UserMapper userMapper; @Autowired private RedisService redisService; private Lock lock = new ReentrantLock(); public String getUser01(Long id){ //定義key, key以當前的類名+方法名+id+參數值 String key = this.getClass().getName() + "-" + Thread.currentThread().getStackTrace()[1].getMethodName() + "-id:" + id; //1查詢redis String username = redisService.getString(key); if (!StringUtil.isNullOrEmpty(username)) { return username; } String resultUsaerName = null; try { //開啟鎖 lock.lock(); Users user = userMapper.getUser(id); if (username == null) { return null; } resultUsaerName =user.getName(); redisService.setString(key, resultUsaerName); } catch (Exception e) { // TODO: handle exception }finally { //釋放鎖 lock.unlock(); } //3直接返回 return resultUsaerName; } //穿透解決方案 public String getUser02(Long id){ //定義key, key以當前的類名+方法名+id+參數值 String key = this.getClass().getName() + "-" + Thread.currentThread().getStackTrace()[1].getMethodName() + "-id:" + id; //1查詢redis System.out.println("查詢redis緩存"+"key"+key+".resultUserName"); String username = redisService.getString(key); if (!StringUtil.isNullOrEmpty(username)) { return username; } String resultUsaerName = null; //如果數據庫中,沒有對應的數據信息的時候 System.out.println("查詢數據庫:id"+id); Users user = userMapper.getUser(id); if (user == null) { resultUsaerName="${null}"; //做個標記 客戶端識別到后 提示下吧 }else { resultUsaerName=user.getName(); } System.out.println("寫入redis緩存"+"key"+key+".resultUserName"+resultUsaerName); redisService.setString(key, resultUsaerName); //3直接返回 return resultUsaerName; } }
package com.toov5.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import com.alibaba.fastjson.JSONObject; import com.toov5.entity.Users; import com.toov5.mapper.UserMapper; import io.netty.util.internal.StringUtil; @Component public class UserService { @Autowired private EhCacheUtils ehCacheUtils; @Autowired private RedisService redisService; @Autowired private UserMapper userMapper; //定義個全局的cache名字 private String cachename ="userCache"; public Users getUser(Long id){ //先查詢一級緩存 key以當前的類名+方法名+id+參數值 String key = this.getClass().getName() + "-" + Thread.currentThread().getStackTrace()[1].getMethodName() + "-id:" + id; //查詢一級緩存數據有對應值的存在 如果有 返回 Users user = (Users)ehCacheUtils.get(cachename, key); if (user != null) { System.out.println("key"+key+",直接從一級緩存獲取數據"+user.toString()); return user; } //一級緩存沒有對應的值存在,接着查詢二級緩存 // redis存對象的方式 json格式 然后反序列號 String userJson = redisService.getString(key); //如果rdis緩存中有這個對應的值,修改一級緩存 最下面的會有的 相同會覆蓋的 if (!StringUtil.isNullOrEmpty(userJson)) { //有 轉成json JSONObject jsonObject = new JSONObject();//用的fastjson Users resultUser = jsonObject.parseObject(userJson,Users.class); ehCacheUtils.put(cachename, key, resultUser); return resultUser; } //都沒有 查詢DB Users user1 = userMapper.getUser(id); if (user1 == null) { return null; } //保證兩級緩存有效期相同!? 一級緩存時間-二級緩存執行的時間 //一級緩存時間 等於 二級緩存剩下的時間 //存放到二級緩存 redis中 redisService.setString(key, new JSONObject().toJSONString(user1)); //存放到一級緩存 Ehchache ehCacheUtils.put(cachename, key, user1); return user1; } }
mapper
package com.toov5.mapper; import java.util.List; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import org.springframework.cache.annotation.CacheConfig; import org.springframework.cache.annotation.Cacheable; import com.toov5.entity.Users; //引入的jar包后就有了這個注解了 非常好用 (配置緩存的基本信息) @CacheConfig(cacheNames={"userCache"}) //緩存的名字 整個類的 public interface UserMapper { @Select("SELECT ID ,NAME,AGE FROM users where id=#{id}") @Cacheable //讓這個方法實現緩存 查詢完畢后 存入到緩存中 不是每個方法都需要緩存呀!save()就不用了吧 Users getUser(@Param("id") Long id); }
entity
package com.toov5.entity; import java.io.Serializable; import lombok.Data; @Data public class Users implements Serializable{ private String name; private Integer age; }
controller
package com.toov5.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.toov5.service.SnowslideService; @RestController public class UserRedisController { @Autowired private SnowslideService snowslideService; @RequestMapping("/getUser02") public String getUser02(Long id){ return snowslideService.getUser02(id); } }
package com.toov5.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.toov5.entity.Users; import com.toov5.service.UserService; @RestController public class IndexController { @Autowired private UserService userService; @RequestMapping("/userId") public Users getUserId(Long id){ return userService.getUser(id); } }
啟動類
package com.toov5.app; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cache.annotation.EnableCaching; @EnableCaching //開啟緩存 @MapperScan(basePackages={"com.toov5.mapper"}) @SpringBootApplication(scanBasePackages={"com.toov5.*"}) public class app { public static void main(String[] args) { SpringApplication.run(app.class, args); } }
yml
###端口號配置 server: port: 8080 ###數據庫配置 spring: datasource: url: jdbc:mysql://localhost:3306/test username: root password: root driver-class-name: com.mysql.jdbc.Driver test-while-idle: true test-on-borrow: true validation-query: SELECT 1 FROM DUAL time-between-eviction-runs-millis: 300000 min-evictable-idle-time-millis: 1800000 # 緩存配置讀取 cache: type: ehcache ehcache: config: classpath:app1_ehcache.xml redis: database: 0 jedis: pool: max-active: 8 max-wait: -1 max-idle: 8 min-idle: 0 timeout: 10000 cluster: nodes: - 192.168.91.5:9001 - 192.168.91.5:9002 - 192.168.91.5:9003 - 192.168.91.5:9004 - 192.168.91.5:9005 - 192.168.91.5:9006
<?xml version="1.0" encoding="UTF-8"?> <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"> <diskStore path="java.io.tmpdir/ehcache-rmi-4000" /> <!-- 默認緩存 --> <defaultCache maxElementsInMemory="1000" eternal="true" timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="true" diskSpoolBufferSizeMB="30" maxElementsOnDisk="10000000" diskPersistent="true" diskExpiryThreadIntervalSeconds="120" memoryStoreEvictionPolicy="LRU"> </defaultCache> <!-- demo緩存 --><!-- name="userCache" 對應我們在 @CacheConfig(cacheNames={"userCache"}) !!!!! --> <!--Ehcache底層也是用Map集合實現的 --> <cache name="userCache" maxElementsInMemory="1000" eternal="false" timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="true" diskSpoolBufferSizeMB="30" maxElementsOnDisk="10000000" diskPersistent="false" diskExpiryThreadIntervalSeconds="120" memoryStoreEvictionPolicy="LRU"> <!-- LRU緩存策略 --> <cacheEventListenerFactory class="net.sf.ehcache.distribution.RMICacheReplicatorFactory" /> <!-- 用於在初始化緩存,以及自動設置 --> <bootstrapCacheLoaderFactory class="net.sf.ehcache.distribution.RMIBootstrapCacheLoaderFactory" /> </cache> </ehcache>
再加一個攔截
運行結果:
把空結果,也給緩存起來,這樣下次同樣的請求就可以直接返回空了,即可以避免當查詢的值為空時引起的緩存穿透。同時也可以單獨設置個緩存區域存儲空值,對要查詢的key進行預先校驗,然后再放行給后面的正常緩存處理邏輯。
注意:再給對應的ip存放真值的時候,需要先清除對應的之前的空緩存。
補充熱點key
熱點key:某個key訪問非常頻繁,當key失效的時候有打量線程來構建緩存,導致負載增加,系統崩潰。
解決辦法:
①使用鎖,單機用synchronized,lock等,分布式用分布式鎖。
②緩存過期時間不設置,而是設置在key對應的value里。如果檢測到存的時間超過過期時間則異步更新緩存。
③在value設置一個比過期時間t0小的過期時間值t1,當t1過期的時候,延長t1並做更新緩存操作。