缓存使用及常见问题解决
作者:互联网
缓存引入
-
配置文件
-
#启动: nohup java -jar xxx.jar > log.admin 2>&1 & server: port: 8090 compression: enabled: true connection-timeout: 3000 spring: redis: host: 192.168.0.128 port: 6380 datasource: url: jdbc:mysql://192.168.0.128:3307/enjoy?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull username: root password: root driverClassName : com.mysql.jdbc.Driver test-on-borrow: false test-while-idle: true time-between-eviction-runs-millis: 3600000 mybatis: mapperLocations: classpath:mapper/*.xml #logging: # level: # com: # enjoy: # dao: debug
-
-
redis的序列化策略
-
package com.enjoy.config; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.CachingConfigurerSupport; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.concurrent.ConcurrentMapCache; import org.springframework.cache.interceptor.KeyGenerator; import org.springframework.cache.support.SimpleCacheManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; 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.core.RedisTemplate; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import java.lang.reflect.Method; import java.time.Duration; import java.util.Collections; @Configuration @EnableCaching public class CacheConfig extends CachingConfigurerSupport { //key的生成,springcache的内容,跟具体实现缓存器无关 //自定义本项目内的key的方式 @Bean public KeyGenerator keyGenerator() { return new KeyGenerator() { @Override public Object generate(Object target, Method method, Object... params) { StringBuilder sb = new StringBuilder(); sb.append(target.getClass().getSimpleName()); sb.append(method.getName()); for (Object obj : params) { sb.append(obj.toString()); } return sb.toString(); } }; } //不支持过期时间 // @Bean // public CacheManager cacheManager() { // //jdk里,内存管理器 // SimpleCacheManager cacheManager = new SimpleCacheManager(); // cacheManager.setCaches(Collections.singletonList(new ConcurrentMapCache("province"))); // return cacheManager; // } @Bean public CacheManager cacheManager(RedisConnectionFactory connectionFactory) { return RedisCacheManager .builder(connectionFactory) .cacheDefaults( RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofSeconds(20))) //缓存时间绝对过期时间20s .transactionAware() .build(); } /** * 序列化object对象为json字符串 */ // @Bean // public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { // RedisTemplate<String, Object> template = new RedisTemplate<>(); // template.setConnectionFactory(factory); // // //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值 // Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class); // ObjectMapper mapper = new ObjectMapper(); // mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); // serializer.setObjectMapper(mapper); // template.setValueSerializer(serializer); // // //使用StringRedisSerializer来序列化和反序列化redis的key值 // template.setKeySerializer(new StringRedisSerializer()); // template.afterPropertiesSet(); // return template; // } /** * JdkSerializationRedisSerializer: 序列化java对象(被序列化的对象必须实现Serializable接口),无法转义成对象 */ @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); //使用jdk的序列化 template.setValueSerializer(new JdkSerializationRedisSerializer()); //使用StringRedisSerializer来序列化和反序列化redis的key值 template.setKeySerializer(new StringRedisSerializer()); template.afterPropertiesSet(); return template; } }
-
1.String用StringRedisSerializer来序列化
2.Object用JdkSerializationRedisSerializer序列化,但是对象必须实现Seriablizable接口,如果没有实现,可以用Jackson2JsonRedisSerializer来序列化
-
高并发解决方案
-
遇到高并发,第一选择都是加缓冲,之后的升级方案,缓存/动态页面静态化/集群/拆字诀/同步转异步/限流熔断降级
-
缓存是高并发的首选策略
-
缓存,是最简单也是效果最好的,至少能把性能提升100倍
-
什么样的数据适合缓存?
- 1.访问频率高
- 2.更改频率低
- 3.一致性要求不高
缓存效益
-
投入/产出比
-
内存性能>硬盘,但是内存的价格也更贵,所以要最小的内存完成最大的性能,根据28定律,20%的业务数据是访问频繁的,另外是不频繁的,所以只要把20%频繁访问的数据访问性能提高,就可以提高整个系统的性能
- 命中率就是从缓存中读取次数与总读取次数的比值
- miss率就是1-命中率
缓存—常规逻辑流程
- 从缓存中读取数据,有则直接返回,没有则去数据库读,并保持在缓存中,再返回
测试
-
省份信息缓存
-
原始类
-
@Override public Provinces detail(String provinceid) { Provinces provinces = null; System.out.println("数据库中得到数据--------"+System.currentTimeMillis()); provinces = provincesDao.detail(provinceid); if (null != provinces){ provinces.setCities(citiesDao.list(provinceid)); } return provinces; }
-
-
按照最简单的缓存逻辑加缓存
-
@Override public Provinces detail(String provinceid) { Provinces provinces = null; //在redis查询 provinces = (Provinces)redisTemplate.opsForValue().get(provinceid); if (null != provinces){ // redisTemplate.expire(provinceid,20000, TimeUnit.MILLISECONDS); System.out.println("缓存中得到数据"); return provinces; } // 这里的super是查询数据库方法 provinces = super.detail(provinceid); if (null != provinces){ redisTemplate.opsForValue().set(provinceid,provinces);//set缓存 redisTemplate.expire(provinceid,20000, TimeUnit.MILLISECONDS);//设置过期 } return provinces; }
-
失效期最大作用是把不常用的数据清除掉,这样省空间
-
对应于前面的用20%的存储来解决100%的数据,这种存在的数据就是热点数据
-
很多场景,业务上线半年以后,某些接口出现性能瓶颈,如果可以用缓存解决此时可以通过缓存解决
-
测试2
-
对于更新操作,需要双删
-
@Override public Provinces update(Provinces entity) {//双删 redisTemplate.delete(entity.getProvinceid());//直接删除缓存,预防数据库成功,缓存失败 super.update(entity); redisTemplate.delete(entity.getProvinceid());//双删 return entity; }
- 因为读比写入快,有可能更新数据的时候,又有读线程往缓存中添加了数据,所以更新完数据后,需要删除
-
第一步的删除是用来预防数据库成功,但是后一步缓存删除失败
-
测试3
- 但是测试2中的代码侵入性太强,所以可以通过aop
缓存–springcahce的用法
-
package com.enjoy.service; import com.enjoy.entity.Provinces; import org.springframework.cache.annotation.*; import org.springframework.stereotype.Service; /** * springcache优雅实现缓存 */ //@Service("provincesService") @CacheConfig(cacheNames="province") //通用配置 public class ProvincesServiceImpl2 extends ProvincesServiceImpl implements ProvincesService{ // @Cacheable(value = "province", // key = "#root.targetClass.simpleName+':'+#root.methodName+':'+#provinceid") @Cacheable// value指定当前接口,要使用哪一个缓存器 --- 如果该缓存器不存在,则创建一个 public Provinces detail(String provinceid) {//一个接口方法,对应一个缓存器 return super.detail(provinceid); } //这个AOP,是先删缓存,先改数据库? @CachePut(key = "#entity.provinceid") public Provinces update(Provinces entity) { return super.update(entity); } @CacheEvict public void delete(String provinceid) { super.delete(provinceid); } //组合配置 @Caching(put = { @CachePut(key = "#entity.provinceid"), @CachePut(key = "#entity.provinceid")} ) public Provinces add(Provinces entity) { return super.add(entity); } }
-
标准化的缓存管理器
-
@Cacheable:缓存查询
-
自定义cacheKey
//key的生成,springcache的内容,跟具体实现缓存器无关 //自定义本项目内的key的方式 @Bean public KeyGenerator keyGenerator() { return new KeyGenerator() { @Override public Object generate(Object target, Method method, Object... params) { StringBuilder sb = new StringBuilder(); sb.append(target.getClass().getSimpleName()); sb.append(method.getName()); for (Object obj : params) { sb.append(obj.toString()); } return sb.toString(); } }; }
-
@Cacheable的底层就是实现了测试2中的缓存逻辑,这里是用的aop环绕增强
-
-
@CachePut : 缓存增改
-
@CacheEvict : 缓存删除
-
@Caching : 组合多个注解
-
@CacheConfig : 抽取公共配置(类级别)
-
-
引入spring-cache
-
//不支持过期时间 // 本地内存缓存器 @Bean public CacheManager cacheManager() { //jdk里,内存管理器 SimpleCacheManager cacheManager = new SimpleCacheManager(); cacheManager.setCaches(Collections.singletonList(new ConcurrentMapCache("province"))); return cacheManager; } // redis缓存器 @Bean public CacheManager cacheManager(RedisConnectionFactory connectionFactory) { return RedisCacheManager .builder(connectionFactory) .cacheDefaults( RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofSeconds(20))) //缓存时间绝对过期时间20s .transactionAware() .build(); }
缓存过期与一致性问题
- 1、实时更新:
—同步去调用cache增删改 - 2、准实时更新:
—甩锅第三方–>我的数据变啦
---------观察者模式/发布订阅/mq - 3、定时任务给你惊喜
------每天凌晨统一全量刷新
缓存击穿
- 某个key失效的一刹那,查询量非常大,导致数据库压力徒增,称之缓存击穿。
缓存雪崩
- 缓存集中在一段时间内失效,导致所有的查询都落在数据库上,称之缓存雪崩。
方案
- 1、加锁、限流:
------对某个key只允许一个线程查询数据和写缓存 - 2、缓存预热
-----常用值,使用http接口预热错峰加载 - 3、改被动失效为主动失效
-----什么时候失效,我说了算
方案1测试
-
package com.enjoy.service; import com.enjoy.entity.Provinces; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * 缓存雪崩 */ //@Service("provincesService") public class ProvincesServiceImpl3 extends ProvincesServiceImpl implements ProvincesService{ private static final Logger logger = LoggerFactory.getLogger(ProvincesServiceImpl3.class); @Resource private CacheManager cm; private ConcurrentHashMap<String, Lock> locks = new ConcurrentHashMap<>();//线程安全的 private static final String CACHE_NAME = "province"; public Provinces detail(String provinceid) { // 1.从缓存中取数据 Cache.ValueWrapper valueWrapper = cm.getCache(CACHE_NAME).get(provinceid); if (valueWrapper != null) { logger.info("缓存中得到数据"); return (Provinces) (valueWrapper.get()); } //2.加锁排队,阻塞式锁---100个线程走到这里---同一个sql的取同一把锁 doLock(provinceid);//32个省,最多只有32把锁,1000个线程 try{//第二个线程进来了 // 一次只有一个线程 //双重校验,不加也没关系,无非是多刷几次库 valueWrapper = cm.getCache(CACHE_NAME).get(provinceid);//第二个线程,能从缓存里拿到值? if (valueWrapper != null) { logger.info("缓存中得到数据"); return (Provinces) (valueWrapper.get());//第二个线程,这里返回 } Provinces provinces = super.detail(provinceid); // 3.从数据库查询的结果不为空,则把数据放入缓存中,方便下次查询 if (null != provinces){ cm.getCache(CACHE_NAME).put(provinceid, provinces); } return provinces; }catch(Exception e){ return null; }finally{ //4.解锁 releaseLock(provinceid); } } private void releaseLock(String userCode) { ReentrantLock oldLock = (ReentrantLock) locks.get(userCode); if(oldLock !=null && oldLock.isHeldByCurrentThread()){ oldLock.unlock(); } } private void do Lock(String lockcode) {//给一个搜索条件,对应一个锁 //provinceid有不同的值,参数多样化 //provinceid相同的,加一个锁,---- 不是同一个key,不能用同一个锁 ReentrantLock newLock = new ReentrantLock();//创建一个锁 Lock oldLock = locks.putIfAbsent(lockcode, newLock);//若已存在,则newLock直接丢弃 if(oldLock == null){ newLock.lock(); }else{ oldLock.lock(); } } }
-
除了加锁的方法,还有缓存预热(让每一个key有时间差)和改被动失效为主动失效(自己做一个系统监控key的状态,可以动态管理)
缓存穿透
-
黑客攻击,查询库里不存在的数据,导致所有的请求都打到mysql库上
-
最好是根据业务特点来定制一套规则,用来校验缓存请求的有效性
布隆过滤器
- 假设是string数据,通过一种算法进行映射,而布隆过滤器做了多个int值映射,如果映射过去发现byte[]数组里面为1,则说明这个值是存在的
- 当外界查询aaa数据时,如果发现映射出的位置的数据都是1,则认为此数据在系统中是存在的,只要有一个位置为0,就认为不存在
测试
-
package com.enjoy.service; import com.enjoy.entity.Provinces; import com.google.common.base.Charsets; import com.google.common.hash.BloomFilter; import com.google.common.hash.Funnels; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.CachePut; import org.springframework.cache.annotation.Cacheable; import javax.annotation.PostConstruct; import java.util.List; /** * 缓存穿透 */ //@Service("provincesService") public class ProvincesServiceImpl4 extends ProvincesServiceImpl implements ProvincesService{ private BloomFilter<String> bf =null; //等效成一个set集合 @PostConstruct //对象创建后,自动调用本方法 public void init(){//在bean初始化完成后,实例化bloomFilter,并加载数据 List<Provinces> provinces = this.list(); //当成一个SET----- 占内存,比hashset占得小很多 bf = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), provinces.size());// 32个 for (Provinces p : provinces) { bf.put(p.getProvinceid()); } } @Cacheable(value = "province") public Provinces detail(String provinceid) { //先判断布隆过滤器中是否存在该值,值存在才允许访问缓存和数据库 if(!bf.mightContain(provinceid)){ System.out.println("非法访问--------"+System.currentTimeMillis()); return null; } System.out.println("数据库中得到数据--------"+System.currentTimeMillis()); Provinces provinces = super.detail(provinceid); return provinces; } @CachePut(value = "province",key = "#entity.provinceid") public Provinces update(Provinces entity) { super.update(entity); return entity; } @CacheEvict(value = "province",key = "#entity.provinceid") public Provinces add(Provinces entity) { super.add(entity); bf.put(entity.getProvinceid());//新生成,加入过滤器 return entity; } @Override @CacheEvict("province") public void delete(String provinceid) { super.delete(provinceid); } }
标签:Provinces,缓存,return,provinceid,常见问题,解决,import,public 来源: https://blog.csdn.net/Markland_l/article/details/116066997