返回顶部
首页 > 资讯 > 后端开发 > 其他教程 >利用Redis进行数据缓存的项目实践
  • 120
分享到

利用Redis进行数据缓存的项目实践

2024-04-02 19:04:59 120人浏览 安东尼
摘要

目录1. 引言2. 将信息添加到缓存的业务流程3. 实现代码3.1 代码实现(信息添加到缓存中)3.2 缓存更新策略3.3 实现主动更新4. 缓存穿透4.1 解决缓存穿透(使用空对象

1. 引言

缓存有啥用?

缓存有啥缺点?

  • 如何保证数据库与缓存的数据一致性问题?
  • 维护缓存代码
  • 搭建缓存一般是以集群的形式进行搭建,需要运维的成本

2. 将信息添加到缓存的业务流程

在这里插入图片描述

上图可以清晰的了解Redis项目中所处的位置,是数据库与客户端之间的一个中间件,也是数据库的保护伞。有了Redis可以帮助数据库进行请求的阻挡,阻止请求直接打入数据库,提高响应速率,极大的提升了系统的稳定性。

3. 实现代码

在这里插入图片描述

下面将根据查询商铺信息来作为背景进行代码书写,具体的流程图如上所示。

3.1 代码实现(信息添加到缓存中)

public static final String SHOPCACHEPREFIX = "cache:shop:";
    
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    // JSON工具
    ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public Result queryById(Long id) {
        //从Redis查询商铺缓存
        String cacheShop = stringRedisTemplate.opsForValue().get(SHOPCACHEPREFIX + id);

        //判断缓存中数据是否存在
        if (!StringUtil.isNullOrEmpty(cacheShop)) {
            //缓存中存在则直接返回
            try {
                // 将子字符串转换为对象
                Shop shop = objectMapper.readValue(cacheShop, Shop.class);
                return Result.ok(shop);
            } catch (jsonProcessingException e) {
                e.printStackTrace();
            }
        }

        //缓存中不存在,则从数据库里进行数据查询
        Shop shop = getById(id);

        //数据库里不存在,返回404
        if (null==shop){
            return Result.fail("信息不存在");
        }
        //数据库里存在,则将信息写入Redis
        try {
            String shopJSon = objectMapper.writeValueAsString(shop);
          stringRedisTemplate.opsForValue().set(SHOPCACHEPREFIX+id,shopJSon,30,TimeUnit.MINUTES);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        //返回
        return Result.ok(shop);
    }

3.2 缓存更新策略

数据库与缓存数据一致性问题,当数据库信息修改后,缓存的信息应该如何处理?

 内存淘汰超时剔除主动更新
说明不需要自己进行维护,利用Redis的淘汰机制进行数据淘汰给缓存数据添加TTL编写业务逻辑,在修改数据库的同时更新缓存
一致性差劲一般
维护成本

这里其实是需要根据业务场景来进行选择

  • 高一致性:选主动更新
  • 低一致性:内存淘汰和超时剔除

3.3 实现主动更新

此时需要实现数据库与缓存一致性问题,在这个问题之中还有多个问题值得深思

删除缓存还是更新缓存?
当数据库发生变化时,我们如何处理缓存中无效的数据,是删除它还是更新它?
更新缓存:每次更新数据库都更新缓存,无效写操作较多
删除缓存:更新数据库时删除缓存,查询时再添加缓存
由此可见,选择删除缓存是高效的。

如何保证缓存与数据库的操作的同时成功或失败?
单体架构:单体架构中采用事务解决
分布式架构:利用分布式方案进行解决

先删除缓存还是先操作数据库?

在这里插入图片描述

并发情况下,上述情况是极大可能会发生的,这样子会导致缓存与数据库数据库不一致。

请添加图片描述

先操作数据库,在操作缓存这种情况,在缓存数据TTL刚好过期时,出现一个A线程查询缓存,由于缓存中没有数据,则向数据库中查询,在这期间内有另一个B线程进行数据库更新操作和删除缓存操作,当B的操作在A的两个操作间完成时,也会导致数据库与缓存数据不一致问题。

完蛋!!!两种方案都会造成数据库与缓存一致性问题的发生,那么应该如何来进行选择呢?

虽然两者方案都会造成问题的发生,但是概率上来说还是先操作数据库,再删除缓存发生问题的概率低一些,所以可以选择先操作数据库,再删除缓存的方案。

个人见解:
如果说我们在先操作数据库,再删除缓存方案中线程B删除缓存时,我们利用java来删除缓存会有Boolean返回值,如果是false,则说明缓存已经不存在了,缓存不存在了,则会出现上图的情况,那么我们是否可以根据删除缓存的Boolean值来进行判断是否需要线程B来进行缓存的添加(因为之前是需要查询的线程来添加缓存,这里考虑线程B来添加缓存,线程B是操作数据库的缓存),如果线程B的添加也在线程A的写入缓存之前完成也会造成数据库与缓存的一致性问题发生。那么是否可以延时一段时间(例如5s,10s)再进行数据的添加,这样子虽然最终会统一数据库与缓存的一致性,但是若是在这5s,10s内又有线程C,D等等来进行缓存的访问呢?C,D线程的访问还是访问到了无效的缓存信息。
所以在数据库与缓存的一致性问题上,除非在写入正确缓存之前拒绝相关请求进行服务器来进行访问才能避免用户访问到错误信息,但是拒绝请求对用户来说是致命的,极大可能会导致用户直接放弃使用应用,所以我们只能尽可能的减少问题可能性的发生。(个人理解,有问题可以在评论区留言赐教)

  @Override
    @Transactional
    public Result updateShop(Shop shop) {
        Long id = shop.getId();
        if (null==id){
            return Result.fail("店铺id不能为空");
        }
        //更新数据库
        boolean b = updateById(shop);
        //删除缓存
        stringRedisTemplate.delete(SHOPCACHEPREFIX+shop.getId());
        return Result.ok();
    }

4. 缓存穿透

缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。

解决方案:

缓存空对象

在这里插入图片描述

缺点:

  • 空间浪费
  • 如果缓存了空对象,在空对象的有效期内,我们后台在数据库新增了和空对象相同id的数据,这样子就会造成数据库与缓存一致性问题

布隆过滤器

在这里插入图片描述

优点:

内存占用少

缺点:

  • 实现复杂
  • 存在误判的可能(存在的数据一定会判断成功,但是不存在的数据也有可能会放行进来,有几率造成缓存穿透)

4.1 解决缓存穿透(使用空对象进行解决)

public static final String SHOPCACHEPREFIX = "cache:shop:";

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    // JSON工具
    ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public Result queryById(Long id) {
        //从Redis查询商铺缓存
        String cacheShop = stringRedisTemplate.opsForValue().get(SHOPCACHEPREFIX + id);

        //判断缓存中数据是否存在
        if (!StringUtil.isNullOrEmpty(cacheShop)) {
            //缓存中存在则直接返回
            try {
                // 将子字符串转换为对象
                Shop shop = objectMapper.readValue(cacheShop, Shop.class);
                return Result.ok(shop);
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
        }

        // 因为上面判断了cacheShop是否为空,如果进到这个方法里面则一定是空,直接过滤,不打到数据库
        if (null != cacheShop){
            return Result.fail("信息不存在");
        }

        //缓存中不存在,则从数据库里进行数据查询
        Shop shop = getById(id);

        //数据库里不存在,返回404
        if (null==shop){
            // 缓存空对象
            stringRedisTemplate.opsForValue().set(SHOPCACHEPREFIX+id,"",2,TimeUnit.MINUTES);
            return Result.fail("信息不存在");
        }
        //数据库里存在,则将信息写入Redis
        try {
            String shopJSon = objectMapper.writeValueAsString(shop);
            stringRedisTemplate.opsForValue().set(SHOPCACHEPREFIX+id,shopJSon,30,TimeUnit.MINUTES);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        //返回
        return Result.ok(shop);
    }

上述方案终究是被动方案,我们可以采取一些主动方案,例如

  • 给id加复杂度
  • 权限
  • 热点参数的限流

5. 缓存雪崩

缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

解决方案:

  • 给不同的Key的TTL添加随机值
    大量的Key同时失效,极大可能是TTL相同,我们可以随机给TTL
  • 利用Redis集群提高服务的可用性
  • 给缓存业务添加降级限流策略
  • 给业务添加多级缓存

6. 缓存击穿

缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

常见的解决方案:

  • 互斥锁
  • 逻辑过期

互斥锁:

在这里插入图片描述

即采用锁的方式来保证只有一个线程去重建缓存数据,其余拿不到锁的线程休眠一段时间再重新重头去执行查询缓存的步骤

优点:

  • 没有额外的内存消耗(针对下面的逻辑过期方案)
  • 保证了一致性

缺点:

  • 线程需要等待,性能受到了影响
  • 可能会产生死锁

逻辑过期:

在这里插入图片描述

逻辑过期是在缓存数据中额外添加一个属性,这个属性就是逻辑过期的属性,为什么要使用这个来判断是否过期而不使用TTL呢?因为使用TTL的话,一旦过期,就获取不到缓存中的数据了,没有拿到锁的线程就没有旧的数据可以返回。

它与互斥锁最大的区别就是没有线程的等待了,谁先获取到锁就去重建缓存,其余线程没有获取到锁就返回旧数据,不去做休眠,轮询去获取锁。

重建缓存会新开一个线程去执行重建缓存,目的是减少抢到锁的线程的响应时间。

优点:

线程无需等待,性能好

缺点:

  • 不能保证一致性
  • 缓存中有额外的内存消耗
  • 实现复杂

两个方案各有优缺点:一个保证了一致性,一个保证了可用性,选择与否主要看业务的需求是什么,侧重于可用性还是一致性。

6.1 互斥锁代码

互斥锁的锁用什么?

使用Redis命令的setnx命令。

首先实现获取锁和释放锁的代码

    
    private boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    
    private void unLock(String key) {
        stringRedisTemplate.delete(key);
    }

在这里插入图片描述

代码实现

public Shop queryWithMutex(Long id) throws InterruptedException {
        //从Redis查询商铺缓存
        String cacheShop = stringRedisTemplate.opsForValue().get(SHOPCACHEPREFIX + id);

        //判断缓存中数据是否存在
        if (!StringUtil.isNullOrEmpty(cacheShop)) {
            //缓存中存在则直接返回
            try {
                // 将子字符串转换为对象
                Shop shop = objectMapper.readValue(cacheShop, Shop.class);
                return shop;
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
        }

        // 因为上面判断了cacheShop是否为空,如果进到这个方法里面则一定是空,直接过滤,不打到数据库
        if (null != cacheShop) {
            return null;
        }

        Shop shop = new Shop();
        // 缓存击穿,获取锁
        String lockKey = "lock:shop:" + id;
        try{
            boolean b = tryLock(lockKey);
            if (!b) {
                // 获取锁失败了
                Thread.sleep(50);
                return queryWithMutex(id);
            }
            //缓存中不存在,则从数据库里进行数据查询
           shop = getById(id);

            //数据库里不存在,返回404
            if (null == shop) {
                // 缓存空对象
                stringRedisTemplate.opsForValue().set(SHOPCACHEPREFIX + id, "", 2, TimeUnit.MINUTES);
                return null;
            }
            //数据库里存在,则将信息写入Redis
            try {
                String shopJSon = objectMapper.writeValueAsString(shop);
                stringRedisTemplate.opsForValue().set(SHOPCACHEPREFIX + id, shopJSon, 30, TimeUnit.MINUTES);
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
        }catch (Exception e){

        }finally {
            // 释放互斥锁
            unLock(lockKey);
        }

        //返回
        return shop;

    }

6.2 逻辑过期实现

逻辑过期不设置TTL

在这里插入图片描述

代码实现

@Data
public class RedisData {
    private LocalDateTime expireTime;
    private Object data;
}

由于是热点key,所以key基本都是手动导入到缓存,代码如下

  
    public void saveShopToRedis(Long id,Long expireSeconds){
        // 查询店铺数据
        Shop shop = getById(id);
        // 封装为逻辑过期
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
        // 写入Redis
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id, JSONUtil.toJsonStr(redisData));
    }

逻辑过期代码实现


    public Shop queryWithPassLogicalExpire(Long id) throws InterruptedException {
        //1. 从Redis查询商铺缓存
        String cacheShop = stringRedisTemplate.opsForValue().get(SHOPCACHEPREFIX + id);

        //2. 判断缓存中数据是否存在
        if (StringUtil.isNullOrEmpty(cacheShop)) {
            // 3. 不存在
            return null;
        }
        // 4. 存在,判断是否过期
        RedisData redisData = JSONUtil.toBean(cacheShop, RedisData.class);
        JSONObject jsonObject = (JSONObject) redisData.getData();
        Shop shop = JSONUtil.toBean(jsonObject, Shop.class);
        LocalDateTime expireTime = redisData.getExpireTime();

        // 5. 判断是否过期
        if (expireTime.isAfter(LocalDateTime.now())){
            // 5.1 未过期
            return shop;
        }
        // 5.2 已过期
        String lockKey = "lock:shop:"+id;
        boolean flag = tryLock(lockKey);
        if (flag){
            // TODO 获取锁成功,开启独立线程,实现缓存重建,建议使用线程池去做
            CACHE_REBUILD_EXECUTOR.submit(()->{
                try {
                    // 重建缓存
                    this.saveShopToRedis(id,1800L);
                }catch (Exception e){
                    
                }finally {
                    // 释放锁
                    unLock(lockKey);
                }
             
            });

        }
        // 获取锁失败,返回过期的信息
        return shop;
    }

    
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

到此这篇关于利用Redis进行数据缓存的项目实践的文章就介绍到这了,更多相关Redis 数据缓存内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

--结束END--

本文标题: 利用Redis进行数据缓存的项目实践

本文链接: https://lsjlt.com/news/150983.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • 利用Redis进行数据缓存的项目实践
    目录1. 引言2. 将信息添加到缓存的业务流程3. 实现代码3.1 代码实现(信息添加到缓存中)3.2 缓存更新策略3.3 实现主动更新4. 缓存穿透4.1 解决缓存穿透(使用空对象...
    99+
    2024-04-02
  • 怎么在javaWeb项目中使用Redis进行缓存
    怎么在javaWeb项目中使用Redis进行缓存?针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。一:serviceImpl定义:@Service  &nb...
    99+
    2023-05-30
  • Spring项目中使用Cache Redis实现数据缓存
    目录Spring项目中实现数据缓存一、Spring Cache + Redis 介绍二、项目中集成1. 引入依赖2. 添加 redis 配置类3. 配置文件增加 redis 配置4....
    99+
    2024-04-02
  • node.js利用redis数据库缓存数据的方法
    一、运行redis Redis服务器默认使用6379端口 redis-server 自定义端口 redis-server port 6390 客户端 redis-cli 指定ip和端口连接...
    99+
    2022-06-04
    缓存 数据库 方法
  • 怎么在ssm项目中使用redis缓存查询数据
    怎么在ssm项目中使用redis缓存查询数据?相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。xml配置:<bean id=&qu...
    99+
    2024-04-02
  • Redis作为缓存,mysql的数据如何与redis进行同步?
    Redis作为缓存,mysql的数据如何与redis进行同步? 一定要设置前提,先介绍业务背景 延时双删 双写一致性:当修改了数据库的数据也要同时更新缓存的数据,缓存和数据库的数据要保持一致 读操...
    99+
    2023-10-11
    redis
  • 浅谈redis缓存在项目中的使用
    背景 Redis 是一个开源的内存数据结构存储系统。 可以作为数据库、缓存和消息中间件使用。 支持多种类型的数据结构。 Redis 内置了 复制(replication),LUA脚...
    99+
    2024-04-02
  • 利用MySQL开发实现数据缓存与加速的项目经验探讨
    随着互联网的快速发展,大量的数据被不断生成和存储。对于开发者来说,如何高效地处理和管理这些数据成为一个非常重要的挑战。在这个过程中,数据缓存和加速成为了一个关键的技术。MySQL作为一个常见的关系型数据库管理系统,具有良好的性能和稳定性,被...
    99+
    2023-11-02
    数据缓存 加速 MySQL开发
  • Memcache缓存技术在PHP项目中的应用和实践
    Memcache是一种开源的、分布式的缓存技术。它通过将数据存储在内存中,极大地提高了数据的访问速度,从而提升了网站的性能和响应速度。在PHP项目中,Memcache缓存技术也被广泛应用,并且取得了很好的效果。本篇文章将深入探讨Memcac...
    99+
    2023-05-17
    Memcache 缓存技术 PHP项目
  • SpringBoot2 中怎么利用Redis数据库实现缓存管理
    SpringBoot2 中怎么利用Redis数据库实现缓存管理,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。一、Redis简介Spring Boot中除了对常用...
    99+
    2023-06-02
  • SpringBoot项目中使用redis缓存的方法步骤
    本文介绍了SpringBoot项目中使用redis缓存的方法步骤,分享给大家,具体如下:Spring Data Redis为我们封装了Redis客户端的各种操作,简化使用。 - 当Redis当做数据库或者消息队列来操作时,我们一般使用Red...
    99+
    2023-05-30
    spring boot redis
  • 怎样用redis实现数据库缓存
    怎样用redis实现数据库缓存?针对这个问题,这篇文章给出了相对应的分析和解答,希望能帮助更多想解决这个问题的朋友找到更加简单易行的办法。创建并登陆redis数据库,这是使用的是商业版,也可以自建 &nbs...
    99+
    2024-04-02
  • ASP、存储、Spring和Unix:如何在项目中进行最佳实践?
    在软件开发中,使用最佳实践是非常重要的。它可以提高代码的质量、可维护性和可扩展性。本文将介绍如何在ASP、存储、Spring和Unix等技术中使用最佳实践。 ASP最佳实践 ASP是一种Web开发技术,它可以用于创建动态Web应用程序。以...
    99+
    2023-10-18
    存储 spring unix
  • 在项目中使用redis做缓存的一些思路
    目录在项目中redis做缓存的一些思路首先,缓存的对象有三种本人走过的一些弯路为什么没用Redis做缓存使用Table作本地缓存使用Redis作缓存让我们来思考一下下面几个问题那么使...
    99+
    2024-04-02
  • Springboot/Springcloud项目集成redis进行存取的过程解析
    目录一、redis去官网https://redis.io/download下载后解压二、在项目中添加redis依赖三、redis的使用四、字符串类型存取(k,v)五、存取对象(k,o...
    99+
    2024-04-02
  • json数据怎么利用hibernate进行存取
    这篇文章将为大家详细讲解有关json数据怎么利用hibernate进行存取,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。一、场景public class OrderModel {privat...
    99+
    2023-05-31
    hibernate json
  • redis实现缓存分页数据的方法
    这篇文章将为大家详细讲解有关redis实现缓存分页数据的方法,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。几种思路:每页数据分别使用一个key缓存,各自设置过期时间,在数...
    99+
    2024-04-02
  • Spring Cache+Redis缓存数据的实现示例
    目录1、为什么使用缓存2、常用的缓存注解2.1 @Cacheable2.2 @CacheEvict2.3、@Cacheput2.4、@Caching2.5、@CacheConfig3...
    99+
    2024-04-02
  • 使用SpringBoot怎么对Redis进行集成来实现缓存
    本篇文章给大家分享的是有关使用SpringBoot怎么对Redis进行集成来实现缓存,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着小编一起来看看吧。Redis简介Redis 是一个开源(BSD许...
    99+
    2023-05-31
    springboot redis
  • Redis通过在Spring Boot项目中使用实现集中式缓存
    这篇文章将为大家详细讲解有关Redis通过在Spring Boot项目中使用实现集中式缓存,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。 利用Spring Initializr来新建一个sp...
    99+
    2023-05-31
    springboot 集中 redis
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作