Redis进阶

Redis是一种key-value的数据库,key一般是String类型,value的类型多种多样,redis的数据结构通常指的都是value的类型

什么是缓存?

缓存就是数据交换的缓存区(Cache),是存储数据的临时 地方,读写性能较高

缓存的作用

降低后端的负载(数据库压力)

提高读写效率,降低响应时间

缓存的成本

数据一致性成本(双写一致问题)

代码维护成本(缓存三兄弟)

运维成本(高可用,集群模式)

一.Spring框架集成Redis的Java客户端

SpringData是Spring中数据操作的模块,包含对各种数据库的集成,其中对Redis的集成模块就叫做SpringDataRedis

1.提供了对不同Redis客户端的整合(Lettuce和Jedis,SpringBoot2.x起默认选择Lettuce)

2.提供了RedisTemplate统一API来操作Redis

3.支持Redis哨兵和Redis集群

4.支持基于JDK,JSON,字符串,String对象的数据序列化及反序列化

SpringDataRedis的序列化方式

首先介绍什么是序列化?什么是反序列化?

序列化:在数据传输的时候,将内存中的对象转换为可以存储或传输的格式(字节流)的过程

反序列化:将存储或传输的格式转换回内存中的对象的过程

SpringDataRedis默认使用的是JDK中提供的序列化工具,通常我们在使用RedisTemple时都会写一个配置类,将序列化器和反序列化器改为String方式,来对对象进行序列化,反序列化

那么为什么需要我们重新指定序列化器和反序列化器,为什么不使用默认提供的jdk中的序列化器和反序列化器?

首先我们要知道,在网络传输过程中数据都是以字节流的形式传输的,也就是数组保存,传输的数据不可读,看着像乱码,我们要将对象转为JSON的格式存储

尽管JSON的序列化方式可以满足可读需求,但是还存在一些问题

它会将JSON格式存储的对象对象的类地址保存,用于反序列化,存入redis中会带来额外的内存开销

StringRedisTemple是Spring提供的一个类,它的key和value序列化方式默认就是String方式,所以我们在使用的时候,键值都得用String类型,要存储对象时,把对象转为JSON字符串存储,或者将对象中的数据都转为String类型

StringRedisTemplate和RedisTemplate的区别

StringRedisTemple存储对象时不需要存储类的地址,但需要我们手动将json格式字符串转化为对象

二.使用redis作为缓存中间件

在项目中我们经常会使用缓存来减轻数据库的压力,在高并发的业务场景下,数据库是扛不住大量请求,这时候我们就需要增加一个或多个中间层来减轻数据库压力,而redis就是将数据存储到缓存中,读写速度远远大于从磁盘中读写

首先我们要明白什么样的数据需要添加到缓存中:需要频繁查询的数据

java 复制代码
    @Override
    public List<ShopType> queryTypeList() {
        //先查询缓存判断是否存在
        String key = "cache:shopType";
        String shopTypeJson = stringRedisTemplate.opsForValue().get(key);
        if(StrUtil.isNotBlank(shopTypeJson))
        {
            List<ShopType> shopTypes = JSONUtil.toList(shopTypeJson,ShopType.class);
            return shopTypes;
        }
        List<ShopType> shopTypes = query().orderByAsc("sort").list();
        if(shopTypes == null)
        {
            return null;
        }
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shopTypes));
        return shopTypes;
    }

缓存更新策略

内存淘汰:在缓存内存不足时,redis会主动淘汰部分数据

超时剔除:给缓存添加数据的时候设置过期时间

主动更新:编写业务逻辑,在数据库更新的时候,主动去更新缓存

操作缓存和数据库时有三个问题需要考虑

1.删除缓存还是更新缓存?

更新缓存:每次更新数据库都更新缓存,无效写操作较多,浪费内存资源

删除缓存:更新数据库时让缓存失效,查询时再更新缓存,这样即使无效的写操作,我们也不会主动去同步的更新缓存

2.如何保证缓存与数据库的操作同时成功或失败?

单体系统:将缓存与数据库操作放在一个事务中,也就是将操作缓存和操作数据库写在一个方法内,给Service层添加Transational注解,让spring帮我们统一管理事务

分布式系统:利用TCC等分布式事务方案

3.先操作缓存还是先操作数据库?

并发产生的双写一致性问题

数据库中数据被修改,我们需要同步的去删除缓存

无论是先操作数据库还是先删除缓存,都会出现有线程读取到脏数据的情况,推荐使用先操作数据库的方式,而且两种方式如果没有上锁的话都是弱一致,都是最终确保数据的一致性

三.缓存击穿,缓存穿透,缓存雪崩及解决方案

缓存击穿

概述:缓存击穿是指一个热点key过期或这个缓存业务重建复杂,恰好再这个时间点有大量请求到来,查询缓存没有,请求全部打到数据库中,给数据库带来巨大压力

解决方案:互斥锁,逻辑过期

**互斥锁:**互相等待,只有一个线程在重建缓存,其他线程都在等待,性能差

在使用互斥锁方案中,如果是多集群模式,需要使用分布式锁,因为在每一个Tomact服务器中都有一把锁,多个服务器就会有多把锁

**逻辑过期:**再添加缓存数据时,不设置过期时间,只增加一个过期字段,对数据一致性需求高的业务不可用,因为在重建缓存之前,如果数据库中的数据更改了,没有获得锁的线程直接返回了旧数据

为什么需要开辟一个线程单独重构缓存,在加入双写一致性问题中不是解决了双写一致问题吗?

首先,需要明确的是在逻辑过期方案中写缓存业务的时候,操作数据库时不会删除缓存,因为逻辑过期就是为了这个缓存key一直存在,所以在逻辑过期解决方案中是需要在缓存逻辑过期后去更新缓存的,确保缓存中的是新数据

缓存穿透

概述:缓存穿透是指客户端请求的数据在缓存和数据库中都不存在,这样缓存永远都不不会生效,请求全部打到数据库,这种情况一般是遭到了恶意攻击

解决方案:缓存空对象,布隆过滤

**缓存空对象:**当数据库中也查询不到这个数据,将这个数据的值存为null存到缓存中

优点**:**实现简单,维护方便

缺点**:**额外的内存消耗,可能造成短期的数据不一致(在构建缓存后,数据库有了数据)

布隆过滤:

**概述:**布隆过滤是一种算法,底层使用BitMap来实现,也就是一个bit数组,用于快速判断一个元素是否在一个集合中

优点:内存空间占用少,没有多余的key

缺点:实现复杂,存在误差(数组越长,误判率越低)

缓存雪崩

概述:在同一时间点内,大量的缓存同时过期或redis服务宕机,导致请求全部到达数据库,带来巨大压力

大量缓存失效:给缓存失效时间添加随机值

redis服务宕机

利用redis集群提高服务的可用性

给业务添加多级缓存

给缓存业务添加降级限流策略

示例代码:

java 复制代码
 private Shop queryWithMutex(Long id){
        String key = RedisConstants.CACHE_SHOP_KEY +id;
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        if(StrUtil.isNotBlank(shopJson)){

            Shop shop = JSONUtil.toBean(shopJson,Shop.class);
            return shop;
        }
        if(shopJson != null)
        {
            return null;
        }
        // TODO 实现缓存重建,获取互斥锁
        String lockKey = "lock:shop"+id;
        Shop shop = null;
        // TODO 判断是否获取成功
        try {
            boolean isLock = tryLock(lockKey);
            if(!isLock){
                // TODO 失败,则休眠重试
                Thread.sleep(50);
                return queryWithMutex(id);//递归重试
            }
            // TODO 成功,查询数据库
            shop = getById(id);//Mybatis-Plus提供的单表查询
            //模拟重建延时
            Thread.sleep(200);
            if(shop == null){
                stringRedisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }
            stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        }catch(InterruptedException e){
            throw new RuntimeException(e);
        }finally{
            unlock(lockKey);
        }
        //TODO 写入缓存,释放互斥锁
        return shop;
    }
相关推荐
诗9趁年华2 小时前
Cache-Aside模式下Redis与MySQL数据一致性问题分析
数据库·redis·mysql
L.EscaRC2 小时前
Redis 底层运行机制与原理浅析
数据库·redis·缓存
爱吃烤鸡翅的酸菜鱼2 小时前
Java【缓存设计】定时任务+分布式锁实战:Redis vs Redisson实现状态自动扭转以及全量刷新预热机制
java·redis·分布式·缓存·rabbitmq
我科绝伦(Huanhuan Zhou)2 小时前
Redis 生产环境安全基线配置指南:从风险分析到实操加固
数据库·redis·安全
彩旗工作室2 小时前
如何在自己的服务器上部署 n8n
开发语言·数据库·nodejs·n8n
小白起 v2 小时前
自动更新工期触发器(MYSQL)
数据库·sql·oracle
JIngJaneIL2 小时前
数码商城系统|电子|基于SprinBoot+vue的商城推荐系统(源码+数据库+文档)
java·前端·数据库·vue.js·论文·毕设·数码商城系统
堕落年代4 小时前
Spring三级缓存通俗易懂讲解
java·spring·缓存
~我爱敲代码~5 小时前
使用XSHELL远程操作数据库
数据库·adb