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

目录
  • 1. 引言
  • 2. 将信息添加到缓存的业务流程
  • 3. 实现代码
    • 3.1 代码实现(信息添加到缓存中)
    • 3.2 缓存更新策略
    • 3.3 实现主动更新
  • 4. 缓存穿透
    • 4.1 解决缓存穿透(使用空对象进行解决)
  • 5. 缓存雪崩
  • 6. 缓存击穿
    • 6.1 互斥锁代码
    • 6.2 逻辑过期实现

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命令。

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

    /**
     * 尝试获取锁
     *
     * @param key
     * @return
     */
    private boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    /**
     * 删除锁
     *
     * @param key
     */
    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基本都是手动导入到缓存,代码如下

  /**
     * 逻辑过期时间对象写入缓存
     * @param id
     * @param expireSeconds
     */
    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));
    }

逻辑过期代码实现

/**
     * 缓存击穿:逻辑过期解决
     * @param id
     * @return
     * @throws InterruptedException
     */
    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 数据缓存内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • spring整合redis实现数据缓存的实例代码

    数据缓存原因:有些数据比较多,如果每次访问都要进行查询,无疑给数据库带来太大的负担,将一些庞大的查询数据并且更新次数较少的数据存入redis,能为系统的性能带来良好的提升. 业务逻辑思路:登入系统,访问数据时,检查redis是否有缓存,有则直接从redis中提取,没有则从数据库查询出,并存入redis中做缓存. 为什么要用redis做缓存: (1)异常快速:Redis的速度非常快,每秒能执行约11万集合,每秒约81000+条记录. (2)支持丰富的数据类型:Redis支持最大多数开发人员已经知道

  • 详解Spring Boot使用redis实现数据缓存

    基于spring Boot 1.5.2.RELEASE版本,一方面验证与Redis的集成方法,另外了解使用方法. 集成方法 1.配置依赖 修改pom.xml,增加如下内容. <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> 2.配置R

  • Spring项目中使用Cache Redis实现数据缓存

    目录 Spring项目中实现数据缓存 一.Spring Cache + Redis 介绍 二.项目中集成 1. 引入依赖 2. 添加 redis 配置类 3. 配置文件增加 redis 配置 4. 启动安装好的 redis 三.Spring Cache 常用注解介绍 1. @Cacheable 2. @CachePut 3. @CacheEvict 四.功能里实现缓存操作 查询操作 导入操作 Spring项目中实现数据缓存 有时候我们为了提高查询速度,会使用缓存,但是并不是所有数据都适合放到缓存

  • Python的Flask框架使用Redis做数据缓存的配置方法

    Redis是一款依据BSD开源协议发行的高性能Key-Value存储系统.会把数据读入内存中提高存取效率.Redis性能极高能支持超过100K+每秒的读写频率,还支持通知key过期等等特性,所以及其适合做缓存. 下载安装 根据redis中文网使用wget下载压缩包 $ wget http://download.redis.io/releases/redis-3.0.5.tar.gz $ tar xzf redis-3.0.5.tar.gz $ cd redis-3.0.5 $ make 二进制文

  • 使用Spring Data Redis实现数据缓存的方法

    引言 目前很多系统为了解决数据读写的性能瓶颈,在系统架构设计中使用Redis实现缓存,Spring框架为了让开发人员更加方便快捷的使用Redis实现缓存,对Redis的操作进行了包装. 0.缓存 个人理解的缓存是指用于存储频繁使用的数据的空间,关注点是存储数据的空间和使用频繁的数据.缓存技术,简单的说就是先从缓存中查询数据是否存在,存在则直接返回,不存在再执行相应的操作获取数据,并将获取的数据存储到缓存中,它是一种提升系统性能的重要方法. 1.Redis Redis是一个开源的.内存存储key-

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

    目录 1. 引言 2. 将信息添加到缓存的业务流程 3. 实现代码 3.1 代码实现(信息添加到缓存中) 3.2 缓存更新策略 3.3 实现主动更新 4. 缓存穿透 4.1 解决缓存穿透(使用空对象进行解决) 5. 缓存雪崩 6. 缓存击穿 6.1 互斥锁代码 6.2 逻辑过期实现 1. 引言 缓存有啥用? 降低对数据库的请求,减轻服务器压力 提高了读写效率 缓存有啥缺点? 如何保证数据库与缓存的数据一致性问题? 维护缓存代码 搭建缓存一般是以集群的形式进行搭建,需要运维的成本 2. 将信息添加

  • Springboot+redis+Vue实现秒杀的项目实践

    目录 1.Redis简介 2.实现代码 3.启动步骤 4.使用ab进行并发测试 5.线程安全 6.总结 7.参考资料 1.Redis简介 Redis是一个开源的key-value存储系统. Redis的五种基本类型:String(字符串),list(链表),set(集合),zset(有序集合),hash,stream(Redis5.0后的新数据结构) 这些数据类型都支持push/pop.add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的. Redis的应用场景为配合关

  • Spring Boot项目利用Redis实现集中式缓存实例

    在高并发请求的web服务架构中,随着数据量的提升,缓存机制为绝大多数的后台开发所使用.这篇文章主要介绍如何在Spring Boot项目中为Entity添加利用Redis实现的集中式缓存. 1. 利用Spring Initializr来新建一个spring boot项目 2. 在pom.xml中添加redis.mysql和cache等相关依赖.一般情况下,缓存一般是在大规模数据库存储下所需要的 <dependency> <groupId>org.springframework.boo

  • 浅谈redis缓存在项目中的使用

    背景 Redis 是一个开源的内存数据结构存储系统. 可以作为数据库.缓存和消息中间件使用. 支持多种类型的数据结构. Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence). 通过 Redis 哨兵(Sentinel)和 Redis 集群(Cluster)的自动分区,提供高可用性(high availability). 基本数

  • Golang并发读取文件数据并写入数据库的项目实践

    目录 需求 项目结构 获取data目录下的文件 按行读取文本数据 数据类型定义 并发读取文件 将数据写入数据库 完整main.go代码 测试运行 需求 最近接到一个任务,要把一批文件中的十几万条JSON格式数据写入到Oracle数据库中,Oracle是企业级别的数据库向来以高性能著称,所以尽可能地利用这一特性.当时第一时间想到的就是用多线程并发读文件并操作数据库,而Golang是为并发而生的,用Golang进行并发编程非常方便,因此这里选用Golang并发读取文件并用Gorm操作数据库.然而Go

随机推荐