本地缓存和分布式缓存_价值成长的博客-CSDN博客
读模式的缓存失效问题_价值成长的博客-CSDN博客
写模式的缓存一致性问题_价值成长的博客-CSDN博客
使用分布式锁解决读模式缓存失效(缓存击穿)和写模式缓存一致性问题!!!
公共代码:
CategoryBrandRelationService.java
package com.atguigu.gulimall.product.service;
import com.atguigu.gulimall.product.entity.BrandEntity;
import com.baomidou.mybatisplus.extension.service.IService;
import com.atguigu.common.utils.PageUtils;
import com.atguigu.gulimall.product.entity.CategoryBrandRelationEntity;
import java.util.List;
import java.util.Map;
/**
* 品牌分类关联
*
* @author jude
* @email [email protected]
* @date 2022-08-24 13:21:08
*/
public interface CategoryBrandRelationService extends IService {
PageUtils queryPage(Map params);
void saveDetail(CategoryBrandRelationEntity categoryBrandRelation);
void updateBrand(Long brandId, String name);
void updateCategory(Long catId, String name);
List getBrandsByCatId(Long catId);
}
CategoryServiceImpl.java:
getCatalogJson()
@Cacheable(value = "category",key = "#root.methodName")
@Override
public Map> getCatalogJson() {
//给缓存中放json字符串,拿出的json字符串,反序列为能用的对象
//1、加入缓存逻辑,缓存中存的数据是json字符串
//JSON跨语言。跨平台兼容。
String catalogJson = redisTemplate.opsForValue().get("catalogJson");
if (StringUtils.isEmpty(catalogJson)) {
System.out.println("缓存不命中...查询数据库...");
//2、缓存中没有数据,查询数据库
//Map> catalogJsonFromDb = getCatalogJsonFromDbWithLocalLock();
Map> catalogJsonFromDb = getCatalogJsonFromDbWithRedisLock();
// Map> catalogJsonFromDb = getCatalogJsonFromDbWithRedissonLock();
return catalogJsonFromDb;
}
System.out.println("缓存命中...直接返回...");
//转为指定的对象
Map> result = JSON.parseObject(catalogJson,new TypeReference
private Map> getDataFromDb() {
//得到锁以后,我们应该再去缓存中确定一次,如果没有才需要继续查询
String catalogJson = redisTemplate.opsForValue().get("catalogJson");
if (!StringUtils.isEmpty(catalogJson)) {
//缓存不为空直接返回
Map> result = JSON.parseObject(catalogJson, new TypeReference
CategoryServiceImpl.java:
public Map> getCatalogJsonFromDbWithLocalLock() {
//1、synchronized (this):SpringBoot所有的组件在容器中都是单例的。
//TODO 本地锁:synchronized,JUC(Lock),在分布式情况下,想要锁住所有,必须使用分布式锁
synchronized (this) {
//得到锁以后,我们应该再去缓存中确定一次,如果没有才需要继续查询
return getDataFromDb();
}
}
redis分布式锁代码实现(读模式):只实现了类似ReentrantLock
CategoryServiceImpl.java:
getCatalogJsonFromDbWithRedisLock()
public Map> getCatalogJsonFromDbWithRedisLock() {
// 加锁保证原子性 set resource_name anystring NX EX max-lock-time
// 解锁保证原子性 lua脚本
//1、占分布式锁。去redis占坑 设置过期时间必须和加锁是同步的,保证原子性(避免死锁) setIfAbsent==setNX/setNXEX
// uuid保证加锁和删锁是同一个锁
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
if (lock) {
System.out.println("获取分布式锁成功...");
Map> dataFromDb = null;
try {
//加锁成功...执行业务
dataFromDb = getDataFromDb();
} finally {
// lua脚本解锁,可以保证操作的原子性
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
//删除锁
redisTemplate.execute(new DefaultRedisScript(script, Long.class), Arrays.asList("lock"), uuid);
}
return dataFromDb;
} else {
System.out.println("获取分布式锁失败...等待重试...");
//加锁失败,重试机制,休眠一百毫秒
try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
return getCatalogJsonFromDbWithRedisLock(); //自旋的方式
}
}
///加锁
String token = UUID.randomUUID().toString();
String lock = jedis.set(key, token, "NX", "EX",20);
1. Overview · redisson/redisson Wiki · GitHub
Redisson功能:分布式对象,分布式集合,分布式锁
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。其中包括(BitSet
, Set
, Multimap
, SortedSet
, Map
, List
, Queue
, BlockingQueue
, Deque
, BlockingDeque
, Semaphore
, Lock
, AtomicLong
, CountDownLatch
, Publish / Subscribe
, Bloom filter
, Remote service
, Spring cache
, Executor service
, Live Object service
, Scheduler service
) Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。
一句话:将分布式对象存放在redis里。
特点:1. 底层时lua脚本,保证了锁的原子性。 2. 看门狗机制(锁过期自动续期),解决死锁问题。
3.1 eg: IndexController.java
package com.atguigu.gulimall.product.web;
import com.atguigu.gulimall.product.entity.CategoryEntity;
import com.atguigu.gulimall.product.service.CategoryService;
import com.atguigu.gulimall.product.vo.Catelog2Vo;
import org.redisson.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Controller
public class IndexController {
@Autowired
CategoryService categoryService;
@Autowired
RedissonClient redisson;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@ResponseBody
@GetMapping(value = "/hello")
public String hello() {
//1、获取一把锁,只要锁的名字一样,就是同一把锁
RLock lock = redisson.getLock("my-lock");
//2、加锁
lock.lock(); //阻塞式等待。默认加的锁都是30s
//1)、锁的自动续期,如果业务超长,运行期间自动锁上新的30s。不用担心业务时间长,锁自动过期被删掉
//2)、加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认会在30s内自动过期,不会产生死锁问题
// lock.lock(10,TimeUnit.SECONDS); //最佳实战:10秒钟自动解锁,自动解锁时间一定要大于业务执行时间
//问题:在锁时间到了以后,不会自动续期
//1、如果我们传递了锁的超时时间,就发送给redis执行脚本,进行占锁,默认超时就是 我们制定的时间
//2、如果我们指定锁的超时时间,就使用 lockWatchdogTimeout = 30 * 1000 【看门狗默认时间】
//只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】,每隔10秒都会自动的再次续期,续成30秒
// internalLockLeaseTime 【看门狗时间】 / 3, 10s
try {
System.out.println("加锁成功,执行业务..." + Thread.currentThread().getId());
try { TimeUnit.SECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
} catch (Exception ex) {
ex.printStackTrace();
} finally {
//3、解锁 假设解锁代码没有运行,Redisson会不会出现死锁
System.out.println("释放锁..." + Thread.currentThread().getId());
lock.unlock();
}
return "hello";
}
/**
* 保证一定能读到最新数据,修改期间,写锁是一个排它锁(互斥锁、独享锁),读锁是一个共享锁
* 写锁没释放读锁必须等待
* 读 + 读 :相当于无锁,并发读,只会在Redis中记录好,所有当前的读锁。他们都会同时加锁成功
* 写 + 读 :必须等待写锁释放
* 写 + 写 :阻塞方式
* 读 + 写 :有读锁。写也需要等待
* 只要有写的存都必须等待
* @return
*/
@GetMapping(value = "/write")
@ResponseBody
public String writeValue() {
String s = "";
RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
RLock rLock = readWriteLock.writeLock();
try {
//1、改数据加写锁,读数据加读锁
rLock.lock();
s = UUID.randomUUID().toString();
ValueOperations ops = stringRedisTemplate.opsForValue();
ops.set("writeValue",s);
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rLock.unlock();
}
return s;
}
@GetMapping(value = "/read")
@ResponseBody
public String readValue() {
String s = "";
RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
//加读锁
RLock rLock = readWriteLock.readLock();
try {
rLock.lock();
ValueOperations ops = stringRedisTemplate.opsForValue();
s = ops.get("writeValue");
try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }
} catch (Exception e) {
e.printStackTrace();
} finally {
rLock.unlock();
}
return s;
}
/**
* 车库停车
* 3车位
* 信号量也可以做分布式限流
*/
@GetMapping(value = "/park")
@ResponseBody
public String park() throws InterruptedException {
RSemaphore park = redisson.getSemaphore("park");
park.acquire(); //获取一个信号、获取一个值,占一个车位
boolean flag = park.tryAcquire();
if (flag) {
//执行业务
} else {
return "error";
}
return "ok=>" + flag;
}
@GetMapping(value = "/go")
@ResponseBody
public String go() {
RSemaphore park = redisson.getSemaphore("park");
park.release(); //释放一个车位
return "ok";
}
/**
* 放假、锁门
* 1班没人了
* 5个班,全部走完,我们才可以锁大门
* 分布式闭锁
*/
@GetMapping(value = "/lockDoor")
@ResponseBody
public String lockDoor() throws InterruptedException {
RCountDownLatch door = redisson.getCountDownLatch("door");
door.trySetCount(5);
door.await(); //等待闭锁完成
return "放假了...";
}
@GetMapping(value = "/gogogo/{id}")
@ResponseBody
public String gogogo(@PathVariable("id") Long id) {
RCountDownLatch door = redisson.getCountDownLatch("door");
door.countDown(); //计数-1
return id + "班的人都走了...";
}
@GetMapping({"/","index.html"})
public String indexPage(Model model){
List categoryEntities = categoryService.getLevel1Category();
model.addAttribute("categories", categoryEntities);
return "index";
}
@ResponseBody
@GetMapping("/index/catalog.json")
public Map> getCatalog(){
Map> catalogJson = categoryService.getCatalogJson();
return catalogJson;
}
}
3.2 redisson分布式锁代码实现(读模式):
pom.xml:
org.redisson
redisson
3.12.0
MyRedissonConfig:
@Configuration
public class MyRedissonConfig {
/**
* 所有对Redisson的使用都是通过RedissonClient
* @return
* @throws IOException
*/
@Bean(destroyMethod="shutdown")
public RedissonClient redisson() throws IOException {
//单节点模式
//1、创建配置
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.56.10:6371");
//2、根据Config创建出RedissonClient实例
//Redis url should start with redis:// or rediss://
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
CategoryServiceImpl:
getCatalogJsonFromDbWithRedissonLock()
@Autowired
private RedissonClient redissonClient;
public Map> getCatalogJsonFromDbWithRedissonLock() {
//1、占分布式锁。去redis占坑
//(锁的粒度,越细越快:具体缓存的是某个数据,11号商品) product-11-lock
//RLock catalogJsonLock = redissonClient.getLock("catalogJson-lock");
//创建读锁
RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("catalogJson-lock");
RLock rLock = readWriteLock.readLock();
Map> dataFromDb = null;
try {
rLock.lock();
//加锁成功...执行业务
dataFromDb = getDataFromDb();
} finally {
rLock.unlock();
}
return dataFromDb;
}
3.3 redisson分布式锁代码实现(写模式,整合 spring cache:简化缓存开发):
Spring Cache_价值成长的博客-CSDN博客
pom.xml
org.springframework.boot
spring-boot-starter-data-redis
org.springframework.boot
spring-boot-starter-cache
application.properties
spring.cache.type=redis
spring.cache.redis.time-to-live=3600000
# 缓存前缀,如果没有就默认使用缓存的名字作为前缀
#spring.cache.redis.key-prefix=CACHE_
# 是否缓存空置:解决缓存穿透问题
spring.cache.redis.use-key-prefix=true
spring.cache.redis.cache-null-values=true
GulimallProductApplication.java 开启缓存
package com.atguigu.gulimall.product;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
/**
* 1、整合MyBatis-Plus
* 1)、导入依赖
*
* com.baomidou
* mybatis-plus-boot-starter
* 3.2.0
*
* 2)、配置
* 1、配置数据源;
* 1)、导入数据库的驱动。https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-versions.html
* 2)、在application.yml配置数据源相关信息
* 2、配置MyBatis-Plus;
* 1)、使用@MapperScan
* 2)、告诉MyBatis-Plus,sql映射文件位置
*
* 2、逻辑删除
* 1)、配置全局的逻辑删除规则(省略)
* 2)、配置逻辑删除的组件Bean(省略)
* 3)、给Bean加上逻辑删除注解@TableLogic
*
* 3、JSR303
* 1)、给Bean添加校验注解:javax.validation.constraints,并定义自己的message提示
* 2)、开启校验功能@Valid
* 效果:校验错误以后会有默认的响应;
* 3)、给校验的bean后紧跟一个BindingResult,就可以获取到校验的结果
* 4)、分组校验(多场景的复杂校验)
* 1)、 @NotBlank(message = "品牌名必须提交",groups = {AddGroup.class,UpdateGroup.class})
* 给校验注解标注什么情况需要进行校验
* 2)、@Validated({AddGroup.class})
* 3)、默认没有指定分组的校验注解@NotBlank,在分组校验情况@Validated({AddGroup.class})下不生效,只会在@Validated生效;
*
* 5)、自定义校验
* 1)、编写一个自定义的校验注解
* 2)、编写一个自定义的校验器 ConstraintValidator
* 3)、关联自定义的校验器和自定义的校验注解
* @Documented
* @Constraint(validatedBy = { ListValueConstraintValidator.class【可以指定多个不同的校验器,适配不同类型的校验】 })
* @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
* @Retention(RUNTIME)
* public @interface ListValue {
*
* 4、统一的异常处理
* @ControllerAdvice
* 1)、编写异常处理类,使用@ControllerAdvice。
* 2)、使用@ExceptionHandler标注方法可以处理的异常。
*
*
* 5、模板引擎
* 1)、thymeleaf-starter:关闭缓存
* 2)、静态资源都放在static文件夹下就可以按照路径直接访问
* 3)、页面放在templates下,直接访问
* SpringBoot,访问项目的时候,默认会找index
* 4)、页面修改不重启服务器实时更新
* 1)、引入dev-tools
* 2)、修改完页面 controller shift f9重新自动编译下页面,代码配置,推荐重启
*
* 6、整合redis
* 1)、引入data-redis-starter
* 2)、简单配置redis的host等信息
* 3)、使用SpringBoot自动配置好的StringRedisTemplate来操作redis
* redis-》Map;存放数据key,数据值value
*
* 7、整合redisson作为分布式锁等功能框架
* 1)、引入依赖
*
* org.redisson
* redisson
* 3.12.0
*
* 2)、配置redisson
* MyRedissonConfig给容器中配置一个RedissonClient实例即可
* 3)、使用
* 参照文档做。
*
* 8、整合SpringCache简化缓存开发
* 1)、引入依赖
* spring-boot-starter-cache、spring-boot-starter-data-redis
* 2)、写配置
* (1)、自动配置了哪些
* CacheAuroConfiguration会导入 RedisCacheConfiguration;
* 自动配好了缓存管理器RedisCacheManager
* (2)、配置使用redis作为缓存
* spring.cache.type=redis
* 3)、测试使用缓存
* @Cacheable: Triggers cache population.:触发将数据保存到缓存的操作
* @CacheEvict: Triggers cache eviction.:触发将数据从缓存删除的操作
* @CachePut: Updates the cache without interfering with the method execution.:不影响方法执行更新缓存
* @Caching: Regroups multiple cache operations to be applied on a method.:组合以上多个操作
* @CacheConfig: Shares some common cache-related settings at class-level.:在类级别共享缓存的相同配置
* 1)、开启缓存功能 @EnableCaching
* 2)、只需要使用注解就能完成缓存操作
*
* 4)、原理:
* CacheAutoConfiguration -> RedisCacheConfiguration ->
* 自动配置了RedisCacheManager->初始化所有的缓存->每个缓存决定使用什么配置
* ->如果redisCacheConfiguration有就用已有的,没有就用默认配置
* ->想改缓存的配置,只需要给容器中放一个RedisCacheConfiguration即可
* ->就会应用到当前RedisCacheManager管理的所有缓存分区中
*
*/
//@EnableCaching
@EnableRedisHttpSession
@EnableFeignClients(basePackages = "com.atguigu.gulimall.product.feign")
@EnableDiscoveryClient
@MapperScan("com.atguigu.gulimall.product.dao")
@SpringBootApplication
public class GulimallProductApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallProductApplication.class, args);
}
}
MyCacheConfig.java
package com.atguigu.gulimall.product.config;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@EnableConfigurationProperties(CacheProperties.class)
@Configuration
@EnableCaching
public class MyCacheConfig {
// @Autowired
// public CacheProperties cacheProperties;
/**
* 配置文件的配置没有用上
* @return
*/
@Bean
public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
// config = config.entryTtl();
config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
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;
}
}
CategoryServiceImpl.java:
updateCascade()
/*
* 每一个需要缓存的数据我们都来指定要放到那个名字的缓存。【缓存的分区(按照业务类型分)】
* 代表当前方法的结果需要缓存,如果缓存中有,方法都不用调用,如果缓存中没有,会调用方法。最后将方法的结果放入缓存
* 默认行为
* 如果缓存中有,方法不再调用
* key是默认生成的:缓存的名字::SimpleKey::[](自动生成key值)
* 缓存的value值,默认使用jdk序列化机制,将序列化的数据存到redis中
* 默认时间是 -1:
*
* 自定义操作:key的生成
* 指定生成缓存的key:key属性指定,接收一个Spel
* 指定缓存的数据的存活时间:配置文档中修改存活时间
* 将数据保存为json格式
*
*
* 4、Spring-Cache的不足之处:
* 1)、读模式
* 缓存穿透:查询一个null数据。解决方案:缓存空数据
* 缓存击穿:大量并发进来同时查询一个正好过期的数据。解决方案:加锁 ? 默认是无加锁的;使用sync = true来解决击穿问题
* 缓存雪崩:大量的key同时过期。解决:加随机时间。加上过期时间
* 2)、写模式:(缓存与数据库一致)
* 1)、读写加锁。
* 2)、引入Canal,感知到MySQL的更新去更新Redis
* 3)、读多写多,直接去数据库查询就行
*
* 总结:
* 常规数据(读多写少,即时性,一致性要求不高的数据,完全可以使用Spring-Cache):写模式(只要缓存的数据有过期时间就足够了)
* 特殊数据:特殊设计
*
* 原理:
* CacheManager(RedisCacheManager)->Cache(RedisCache)->Cache负责缓存的读写
* @return
*/
/**
* 级联更新所有关联的数据
*
* @CacheEvict:失效模式
* @CachePut:双写模式,需要有返回值
* 1、同时进行多种缓存操作:@Caching
* 2、指定删除某个分区下的所有数据 @CacheEvict(value = "category",allEntries = true)
* 3、存储同一类型的数据,都可以指定为同一分区
* @param category
*/
// 同时设置多个缓存操作
// @Caching(evict = {
// @CacheEvict(value = "category",key = "'getLevel1Categorys'"),
// @CacheEvict(value = "category",key = "'getCatalogJson'")
// })
@CacheEvict(value = "category",allEntries = true) //删除某个分区下的所有数据
@Transactional(rollbackFor = Exception.class)
@Override
public void updateCascade(CategoryEntity category) {
RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("catalogJson-lock");
//创建写锁
RLock rLock = readWriteLock.writeLock();
try {
rLock.lock();
this.baseMapper.updateById(category);
categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());
} catch (Exception e) {
e.printStackTrace();
} finally {
rLock.unlock();
}
//同时修改缓存中的数据
//删除缓存,等待下一次主动查询进行更新
}
CategoryBrandRelationDao.java
updateCategory()
@Mapper
public interface CategoryBrandRelationDao extends BaseMapper {
void updateCategory(@Param("catId") Long catId, @Param("name") String name);
}
CategoryBrandRelationDao.xml
update pms_category_brand_relation set catelog_name=#{name} where catelog_id=#{catId}