Java后端学习日记(四):Springboot 2.X 整合Redis作为数据缓存

专栏目录

Java后端学习日记(一):第一个Springboot应用——Hello World!

Java后端学习日记(二):POJO的基本概念,编写,转化和简化

Java后端学习日记(三):Springboot整合Mybatis-Plus

Java后端学习日记(四):Springboot 2.X 整合Redis作为数据缓存

Java后端学习日记(五):Springboot使用@ControllerAdvice捕获和处理异常


项目源码同步更新中

前言

上一期中我们整合了Mybatis-Plus,对数据库进行CRUD,这一期我们来讲讲数据缓存

如果能用内存查找代替磁盘IO查找,那么效率就会高快几十到上百倍

常用的内存型数据库有Memcached,Redis和mongoDB,本文我们选择Redis进行整合

导航

  • 专栏目录
  • 前言
  • 为什么需要数据缓存
  • 为什么选用Redis
  • Springboot 2.X 整合 Redis
    • 1. 安装 Redis
    • 2. 在 pom.xml 中引入依赖
    • 3. 在 application.yml 中配置
    • 4. 配置RedisUtil工具类操作Redis
      • 1)创建Redis操作工具类
      • 2)编写一般方法
      • 3)私用方法——对象转json
      • 4)字符串操作方法
    • 5. 单元测试
  • Redis其它知识补充
    • 缓存雪崩
    • 缓存穿透
    • 缓存击穿

为什么需要数据缓存

作为一个服务端,用户访问服务端时,大部分的请求都是查找请求,而增删改只是小部分请求。如果因为查找请求而频繁地访问数据库,对数据库来说是一种重担:

  1. 从数据库获取数据是一种磁盘IO操作,速度较慢
  2. 查找请求量过大,容易引起数据库宕机

拿一个场景作为例子:

当电商举办活动,一个商品(假设商品id为3)热卖中,很多用户点击这个商品想要获取这个商品的信息,如果没有缓存的情况下,就会有大量的

# TB_COMMODITY 为商品表
SELECT * FROM TB_COMMODITY WHERE ID=3;

并发访问数据库,且不说磁盘IO效率低下,大量的连接请求会使数据库无力处理其它请求,造成数据库宕机

所以说数据缓存在面对大量用户请求的时候是必不可少的

为什么选用Redis

Redis 是当前互联网世界最为流行的 NoSQL(Not Only SQL)数据库。NoSQL 在互联网系统中的作用很大,因为它可以在很大程度上提高互联网系统的性能。
Redis 具备一定持久层的功能,也可以作为一种缓存工具。对于 NoSQL 数据库而言,作为持久层,它存储的数据是半结构化的,这就意味着计算机在读入内存中有更少的规则,读入速度更快。

Redis的更多介绍自行百度

Redis支持的数据结构众多,性能好,支持持久化,具有原子性(单线程),用户还多…总而言之,redis,yes!

初学Redis可以将Redis先当成一个外置的HashMap^_^,存储键值对

Springboot 2.X 整合 Redis

1. 安装 Redis

见菜鸟教程

2. 在 pom.xml 中引入依赖

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-data-redisartifactId>
    
    <exclusions>
        <exclusion>
            <groupId>io.lettucegroupId>
            <artifactId>lettuce-coreartifactId>
        exclusion>
    exclusions>
dependency>

<dependency>
    <groupId>redis.clientsgroupId>
    <artifactId>jedisartifactId>
dependency>

说明:Spring Boot starter(spring-boot-starter-data-redis)默认使用 lettuce。使用jedis时,需要排除该依赖关系,并包含Jedis。

3. 在 application.yml 中配置

#配置redis
spring:
	redis:
		host:localhost
		password:
		port:6379

Redis默认密码为空,如需配置密码,则将redis.conf 或Redis Windows的 redis.windows.conf中的# requirepass foobared这一行的井号去掉,再将foobared改为你要设置的密码即可

4. 配置RedisUtil工具类操作Redis

org.springframework.data.redis.core.RedisTemplate提供了丰富的api供程序员进行Redis的操作,但相对来说较为复杂,其中:

redisTemplate.opsForValue();//操作字符串

redisTemplate.opsForHash();//操作hash

redisTemplate.opsForList();//操作list

redisTemplate.opsForSet();//操作set

redisTemplate.opsForZSet();//操作有序set

作为数据缓存,redis字符串操作是最为常用的:

  • 缓存存入(set)时,将对象序列化为字符串后作为值进行保存
  • 缓存取出(get)时,将字符串取出后反序列化为对象进行操作

1)创建Redis操作工具类

/**
 * Redis操作工具类
 *
 * @author YuC
 */
@Component
public class RedisUtil {

    // StringRedisTemplate == RedisTemplate
    @Autowired
    private RedisTemplate<String, String> redisTemplate;

}

在类的开头,通过@Autowired获取RedisTemplate的实例

2)编写一般方法

所谓一般方法,就是无所谓你传入的key的数据结构类型的方法

    /**
     * common
     */

    /**
     * 指定缓存失效时间
     *
     * @param key
     * @param time
     * @return
     */
    public boolean expire(String key, long time) {
        try {
            if (time > 0) {
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 获取key过期时间
     *
     * @param key 不能为null
     * @return 时间(秒)| 返回0代表永久有效
     */
    public Long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }

    /**
     * 判断key是否存在
     *
     * @param key
     * @return true存在 | false不存在
     */
    public boolean hasKey(String key) {
        try {
            // 使 redisTemplate.hasKey 返回的空值也变为 false
            return redisTemplate.hasKey(key) != null;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 删除缓存
     *
     * @param key
     */
    public void delete(String... key) {
        if (key != null && key.length > 0) {
            redisTemplate.delete(new ArrayList<>(Arrays.asList(key)));
        }
    }

这里对RedisTemplate中的api进行包装,增加了易用性

3)私用方法——对象转json

    /**
     * 对象转json工具
     *
     * @param object
     * @return
     */
    private String objectToJson(Object object) {
        if (object instanceof Integer ||
                object instanceof Long ||
                object instanceof Short ||
                object instanceof Byte ||
                object instanceof Float ||
                object instanceof Double ||
                object instanceof Boolean ||
                object instanceof Character ||
                object instanceof String) {
            return String.valueOf(object);
        } else {
            return JSON.toJSONString(object);
        }

好像有点多此一举=-=

4)字符串操作方法

get、set方法:

    /**
     * 数据结构: String
     */

    /**
     * 缓存获取
     *
     * @param key
     * @param clazz
     * @param 
     * @return
     */
    public <T> T get(String key, Class<T> clazz) {
        // 先对传入的key进行判空,以免报错
        if (key == null) {
            return null;
        }
        String val = redisTemplate.opsForValue()
                .get(key);
        return JSONObject.parseObject(val, clazz);
    }

    /**
     * 缓存置入并设置时间
     *
     * @param key
     * @param object
     * @param expire
     * @return
     */
    public boolean set(String key, Object object, long expire) {
        try {
            redisTemplate.opsForValue()
                    .set(key, objectToJson(object), expire, TimeUnit.SECONDS);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 缓存置入
     *
     * @param key
     * @param value
     * @return
     */
    public Boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, objectToJson(value));
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

包装了get、限时key set以及永久key set,存取策略和刚刚说的1是一样的

当key不存在时set方法:setnx

SETNX key value
将 key 的值设为 value ,当且仅当 key 不存在。
若给定的 key 已经存在,则 SETNX 不做任何动作。
SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。

    /**
     * 当key不存在时,缓存置入
     *
     * @param key
     * @param value
     * @param expire
     * @param timeUnit
     * @return key存在时,返回false
     */
    public Boolean setnx(String key, Object value, long expire, TimeUnit timeUnit) {
        return redisTemplate.opsForValue().setIfAbsent(key, objectToJson(value), expire, timeUnit);
    }

    public Boolean setnx(String key, Object value) {
        return redisTemplate.opsForValue().setIfAbsent(key, objectToJson(value));
    }

包装了限时、不限时的2种setnx方法

递增、递减方法:

    /**
     * 递增
     *
     * @param key
     * @param by
     * @return
     */
    public Long increment(String key, long by) {
        if (by < 0) {
            throw new RuntimeException("递增值须大于0");
        }
        return redisTemplate.opsForValue().increment(key, by);
    }

    /**
     * 递减
     *
     * @param key
     * @param by
     * @return
     */
    public Long decrement(String key, long by) {
        if (by < 0) {
            throw new RuntimeException("递减值须大于0");
        }
        return redisTemplate.opsForValue().increment(key, -by);
    }

递增、递减方法能使设置的整数型value递增/递减,可以达到计数的效果,由于redis操作具有原子性,所以能够在利用这个特性并发地对整数进行递增/递减,可以应用在单机部署的情况下的电商秒杀超卖问题

:必须是对整数型的value进行操作,否则会报InvalidDataAccessApiUsageException: ERR value is not an integer or out of range;

5. 单元测试

我们在Test文件夹下创建测试类RedisTester,进行单元测试:

缓存时间测试:

@SpringBootTest
public class RedisTester {

    @Autowired
    private RedisUtil redisUtil;

    @Test
    public void redisSetter() throws InterruptedException {
        UserPO userPO = new UserPO();
        userPO.setUserName("abc");
        userPO.setPassword("123");
        userPO.setProfile("Hello Redis!");
        // 在redis中,以"user"为key缓存该userPO对象2秒
        redisUtil.set("user",userPO,2);
        // 调用redisGetter方法读取缓存
        redisGetter();
    }

    public void redisGetter() throws InterruptedException {
        // 沉睡1秒
        Thread.sleep(1000);
        System.out.println(redisUtil.get("user", UserPO.class));
    }
}

结果:
在这里插入图片描述
在缓存时间为2秒,等待读取时间为1秒的情况下,redisGetter方法顺利地读取到了存入缓存的对象,而在缓存过期的情况下:

    public void redisGetter() throws InterruptedException {
        // 沉睡3秒
        Thread.sleep(3000);
        System.out.println(redisUtil.get("user", UserPO.class));
    }

Java后端学习日记(四):Springboot 2.X 整合Redis作为数据缓存_第1张图片
redisGetter就读取不到缓存中的结果了

incr递减测试:

    @Test
    public void decrTest(){
        int i=3;
        redisUtil.set("num",i);
        while(i-->0){
            System.out.println(redisUtil.decrement("num", 1));
        }
    }

Java后端学习日记(四):Springboot 2.X 整合Redis作为数据缓存_第2张图片
调用redisTemplate.opsForValue().increment(key, by);会返回被递增/递减后的值,利用此特性,在电商场景中,可以将商品库存数据先set到redis中,当有人下单时,便将redis中的商品库存递减1,再异步操作数据库,这样就可以减轻数据库的压力,使用高效的redis判断操作是否成功,来对用户进行下单成功与否的反馈,提升用户体验。


Redis其它知识补充

参考自敖丙的Redis 缓存雪崩、击穿、穿透

缓存雪崩

倘若在同一时间内有大量的缓存过期/失效,大量请求一下子打入数据库,数据库一下就被打死了,这就是redis缓存雪崩:
Java后端学习日记(四):Springboot 2.X 整合Redis作为数据缓存_第3张图片

缓存穿透

缓存穿透是指,当用户请求一个缓存和数据库都没有的值(如在一个主键自增数据表中查询一个主键为-1的值),服务端在缓存查询无果后会进入数据库查询,当然是查不到结果的

当这个请求被攻击者恶意利用,发送很多个这样的请求,就会导致我们的数据库压力过大,甚至将我们的数据库搞垮

解决方法

  1. 对请求进行拦截,拒绝离谱的请求
  2. 使用布隆过滤器,在查找前先检测请求的key是否在数据库中存在

缓存击穿

缓存击穿指的是,在缓存中有一个key是热点,热点的意思即是有很多用户都在请求着这个key,比如说上面说的商品热卖,很多用户都想点开这个id为3的商品了解详情,这时候我们用redis缓存了id为3的商品信息,本来是没问题的,这个key本身在运行中也扛着大量的并发

倘若这个key现在过期了,而用户请求的并发数量还是没有减少,就会有大量的请求打入数据库,数据库就难顶了

解决方法:设置热点数据永不过期,或在缓存失效入库查询时使用互斥锁

缓存击穿跟雪崩有点相似,不同的地方是:

  • 缓存雪崩是大面积的缓存失效而造成大量请求打入数据库,是一堆KV失效造成的
  • 缓存击穿是缓存热点失效而造成大量请求打入数据库,是单个热点KV失效造成的

  1. 由于在本专栏系列第二篇中已经引入过fastjson,这里的序列化/反序列化就使用fastjson进行操作 ↩︎

你可能感兴趣的:(Java后端学习日记,redis,缓存,java,数据库,电子商务)