缓存

缓存概念
  1. 根据数据使用的规则,二八规律:有20%的数据最常用,加载入缓存/有80%的数据不常用,最好不占用缓存
  2. 不一定是缓存的数据库结果,而是缓存业务结果(数据库的结果经过一些处理)

外存 : 计算机内存与CPU缓存之外的储存器,一般断电数据不丢失,用于数据持久化
内存 : 外存与CPU沟通的桥梁,一般断电之后数据也会被清空
缓存 : 把一些外存的数据存到内存而已 java中一般缓存通过Map来实现的,广义的缓存就是把一些慢存的数据保存到快存上,加快系统运行的速度和效率

常见的缓存举例
CPU的一级缓存、二级缓存
maven的本地仓库
京东的仓储
数据库的索引,以空间换取时间

什么样的数据适合缓存?

  1. 访问频率高 – 读多写少
  2. 更改频率低
  3. 一致性要求不高(比如转账,金融项目不适合大量缓存的原因)

为什么缓存速度快? 内存速度 > 磁盘速度

缓存效能? 最小内存(昂贵)-> 最大功用

重要的指标:
总读取次数 = 从缓存中读取次数 + 从慢速设备上读取的次数
命中率 = 从缓存中读取次数 / 总读取次数
Miss率 = 没有从缓存中读取的次数 / 总读取次数
缓存的命中率是表明缓存是否运行良好的指标,做缓存一定要监控这个指标

缓存在java中的实现

首先需要配置好缓存管理器

import java.lang.reflect.Method;
import java.time.Duration;

import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
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.StringRedisSerializer;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;

@Configuration
@EnableCaching
public class CaCheConfig extends CachingConfigurerSupport {
	//不支持过期时间
	//  @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()
				// 缓存时间绝对 过期时间 20s
				.entryTtl(Duration.ofSeconds(20))).transactionAware().build();
	}

	@Bean
	public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
		RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
		template.setConnectionFactory(factory);
		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;
	}
}
查询数据时的缓存实现逻辑
  1. 从缓存中读取数据
  2. 如果命中,直接返回
  3. 如果未命中,则查询数据库,并将数据加入到缓存中
@Resource
private CacheManager cacheManager;

private static final String CACHE_NAME = "province";

@Override
public Provinces detail(String provinceid) {
	// 1. 从缓存中取数据
	ValueWrapper valueWrapper = cacheManager.getCache(CACHE_NAME).get(provinceid);
	if (valueWrapper != null) {
		// 当然实际过程中 还需要统计缓存命中率参数 如果缓存命中率过低 没有使用缓存的必要
		logger.info("缓存中得到数据");
		return (Provinces) valueWrapper.get();
	}
	// 2. 从数据库查询数据
	Provinces provinces = super.detail(provinceid);
	// 3. 从数据库查询的结果不为空,则把数据放入缓存中,方便下次查询
	if (null != provinces) {
		logger.info("缓存中得到数据");
		cacheManager.getCache(CACHE_NAME).put(provinceid, provinces);
	}
	return provinces;
}
更新数据时的缓存实现逻辑

更新数据时,要进行双删,更改之前和更改之后都进行删除。
为啥要在更改之前进行删除?如果不进行更改前删除,但是后续更改数据库成功,但是更改之后的删除缓存失败,就会导致缓存的不一致了。
为啥要在更改之后进行删除?因为在你进行更改的过程中,可能存在其他线程进行数据库的查询,而且查询通常比
更新要快,因此在更改完成之前又存在了缓存数据。(第二次删除变为存储缓存?)
在更新数据库之前更新缓存,但此时未必就能得到业务结果(输入为key,返回为业务结果,从key到业务结果还有一段路程),通过delete,
实现最简单,不引入业务复杂度

双删不是必须的,提高缓存一致性

@Override
public Provinces update(Provinces entity) {
	cacheManager.getCache(CACHE_NAME).evict(entity.getProvinceid());
	provincesDao.update(entity);
	cacheManager.getCache(CACHE_NAME).evict(entity.getProvinceid());
	return entity;
}
代码优化空间分析–逻辑上的变与不变
  1. 缓存如何使用,它的使用流程框架可以确定,不会再有变化—可抽象成一段宏观的模板性代码
  2. 具体缓存器内部怎么实现,用什么来实现(redis?Memcache?java数组),现在无法确定,但是可以甩锅(只提需求接口)----cache接口
  3. 上述两点结合,就是一个天然的设计模式—模板方法模式
  4. 模板方法模式:即先设计出主业务流程,如下面代码:
    ------主设计师设计出detail方法的流程
    ----1. 从mysql中查询一条数据
    ----2. 将数据返回
    ----3. mysql数据具体怎么查 我不管
SpringCache的使用

SpringCache的使用(不仅仅是redis,还有es、mongodb 比如abcde搜索,五个元素的排列组合,数据大膨胀,使用es)

  1. SpringCache统一定义了缓存器Cache接口 org.springframework.cache.Cache
  2. SpringCache还规定这些缓存器必须要有一个管理器来管理它们 org.springframework.cache.CacheManager
    (1) manager负责创建/查找缓存器,查找过程,以key-value形式记录映射关系
    (2)rediscache的manager实现了cache的自动创建,即当你指定的cache不存在时,自动创建一个供你使用
    (3)通过标签进行标注
    key的自定义
    第一种 :使用SPEL表达式指定
    第二种: 扩展key的生成机制 org.springframework.cache.interceptor.KeyGenerator
/**
 * key的生成 springcache的内容 跟具体实现缓存器有关
 */
@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 object : params) {
				sb.append(object.toString());
			}
			return sb.toString();
		}
	};
}
@Service
@CacheConfig(cacheNames = "province")
public class ProvincesServiceImpl implements ProvincesService {
	
	@Autowired
	private ProvincesDao provincesDao;

	@Autowired
	private CitiesDao citiesDao;

	@Override
	public List<Provinces> list() {
		return provincesDao.list();
	}

	@Cacheable(key = "#id")
	@Override
	public Provinces detail(String id) {
		Provinces provinces = = provincesDao.detail(id);
		if (null != provinces) {
			provinces.setCities(citiesDao.list(id));
		}
		return provinces;
	}

	@CachePut(key = "#entity.provinceid")
	@Override
	public Provinces update(Provinces entity) {
		provincesDao.update(entity);
		return entity;
	}

	//	@Caching(put = @CachePut(key = "#entity.provinceid"))
	@CachePut(key = "#entity.provinceid")
	@Override
	public Provinces add(Provinces entity) {
		provincesDao.insert(entity);
		System.out.println("新增数据 == 》" + entity);
		return entity;
	}

	@CacheEvict(key = "#id")
	@Override
	public void delete(String id) {
		provincesDao.delete(id);
	}
}

使用SpringCache带来了方便,但是少了灵活性

缓存一致性问题

问题:同时有一个请求 A 进行更新操作,一个请求 B 进行查询操作。可能出现:
(1)请求 A 进行写操作(key = 1 value = 2),先删除缓存 key = 1 value = 1
(2)请求 B 查询发现缓存不存在
(3)请求 B 去数据库查询得到旧值 key = 1 value = 1
(4)请求 B 将旧值写入缓存 key = 1 value = 1
(5)请求 A 将新值写入数据库 key = 1 value = 2
导致缓存中数据永远都是脏数据
比较推荐操作顺序:
先删除缓存,再更新数据库,再删缓存(双删,第二次删可异步延时)

缓存使用带来的一致性问题 — 数据同步 有四类方式
缓存过期与一致性问题 — 缓存一致性问题,无论你怎么做,都有漏洞

缓存失效策略

取决于业务要求的一致性和不一致的容忍度

  1. 固定间隔 ----- 缓存数据2分钟后删除(数据变了,多多海涵)
  2. 定时任务 ----- 每天凌晨统一全量刷新
  3. 实时更新 – 同步去调用Cache增删改
  4. 准实时更新 — 甩锅第三方 观察者模式/发布订阅/MQ
方案名称技术特点优点缺点适用场景
数据实时同步更新强一致性,更新数据库同时更新缓存,使用缓存工具类和或编码实现数据一致性强代码耦合 运行期耦合 影响正常业务数据一致实时性要求比较高的场景,如:银行业务、证券交易;
数据准实时更新准一致性,更新数据库后,异步更新缓存,使用观察者模式/发布订阅/MQ实现;数据同步有较短延迟 与业务解耦 不影响正常业务实现复杂,架构较重不适合写操作频繁并且数据一致实时性要求严格的场景;
缓存失效机制弱一致性,基于缓存本身的失效机制实现简单有一定延迟 不保证强一致性 存在缓存雪崩问题;适合读多写少的场景,能接受一定数据延时;
任务调度更新最终一致性,采用任务调度框架,按照一定频率更新;不影响正常业务;不保证一致性 依赖定时任务 容易堆积垃圾数据;适合复杂统计类数据缓存更新,对数据一致实时性要求低的场景;如:统计类数据,BI分析等;
缓存回收策略

FIFO :First In First Out 先进先出算法,即先放入缓存的先被移除
LRU :Least Recently Used 最久未使用算法,使用时间距离现在最久的那个被移除
LFU:Least Frequently Used 最近最少使用算法 一段时间段内使用次数(频率)最少的那个被移除

TTL:Time To Live 存活期 即从缓存中创建时间点开始直到它到期的一个时间段(不管在这个时间段内有没有访问过期)
TTI : Time To Idle 空闲期,即一个数据多久没有被访问将从缓存中移除的时间
也就是如下的概念:
绝对过期:比如设置10分钟有效,则从数据加入缓存开始算,10分钟后清除掉
滑动过期:比如web中的session机制,如果在最近30分钟之内未被访问,就进行回收

缓存问题

缓存击穿 :某一个key刹那间实现,导致大量的查询打到数据库上
缓存雪崩 :部分缓存key在一段时间集中失效,导致所有的查询都打到数据库上,大量的击穿就是雪崩
缓存穿透:恶意请求不存在的数据,故意避开缓存,大量请求数据库

对于上述缓存同步方案的第三种 缓存失效策略 存在雪崩风险 即某个key失效时,外围刚好有大量并发请求到达 若放任大并发传递到mysql 会大概率造成mysql宕机

  1. 缓存击穿(雪崩)的解决方法:
    限流加锁
  2. 缓存穿透的解决方法
    –》 根据业务特点, 搞出一个规则来效验缓存请求的有效性(有规律的订单号、手机号码)
    –》使用布隆过滤器 布隆过滤器的使用方法 类似于java的set集合,只不过它能以更小的内存,存储更大的数据
/**
 * 解决缓存雪崩 -- > 加锁限流
 * 解决缓存穿透 -- > 布隆过滤器
 */
@Service
public class ProvincesServiceImp2 extends DefaultProvincesService implements ProvincesService {

	// 等效成一个Set集合
	private BloomFilter<String> bf = null;

	@PostConstruct
	public void init() {
		// 在bean初始化完成后,实例化BloomFilter,并加载数据
		List<Provinces> provinces = this.list();
		bf = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), provinces.size());
		for (Provinces province : provinces) {
			bf.put(province.getProvinceid());
		}
	}

	private static final Logger logger = LoggerFactory.getLogger(ProvincesService.class);

	@Resource
	private CacheManager cacheManager;

	private static final String CACHE_NAME = "province";

	@Override
	public Provinces detail(String provinceid) {

		// 1.先判断布隆过滤器中是否存在该值,值存在才允许访问缓存和数据库
		if (!bf.mightContain(provinceid)) {
			System.out.println("非法访问--------" + System.currentTimeMillis());
			return null;
		}

		// 2. 从缓存中取数据
		ValueWrapper valueWrapper = cacheManager.getCache(CACHE_NAME).get(provinceid);
		if (valueWrapper != null) {
			logger.info("缓存中得到数据");
			return (Provinces) valueWrapper.get();
		}

		// 3. 加锁排队 阻塞式锁  32个省 最多只有32把锁 1000个线程
		doLock(provinceid);
		try {
			valueWrapper = cacheManager.getCache(CACHE_NAME).get(provinceid);
			if (valueWrapper != null) {
				logger.info("缓存中得到数据");
				return (Provinces) valueWrapper.get();
			}
			Provinces provinces = super.detail(provinceid);
			// 4.从数据库查询的结果不为空,则把数据放入缓存中,方便下次查询
			if (null != provinces) {
				cacheManager.getCache(CACHE_NAME).put(provinceid, provinces);
			}
			return provinces;
		} catch (Exception e) {
			e.printStackTrace();
			throw new RuntimeException(e);
		} finally {
			// 5. 解锁
			releaseLock(provinceid);
		}

	}
	
	private ConcurrentHashMap<String, Lock> locks = new ConcurrentHashMap<String, Lock>();

	private void releaseLock(String key) {
		ReentrantLock oldLock = (ReentrantLock) locks.get(key);
		if (oldLock != null && oldLock.isHeldByCurrentThread()) {
			oldLock.unlock();
		}
	}

	private void doLock(String key) {
		ReentrantLock newLock = new ReentrantLock();
		Lock oldLock = locks.putIfAbsent(key, newLock);
		if (oldLock == null) {
			newLock.lock();
		} else {
			oldLock.lock();
		}
	}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lang20150928

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值