Redis
1、加菜单进Redis缓存
@Override
public Map<String,List<Catelog2Vo>> listTest() {
//从缓存查询数据
String category = stringRedisTemplate.opsForValue().get("category");
if(StringUtils.isEmpty(category)) {
System.out.println("查询数据库。。。。");
Map<String,List<Catelog2Vo>> entities = getCategoryTest();
String s = JSON.toJSONString(entities);
stringRedisTemplate.opsForValue().set("category",s);
return entities;
}
return JSON.parseObject(category,new TypeReference<Map<String,List<Catelog2Vo>>>(){});
}
2、加锁解决缓存击穿
缓存击穿:缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力,加锁解决。
缓存雪崩:缓存中大量数据同一时间过期,可以设置过期时间不一致
缓存穿透:大量请求不存在的数据,可以在缓存中设置null值
查看缓存,无缓存则查询数据库,并设置完缓存再解锁。
3、分布式锁
本地锁只能锁住当前服务,微服务情况下有多个服务,Redis分布式锁就是锁住Redis
①、设置过期时间
@Override
public Map<String, List<Catelog2Vo>> listTestWithRedisLock() {
Map<String, List<Catelog2Vo>> categoryTest = getCategoryTest();
String s = JSON.toJSONString(categoryTest);
//分布式加锁,设置过期时间,放在服务宕机,锁长期占不释放
Boolean category = stringRedisTemplate.opsForValue().setIfAbsent("category", s, 300, TimeUnit.SECONDS);
if(category){
//加锁成功,执行业务
}
else{
//加锁失败,自旋
return listTestWithRedisLock();
}
return categoryTest;
}
②、删除锁
如果业务时间太长,过期时间太短,则有可能删除了别的服务占的锁,因此应该业务做完再删除锁。
Map<String, List<Catelog2Vo>> categoryTest = getCategoryTest();
String s = JSON.toJSONString(categoryTest);
UUID uuid = UUID.randomUUID();
//分布式加锁,设置过期时间,放在服务宕机,锁长期占不释放
Boolean category = stringRedisTemplate.opsForValue().setIfAbsent("category", uuid, 300, TimeUnit.SECONDS);
if(category){
//加锁成功,执行业务
//.......
//业务执行完成,删除锁
String category1 = stringRedisTemplate.opsForValue().get("category");
if(!StringUtils.isEmpty(category1)&&category.equals(uuid)){
stringRedisTemplate.delete("category");
}
}
else{
//加锁失败,自旋
return listTestWithRedisLock();
}
return categoryTest;
③、lua脚本 原子性删除锁
有可能网络原因,业务完成时间为9.5秒,过期时间为10秒,当从redis传回来确定是自己的锁,想删时却删掉了其它服务占的锁。
获取值对比+对比成功删除=原子性
lua脚本
@Override
public Map<String, List<Catelog2Vo>> listTestWithRedisLock() throws InterruptedException {
Map<String, List<Catelog2Vo>> categoryTest = null;
String uuid = UUID.randomUUID().toString();
//分布式加锁,设置过期时间,放在服务宕机,锁长期占不释放
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
if(lock){
System.out.println("获取分布式锁成功。。。。");
//加锁成功,执行业务
try{
categoryTest = listTest();
}finally {
//业务执行完成,删除锁
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Long execute = stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),
Arrays.asList("lock"), uuid);
return categoryTest;
}
}
else{
//加锁失败,自旋
System.out.println("获取分布式锁失败。。。。重试");
Thread.sleep(1000);
return listTestWithRedisLock();
}
}
4、Redisson
1.锁自动续期(看门狗),不用担心业务时间过长,锁被删除,宕机后30秒自动解锁
加锁(设置释放时间)
//设置过期时间的话,不会自动续期
myLock.lock(10,TimeUnit.SECONDS);
2、读写锁
/**
* 保证一定能读到最新数据,修改期间,写锁是一个排它锁(互斥锁、独享锁),读锁是一个共享锁
* 写锁没释放读锁必须等待
* 读 + 读 :相当于无锁,并发读,只会在Redis中记录好,所有当前的读锁。他们都会同时加锁成功
* 写 + 读 :必须等待写锁释放
* 写 + 写 :阻塞方式
* 读 + 写 :有读锁。写也需要等待
* 只要有读或者写的存都必须等待
* @return
*/
@GetMapping(value = "/write")
@ResponseBody
public String writeValue() {
RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
String s=UUID.randomUUID().toString();
RLock rLock = readWriteLock.writeLock();
try{
rLock.lock();
Thread.sleep(20000);
stringRedisTemplate.opsForValue().set("writeValue",s);
}catch (Exception e){
}finally {
rLock.unlock();
}
return s;
}
@GetMapping(value = "/read")
@ResponseBody
public String readValue() {
RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
String s="";
RLock rLock = readWriteLock.readLock();
try{
rLock.lock();
Thread.sleep(10000);
s=stringRedisTemplate.opsForValue().get("writeValue");
}catch (Exception e){
}finally {
rLock.unlock();
}
return s;
}
3、信号量
/**
* 车库停车
* 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";
}
4、缓存和数据库一致性
双写模式,失效模式
解决方案:
如果是用户纬度数据(订单数据、用户数据),这种只有用户自己发起操作,并发几率非常小,不用考虑这个问题,缓存数据加上过期时间,每隔一段时间触发读的主动更新即可
如果是菜单,商品介绍等基础数据,也可以去使用canal订阅binlog的方式
缓存数据+过期时间也足够解决大部分业务对于缓存的要求。
通过读写锁可以实现,(对于经常读写的效率就会变低);
总结:
我们能放入缓存的数据本就不应该是实时性、一致性要求超高的。所以缓存数据的时候加上过期时间,保证每天拿到当前最新数据即可。
我们不应该过度设计,增加系统的复杂性
遇到实时性、一致性要求高的数据,就应该查数据库,即使慢点。
5、SpringCache
①、简化缓存的操作
1、配置依赖
<dependency>
<groupId>org.springframework.b oot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
2、指定缓存类型
spring:
cache:
#指定缓存类型为redis
type: redis
redis:
# 指定redis中的过期时间为1h
time-to-live: 3600000
3、在主配置类上加上注解@EnableCaching
②、自定义内容
1、自定义ttl
# 指定redis中的过期时间为1h
time-to-live: 3600000
2、指定缓存的key值
key = "#root.method.name"
3、配置缓存的value为json
//CacheProperties为一个配置文件类,通过@EnableConfigurationProperties可以将CacheProperties加入进容器
@EnableConfigurationProperties(CacheProperties.class)
@Configuration
@EnableCaching
public class MyCacheConfig {
// @Autowired
// public CacheProperties cacheProperties;
/**
* 配置文件的配置没有用上
* 加入容器的方法直接用容器里的组件,不用@Autowired
* @return
*/
@Bean
public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
// config = config.entryTtl();
//配置key,value的存储格式(GenericJackson2JsonRedisSerializer为泛型)
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;
}
}
③、SpringCache原理与不足
1)、读模式
缓存穿透:查询一个null数据。解决方案:缓存空数据,可通过spring.cache.redis.cache-null-values=true
缓存击穿:大量并发进来同时查询一个正好过期的数据。解决方案:加锁 ? 默认是无加锁的;
使用sync = true来解决击穿问题
缓存雪崩:大量的key同时过期。解决:加随机时间。
2)、写模式:(缓存与数据库一致)
读写加锁。
引入Canal,感知到MySQL的更新去更新Redis
读多写多,直接去数据库查询就行
3)、总结:
常规数据(读多写少,即时性,一致性要求不高的数据,完全可以使用Spring-Cache):
写模式(只要缓存的数据有过期时间就足够了)
特殊数据:特殊设计