本地简单使用缓存
//本地缓存,通过hashmap简单示例
private Map<String,Object> cache=new HashMap<>();
public Object getJsonData() {
if(!cache.containsKey("jsonCache")){
//执行查询业务
Object object = null;
cache.put("jsonCache",object);
return object;
}
return cache.get("jsonCache");
}
以上的方式只是简单的缓存使用,并不适用于多机器一起运行的情况
整合Redis
导入匹配的版本依赖(如果有父pom文件管理版本就无需指定版本)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置redis主机地址(暂时使用单机,后续可以使用多节点、哨兵模式、集群模式)
spring:
redis:
host: yourIp
port: 6379
后续在spring中可以直接使用两个对象
//需要自己定义下数据的序列化使用
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
//提供了str、hash中key及valye的序列化为string
@Bean
@ConditionalOnMissingBean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
在项目中的简单验证
@Autowired
private StringRedisTemplate redisTemplate;
public void testRedisTemplate(){
redisTemplate.opsForValue().set("hello","world");
String hello = redisTemplate.opsForValue().get("hello");
System.out.println(hello);
}
redis压力测试下存在OutOfDirectMemoryError问题
产生原因:
1)、springboot2.0以后默认使用lettuce操作redis的客户端,它使用通信
2)、lettuce的bug导致netty堆外内存溢出
解决方案:由于是lettuce的bug造成,不能直接使用-Dio.netty.maxDirectMemory去调大虚拟机堆外内存
1)、升级lettuce客户端。 2)、切换使用jedis<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <exclusions> <exclusion> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency>
缓存不正确使用造成的问题
1)缓存穿透
指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的null写入缓存,
这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义
风险: 利用不存在的数据进行攻击,数据库瞬时压力增大,最终导致崩溃
解决: null结果缓存,并加入短暂过期时间
2)缓存雪崩
缓存雪崩是指在我们设置缓存时key采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时 压力过重雪崩。
解决: 原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
3)缓存击穿
对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。
如果这个key在大量请求同时进来前正好失效,那么所有对这个key的数据查询都落到db,我们称为缓存击穿。
解决: 加锁。大量并发只让一个去查,其他人等待,查到以后释放锁,其他人获取到锁,先查缓存,就会有数据,不用去db
独立运行机器加锁解决方式(无法解决分布式的情况)
@Autowired
private StringRedisTemplate redisTemplate;
public String testRedisTemplate2(){
String hello = redisTemplate.opsForValue().get("hello");
if(StringUtil.isBlank(hello)){
synchronized (this){
//二次查询缓存 保证在多线程情况下只查询一次数据库
hello = redisTemplate.opsForValue().get("hello");
if(StringUtil.isNotBlank(hello)){
return hello;
}
//TODO 自己的业务逻辑
String value = "world";
//放入缓存中
redisTemplate.opsForValue().set("hello",value);
}
}
return hello;
}
redis锁解决分布式的方式
方式一
@Autowired
private StringRedisTemplate redisTemplate;
public String testRedisTemplateRedisLock(){
String hello = redisTemplate.opsForValue().get("hello");
if(StringUtil.isBlank(hello)){
return getDBData();
}
return hello;
}
public String getDBData() {
// 如果不存在lock中key时就进行创建,存在创建失败
// 存在问题:1、如果执行到自己的业务逻辑时系统中断,导致lock一直存在,那么就会产生死锁
Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent("lock", "1");
if (aBoolean) {
try {
//二次查询缓存
String hello = redisTemplate.opsForValue().get("hello");
if(StringUtil.isNotBlank(hello)){
return hello;
}
//TODO 自己的业务逻辑
String value = "world";
//放入缓存中
redisTemplate.opsForValue().set("hello",value);
return value;
}finally {
redisTemplate.delete("lock");
}
}else{
// 自旋调用
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getDBData();
}
}
方式二
@Autowired
private StringRedisTemplate redisTemplate;
public String testRedisTemplateRedisLock(){
String hello = redisTemplate.opsForValue().get("hello");
if(StringUtil.isBlank(hello)){
return getDBData2();
}
return hello;
}
public String getDBData2() {
// 如果不存在lock中key时就进行创建,存在创建失败,并设置过期时间
// 存在问题:1、如果执行到自己的业务时间超过锁的过期时间,就是业务还在执行,但是锁已经过期,就会导致锁失效(后续可以锁续期)
Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent("lock", "1",1000, TimeUnit.MICROSECONDS);
if (aBoolean) {
try {
//二次查询缓存
String hello = redisTemplate.opsForValue().get("hello");
if(StringUtil.isNotBlank(hello)){
return hello;
}
//TODO 自己的业务逻辑
String value = "world";
//放入缓存中
redisTemplate.opsForValue().set("hello",value);
return value;
}finally {
redisTemplate.delete("lock");
}
}else{
// 自旋调用
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getDBData();
}
}
方式三
@Autowired
private StringRedisTemplate redisTemplate;
public String testRedisTemplateRedisLock(){
String hello = redisTemplate.opsForValue().get("hello");
if(StringUtil.isBlank(hello)){
return getDBData2();
}
return hello;
}
public String getDBData3() {
//增加唯一ID。保证如果是自己加了锁,只有自己才可以关闭
String s = UUID.randomUUID().toString();
// 如果不存在lock中key时就进行创建,存在创建失败,并设置过期时间
// 存在问题:1、如果执行到自己的业务时间超过锁的过期时间,就是业务还在执行,但是锁已经过期,就会导致锁失效(后续可以锁续期)
Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent("lock", s,1000, TimeUnit.MICROSECONDS);
if (aBoolean) {
try {
//二次查询缓存
String hello = redisTemplate.opsForValue().get("hello");
if (StringUtil.isNotBlank(hello)) {
return hello;
}
//TODO 自己的业务逻辑
String value = "world";
//放入缓存中
redisTemplate.opsForValue().set("hello",value);
return value;
} finally {
//但是此处进行移除是非原子性的,redis中一条执行命令才是线程安全的
String lock = redisTemplate.opsForValue().get("lock");
if (s.equals(lock)) {
redisTemplate.delete("lock");
}
}
}else{
// 自旋调用
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getDBData();
}
}
方式四
@Autowired
private StringRedisTemplate redisTemplate;
public String testRedisTemplateRedisLock(){
String hello = redisTemplate.opsForValue().get("hello");
if(StringUtil.isBlank(hello)){
return getDBData2();
}
return hello;
}
public String getDBData4() {
//增加唯一ID。保证如果是自己加了锁,只有自己才可以关闭
String s = UUID.randomUUID().toString();
// 如果不存在lock中key时就进行创建,存在创建失败,并设置过期时间
// 存在问题:1、如果执行到自己的业务时间超过锁的过期时间,就是业务还在执行,但是锁已经过期,就会导致锁失效(后续可以锁续期)
Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent("lock", s,1000, TimeUnit.MICROSECONDS);
if (aBoolean) {
try {
//二次查询缓存
String hello = redisTemplate.opsForValue().get("hello");
if (StringUtil.isNotBlank(hello)) {
return hello;
}
//TODO 自己的业务逻辑
String value = "world";
//放入缓存中
redisTemplate.opsForValue().set("hello",value);
return value;
} finally {
// 通过lua脚本保证redis执行为线程安全
String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
" return redis.call(\"del\",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";
redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), s);
}
}else{
// 自旋调用
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getDBData();
}
}
redisson锁解决分布式的方式
导入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.4</version>
</dependency>
开启配置
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient(){
Config config = new Config();
config.useSingleServer().setAddress("redis://yourIP:6379");
RedissonClient redisson = Redisson.create(config);
return redisson;
}
}
redisson分布式锁
//Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。
@Autowired
private RedissonClient redissonClient;
public String testRedisTemplateRedisLock(){
String hello = redisTemplate.opsForValue().get("hello");
if(StringUtil.isBlank(hello)){
return getDBData2();
}
return hello;
}
public String getDBData4() {
RLock lock = redissonClient.getLock("getDBData4-Lock");
try {
//二次查询缓存
String hello = redisTemplate.opsForValue().get("hello");
if (StringUtil.isNotBlank(hello)) {
return hello;
}
//TODO 自己的业务逻辑
String value = "world";
//放入缓存中
redisTemplate.opsForValue().set("hello",value);
return value;
} finally {
lock.unlock();
}
}
可重入锁(Reentrant Lock)
public String getDBData4() {
RLock lock = redissonClient.getLock("getDBData4-Lock");
try {
//二次查询缓存
String hello = redisTemplate.opsForValue().get("hello");
if (StringUtil.isNotBlank(hello)) {
return hello;
}
//TODO 自己的业务逻辑
String value = "world";
//放入缓存中
redisTemplate.opsForValue().set("hello",value);
return value;
} finally {
lock.unlock();
}
}
读写锁(ReadWriteLock)
// 写锁会阻塞读锁,但是读锁不会阻塞读锁,但读锁会阻塞写锁
// 总之含有写的过程都会被阻塞,只有读读不会被阻塞
// 如果在读中有写锁触发、那么写锁会等待读锁执行完后再执行。
@GetMapping("/read")
@ResponseBody
public String read() {
RReadWriteLock lock = redissonClient.getReadWriteLock("ReadWrite-Lock");
RLock rLock = lock.readLock();
String s = "";
try {
rLock.lock();
System.out.println("读锁加锁"+Thread.currentThread().getId());
Thread.sleep(5000);
s= redisTemplate.opsForValue().get("lock-value");
}finally {
rLock.unlock();
return "读取完成:"+s;
}
}
@GetMapping("/write")
@ResponseBody
public String write() {
RReadWriteLock lock = redissonClient.getReadWriteLock("ReadWrite-Lock");
RLock wLock = lock.writeLock();
String s = UUID.randomUUID().toString();
try {
wLock.lock();
System.out.println("写锁加锁"+Thread.currentThread().getId());
Thread.sleep(10000);
redisTemplate.opsForValue().set("lock-value",s);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
wLock.unlock();
return "写入完成:"+s;
}
}
信号量(Semaphore)
// 可以理解为在一个临界区内可以允许N个资源同时进行,资源为0时,临界期不允许进入了
@GetMapping("/park")
@ResponseBody
public String park() {
RSemaphore park = redissonClient.getSemaphore("park");
try {
park.acquire(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "停进2";
}
@GetMapping("/go")
@ResponseBody
public String go() {
RSemaphore park = redissonClient.getSemaphore("park");
park.release(2);
return "开走2";
}
闭锁(CountDownLatch)
// 可以理解为放闸的开关,当满足放闸条件时,await就不进行阻塞
@GetMapping("/setLatch")
@ResponseBody
public String setLatch() {
RCountDownLatch latch = redissonClient.getCountDownLatch("CountDownLatch");
try {
latch.trySetCount(5);
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
return "门栓被放开";
}
@GetMapping("/offLatch")
@ResponseBody
public String offLatch() {
RCountDownLatch latch = redissonClient.getCountDownLatch("CountDownLatch");
latch.countDown();
return "门栓被放开1";
}
缓存一致性问题
双写模式
当数据更新时,更新数据库时同时更新缓存
这是暂时性的脏数据问题,但是在数据稳定,缓存过期以后,又能得到最新的正确数据
失效模式
数据库更新时将缓存删除
存在问题
当两个请求同时修改数据库,一个请求已经更新成功并删除缓存时又有读数据的请求进来,这时候发现缓存中无数据就去数据库中查询并放入缓存,
在放入缓存前第二个更新数据库的请求成功,这时候留在缓存中的数据依然是第一次数据更新的数据
解决方法
1、缓存的所有数据都有过期时间,数据过期下一次查询触发主动更新 2、读写数据的时候(并且写的不频繁),加上分布式的读写锁。
2、如果是用户纬度数据(订单数据、用户数据),这种并发几率非常小,不用考虑这个问题,缓存数据加上过期时间,每隔一段时间触发读的主动更新即可
如果是菜单,商品介绍等基础数据,也可以去使用canal订阅binlog的方式。
缓存数据+过期时间也足够解决大部分业务对于缓存的要求。
通过加锁保证并发读写,写写的时候按顺序排好队。读读无所谓。所以适合使用读写锁。(业务不关心 脏数据,允许临时脏数据可忽略)
总结
我们能放入缓存的数据本就不应该是实时性、一致性要求超高的。所以缓存数据的时候加上过期时间,保证每天拿到当前最新数据即可。
我们不应该过度设计,增加系统的复杂性
遇到实时性、一致性要求高的数据,就应该查数据库,即使慢点。
SpringCache
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
启动springBoot自动装配@EnableCaching(可能新版本需要该注解就自动配置了)
自定义配置
spring:
cache:
#指定缓存类型为redis
type: redis
redis:
///指定redis中的过期时间为1h
time-to-live: 3600000
默认使用jdk进行序列化,自定义序列化方式需要编写配置类
@Configuration
public class MyCacheConfig {
@Bean
public org.springframework.data.redis.cache.RedisCacheConfiguration redisCacheConfiguration(
CacheProperties cacheProperties) {
org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration
.defaultCacheConfig();
//指定缓存序列化方式为json
config = config.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
//下面的逻辑是复制过来的
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
//设置配置文件中的各项配置,如过期时间
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixKeysWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
}
springCache的使用
//调用该方法时会将结果缓存,缓存名为 testType,key为方法名
//表示该方法的缓存被读取时会加锁(本地锁)
@Cacheable(value = {"testType"},key = "#root.methodName",sync = true)
public Map<String, List<ObejctVo>> getJsonDbWithSpringCache() {
return getJsONDb();
}
//调用该方法会删除缓存testType下的所有cache
@Override
@CacheEvict(value = {"testType"},allEntries = true)
public void updateCascade(JsonEntity entity) {
this.updateById(entity);
if (!StringUtils.isEmpty(entity.getName())) {
jsonService.update(entity);
}
}
Spring-Cache的不足之处
1)、读模式
缓存穿透:查询一个null数据。解决方案:缓存空数据,可通过spring.cache.redis.cache-null-values=true
缓存击穿:大量并发进来同时查询一个正好过期的数据。解决方案:加锁 ? 默认是无加锁的;
使用sync = true来解决击穿问题
缓存雪崩:大量的key同时过期。解决:加随机时间。加上过期时间
2)、写模式:(缓存与数据库一致)
a、读写加锁。
b、引入Canal,感知到MySQL的更新去更新Redis
c 、读多写多,直接去数据库查询就行
3)、总结:
常规数据(读多写少,即时性,一致性要求不高的数据,完全可以使用Spring-Cache):
写模式(只要缓存的数据有过期时间就足够了)
特殊数据:特殊设计
SpringCache注解详情
1、Cache接口:缓存接口,定义缓存操作。实现有 如RedisCache、EhCacheCache、ConcurrentMapCache等
2、cacheResolver:指定获取解析器
3、CacheManager:缓存管理器,管理各种缓存(Cache)组件;如:RedisCacheManager,使用redis作为缓存。指定缓存管理器
4、@Cacheable:
在方法执行前查看是否有缓存对应的数据,如果有直接返回数据,如果没有会调用方法获取数据返回,并缓存起来。
只是Sqel表达式进行key定义
5、@CacheEvict:将一条或多条数据从缓存中删除。
6、@CachePut:将方法的返回值放到缓存中
7、@EnableCaching:开启缓存注解功能
8、@Caching:组合多个缓存注解;可组合(@Cacheable、@CacheEvict、@CachePut)
9、@CacheConfig:统一配置@Cacheable中的value值