微服务 Spring Boot 整合Redis 实战开发解决高并发数据缓存

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
注册配置 MSE Nacos/ZooKeeper,118元/月
云原生网关 MSE Higress,422元/月
简介: 高并发场景下,如何巧妙的利用缓存解决,提高系统的可用性? Redis 缓存来搞定!

@[TOC]

一、什么是 缓存?

缓存(Cache),就是数据交换的缓冲区,俗称的缓存就是缓冲区内的数据,一般从数据库中获取,存储于本地代码,例如:

例1:Static final ConcurrentHashMap<K,V> map = new ConcurrentHashMap<>(); 本地用于高并发

例2:static final Cache<K,V> USER_CACHE = CacheBuilder.newBuilder().build(); 用于redis等缓存

例3:Static final Map<K,V> map =  new HashMap(); 本地缓存

由于其被Static修饰,所以随着类的加载而被加载到内存之中,作为本地缓存,由于其又被final修饰,所以其引用(例3:map)和对象(例3:new HashMap())之间的关系是固定的,不能改变,因此不用担心赋值(=)导致缓存失效;

⛅为什么用缓存?

一句话总结: 因为使用了缓存后,效率会大大的提升,减少了不必要的资源消耗,提升了用户体验。

但是使用缓存会增加代码复杂度和运维的成本,例如:Redis 集群,多主多从,等等

在这里插入图片描述

⚡如何使用缓存

在实际开发中,我们会构建缓存来提升系统的稳定、高可用性,使其性能得到进一步的提升。最常用的是 我们 本地数据与Redis 数据库结合使用

浏览器缓存:主要是存在于浏览器端的缓存

应用层缓存: 可以分为tomcat本地缓存,比如map集合,或者是使用redis作为缓存

数据库缓存: 在数据库中有一片空间是 buffer pool (缓冲池),增改查数据都会先加载到mysql的缓存中

CPU缓存: 当代计算机最大的问题是 cpu性能提升了,但内存读写速度没有跟上,所以为了适应当下的情况,增加了cpu的L1,L2,L3级的缓存

在这里插入图片描述

二、实现一个商家缓存

需求说明

本 项目基于 Spring Boot 整合Redis 并引入 MyBatis-Plus 来完成开发

  • 要求达到第一次加载,查询redis缓存是否存在,若不存在,则查询数据库,查询完毕后,存入redis,再次访问时只获取redis缓存中的数据,不必再次加载数据库,减轻数据库压力。

⌛环境搭建

本项目依赖于 3分钟搞懂阿里云服务器部署Reids并整合Spring Boot 微服务项目

数据库 MySQL 8.0
CREATE TABLE `tb_shop` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  `name` varchar(128) NOT NULL COMMENT '商铺名称',
  `type_id` bigint(20) unsigned NOT NULL COMMENT '商铺类型的id',
  `images` varchar(1024) NOT NULL COMMENT '商铺图片,多个图片以'',''隔开',
  `area` varchar(128) DEFAULT NULL COMMENT '商圈,例如陆家嘴',
  `address` varchar(255) NOT NULL COMMENT '地址',
  `x` double unsigned NOT NULL COMMENT '经度',
  `y` double unsigned NOT NULL COMMENT '维度',
  `avg_price` bigint(10) unsigned DEFAULT NULL COMMENT '均价,取整数',
  `sold` int(10) unsigned zerofill NOT NULL COMMENT '销量',
  `comments` int(10) unsigned zerofill NOT NULL COMMENT '评论数量',
  `score` int(2) unsigned zerofill NOT NULL COMMENT '评分,1~5分,乘10保存,避免小数',
  `open_hours` varchar(32) DEFAULT NULL COMMENT '营业时间,例如 10:00-22:00',
  `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE,
  KEY `foreign_key_type` (`type_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT
pom依赖
// Mybatis-Plus 核心依赖
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.4.3</version>
</dependency>

// hutool 工具包,各种封装功能 一应俱全
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.5</version>
</dependency>
核心配置 application.yaml
server:
  port: 8082
spring:
  application:
    name: easydp
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/db_easy_dp?useSSL=false&serverTimezone=UTC
    username: root
    password: 111111
  redis:
    host: redis ip地址
    port: 6379
    password: redis密码,如没有不写即可
    lettuce:
      pool:
        max-active: 10
        max-idle: 10
        min-idle: 1
        time-between-eviction-runs: 10s
  jackson:
    default-property-inclusion: non_null # JSON处理时忽略非空字段
mybatis-plus:
  type-aliases-package: com.chen.entity # 别名扫描包
logging:
  level:
    com.chen: debug

♨️核心源码

Entity 实体类层

package com.chen.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

import java.io.Serializable;
import java.time.LocalDateTime;

/**
 * @author whc
 * @date 2022/9/3 10:29
 */
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("tb_shop")
public class ShopEntity implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * 主键
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    /**
     * 商铺名称
     */
    private String name;

    /**
     * 商铺类型的id
     */
    private Long typeId;

    /**
     * 商铺图片,多个图片以','隔开
     */
    private String images;

    /**
     * 商圈,例如陆家嘴
     */
    private String area;

    /**
     * 地址
     */
    private String address;

    /**
     * 经度
     */
    private Double x;

    /**
     * 维度
     */
    private Double y;

    /**
     * 均价,取整数
     */
    private Long avgPrice;

    /**
     * 销量
     */
    private Integer sold;

    /**
     * 评论数量
     */
    private Integer comments;

    /**
     * 评分,1~5分,乘10保存,避免小数
     */
    private Integer score;

    /**
     * 营业时间,例如 10:00-22:00
     */
    private String openHours;

    /**
     * 创建时间
     */
    private LocalDateTime createTime;

    /**
     * 更新时间
     */
    private LocalDateTime updateTime;

    @TableField(exist = false)
    private Double distance;
}

Mapper持久化层

package com.chen.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.chen.entity.ShopEntity;

/**
 * @author whc
 * @date 2022/9/3 10:33
 */
public interface ShopMapper extends BaseMapper<ShopEntity> {
    
}

Service 接口

package com.chen.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.chen.common.ResultBean;
import com.chen.dto.ShopDTO;
import com.chen.entity.ShopEntity;

/**
 * @author whc
 * @date 2022/9/3 10:35
 */
public interface ShopService extends IService<ShopEntity> {

    ResultBean<ShopDTO> queryById(Long id);
}

ServiceImpl 实现层

package com.chen.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.chen.common.ResultBean;
import com.chen.dto.ShopDTO;
import com.chen.entity.ShopEntity;
import com.chen.mapper.ShopMapper;
import com.chen.service.ShopService;
import com.chen.utils.RedisConstants;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

/**
 * @author whc
 * @date 2022/9/3 10:36
 */
@Slf4j
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, ShopEntity> implements ShopService{

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public ResultBean<ShopDTO> queryById(Long id) {
        try {
            // 拼接 redis key
            String key = RedisConstants.CACHE_SHOP_KEY + id;

            //从redis中获取是否已存在,若存在,则直接返回
            String json = stringRedisTemplate.opsForValue().get(key);

            //判断如果存在,就返回
            if (StrUtil.isNotBlank(json)) {
                ShopDTO shopDTO = JSONUtil.toBean(json, ShopDTO.class);
                return ResultBean.create(0, "success", shopDTO);
            }

            //从数据库查询数据    getById(id) 是 MyBatis-Plus 提供的查询方法,直接调用即可完成查询
            ShopEntity shopEntity = getById(id);
            //转换对象
            ShopDTO shopDTO = BeanUtil.toBean(shopEntity, ShopDTO.class);

            //将数据存入redis
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shopDTO));
            return ResultBean.create(0, "success", shopDTO);
        } catch (Exception e) {
            log.error("获取商品详情失败! e ==> {}", e);
            return ResultBean.create(-1, "获取商品详情失败! e ==> {}" + e);
        }
    }

}

Controller层

package com.chen.controller;

import com.chen.common.ResultBean;
import com.chen.dto.ShopDTO;
import com.chen.service.ShopService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
 * @author whc
 * @date 2022/9/3 11:06
 */
@RestController
@CrossOrigin
@RequestMapping("/shop")
public class ShopController {

    @Autowired
    private ShopService shopService;

    @GetMapping("/{id}")
    public ResultBean<ShopDTO> queryShopById(@PathVariable("id") Long id) {
        return shopService.queryById(id);
    }
}

工具类

package com.chen.utils;

/**
 * redis key 常量
 * @author whc
 * @date 2022/9/3 13:40
 */
public class RedisConstants {

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

    public static final Long CACHE_SHOP_TTL = 30L;
}

✅测试接口

这里我使用了Redis可视化工具,RESP,地址:https://resp.app/zh/

打开后可以直接连接你的redis数据库,可视化展示

在这里插入图片描述

利用 ApiFox测试接口,可参考 【云原生】前后端分离项目下 如何优雅的联调程序?
在这里插入图片描述

第一次调用耗时 1.61s ,是因为我们第一次redis中无数据,走了查询数据库的操作,然后存入redis,总耗时1.61s

第二次调用

在这里插入图片描述

第二次调用直接走的缓存,可见效率提升了很多!

三、采用 微服务 Spring Boot 注解开启缓存

开启注解启动缓存

Spring 默认支持缓存,但版本必须在3.1以上,在启动类加入 @EnableCaching 开启即可

package com.chen;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

/**
 * @author whc
 * @date 2022/9/3 10:27
 */
//开启缓存支持
@EnableCaching
@MapperScan("com.chen.mapper")
@SpringBootApplication
public class MainApplication {

    public static void main(String[] args) {
        SpringApplication.run(MainApplication.class, args);
    }
}

✂️@CacheEnable 注解详解

@CacheEnable: 缓存存在,则使用缓存;不存在,则执行方法,并将结果塞入缓存

ShopServiceImpl 实现类

 @Cacheable(cacheNames = "shop", key = "#root.methodName")
    public ShopDTO queryById(Long id) {
        try {
            String key = RedisConstants.CACHE_SHOP_KEY + id;

            String json = stringRedisTemplate.opsForValue().get(key);

            if (StrUtil.isNotBlank(json)) {
                ShopDTO shopDTO = JSONUtil.toBean(json, ShopDTO.class);
                return shopDTO;
            }

            ShopEntity shopEntity = getById(id);
            //转换对象
            ShopDTO shopDTO = BeanUtil.toBean(shopEntity, ShopDTO.class);

            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shopDTO), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
            return shopDTO;
        } catch (Exception e) {
            log.error("获取商品详情失败! e ==> {}", e);
            return null;
        }
    }

➿调用接口测试

第一次调用,耗时很长

在这里插入图片描述

再次调用,走缓存

在这里插入图片描述

查看Redis可视化key

在这里插入图片描述

大小 1.11k 字节

再看json存入

在这里插入图片描述

大小 653 字节

综上考虑,出于内存的原因,我们选择使用json存入redis,更省内存!

⛵小结

以上就是【Bug 终结者】对 猿创征文 微服务 Spring Boot 整合Redis 实战开发解决高并发数据缓存 的简单介绍,缓存是我们比较常用的技术,在解决一些高并发场景下,我们巧妙的使用缓存可以极大的减轻服务器的压力,从而提高系统的高可用性,Redis基于内存并且是单线程的,所以说非常的快Redis缓存数据库很重要!

如果这篇【文章】有帮助到你,希望可以给【 Bug 终结者】点个赞👍,创作不易,如果有对【 后端技术】、【 前端领域】感兴趣的小可爱,也欢迎关注❤️❤️❤️ 【 Bug 终结者】❤️❤️❤️,我将会给你带来巨大的【收获与惊喜】💝💝💝!
相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
14天前
|
NoSQL Redis
Redis的数据淘汰策略有哪些 ?
Redis 提供了 8 种数据淘汰策略,分为淘汰易失数据和淘汰全库数据两大类。易失数据淘汰策略包括:volatile-lru、volatile-lfu、volatile-ttl 和 volatile-random;全库数据淘汰策略包括:allkeys-lru、allkeys-lfu 和 allkeys-random。此外,还有 no-eviction 策略,禁止驱逐数据,当内存不足时新写入操作会报错。
47 16
|
13天前
|
缓存 NoSQL 关系型数据库
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
本文详解缓存雪崩、缓存穿透、缓存并发及缓存预热等问题,提供高可用解决方案,帮助你在大厂面试和实际工作中应对这些常见并发场景。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
|
15天前
|
存储 缓存 NoSQL
【赵渝强老师】基于Redis的旁路缓存架构
本文介绍了引入缓存后的系统架构,通过缓存可以提升访问性能、降低网络拥堵、减轻服务负载和增强可扩展性。文中提供了相关图片和视频讲解,并讨论了数据库读写分离、分库分表等方法来减轻数据库压力。同时,文章也指出了缓存可能带来的复杂度增加、成本提高和数据一致性问题。
【赵渝强老师】基于Redis的旁路缓存架构
|
14天前
|
缓存 NoSQL 关系型数据库
Redis和Mysql如何保证数据⼀致?
在项目中,为了解决Redis与Mysql的数据一致性问题,我们采用了多种策略:对于低一致性要求的数据,不做特别处理;时效性数据通过设置缓存过期时间来减少不一致风险;高一致性但时效性要求不高的数据,利用MQ异步同步确保最终一致性;而对一致性和时效性都有高要求的数据,则采用分布式事务(如Seata TCC模式)来保障。
52 14
|
8天前
|
缓存 NoSQL PHP
Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出
本文深入探讨了Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出。文章还介绍了Redis在页面缓存、数据缓存和会话缓存等应用场景中的使用,并强调了缓存数据一致性、过期时间设置、容量控制和安全问题的重要性。
24 5
|
14天前
|
存储 NoSQL 算法
Redis分片集群中数据是怎么存储和读取的 ?
Redis集群采用哈希槽分区算法,共有16384个哈希槽,每个槽分配到不同的Redis节点上。数据操作时,通过CRC16算法对key计算并取模,确定其所属的槽和对应的节点,从而实现高效的数据存取。
43 13
|
14天前
|
存储 NoSQL Redis
Redis的数据过期策略有哪些 ?
Redis 采用两种过期键删除策略:惰性删除和定期删除。惰性删除在读取键时检查是否过期并删除,对 CPU 友好但可能积压大量过期键。定期删除则定时抽样检查并删除过期键,对内存更友好。默认每秒扫描 10 次,每次检查 20 个键,若超过 25% 过期则继续检查,单次最大执行时间 25ms。两者结合使用以平衡性能和资源占用。
38 11
|
14天前
|
监控 NoSQL 测试技术
【赵渝强老师】Redis的AOF数据持久化
Redis 是内存数据库,提供数据持久化功能,支持 RDB 和 AOF 两种方式。AOF 以日志形式记录每个写操作,支持定期重写以压缩文件。默认情况下,AOF 功能关闭,需在 `redis.conf` 中启用。通过 `info` 命令可监控 AOF 状态。AOF 重写功能可有效控制文件大小,避免性能下降。
|
14天前
|
存储 监控 NoSQL
【赵渝强老师】Redis的RDB数据持久化
Redis 是内存数据库,提供数据持久化功能以防止服务器进程退出导致数据丢失。Redis 支持 RDB 和 AOF 两种持久化方式,其中 RDB 是默认的持久化方式。RDB 通过在指定时间间隔内将内存中的数据快照写入磁盘,确保数据的安全性和恢复能力。RDB 持久化机制包括创建子进程、将数据写入临时文件并替换旧文件等步骤。优点包括适合大规模数据恢复和低数据完整性要求的场景,但也有数据完整性和一致性较低及备份时占用内存的缺点。
|
16天前
|
消息中间件 NoSQL Java
Spring Boot整合Redis
通过Spring Boot整合Redis,可以显著提升应用的性能和响应速度。在本文中,我们详细介绍了如何配置和使用Redis,包括基本的CRUD操作和具有过期时间的值设置方法。希望本文能帮助你在实际项目中高效地整合和使用Redis。
35 1
下一篇
无影云桌面