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;
    }
相关推荐
学编程的小程几秒前
从“单模冲锋”到“多模共生”——2026 国产时序数据库新物种进化图谱
数据库·时序数据库
卓怡学长1 分钟前
m111基于MVC的舞蹈网站的设计与实现
java·前端·数据库·spring boot·spring·mvc
存在的五月雨6 分钟前
Redis的一些使用
java·数据库·redis
小冷coding7 小时前
【MySQL】MySQL 插入一条数据的完整流程(InnoDB 引擎)
数据库·mysql
鲨莎分不晴8 小时前
Redis 基本指令与命令详解
数据库·redis·缓存
专注echarts研发20年8 小时前
工业级 Qt 业务窗体标杆实现・ResearchForm 类深度解析
数据库·qt·系统架构
周杰伦的稻香10 小时前
MySQL中常见的慢查询与优化
android·数据库·mysql
冉冰学姐10 小时前
SSM学生社团管理系统jcjyw(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面
数据库·ssm 框架·学生社团管理系统·多角色管理
nvd1111 小时前
深入分析:Pytest异步测试中的数据库会话事件循环问题
数据库·pytest
appearappear11 小时前
如何安全批量更新数据库某个字段
数据库