Kaze
Kaze
Published on 2022-04-24 / 99 Visits
0
0

Redis

什么是Redis

简介

  • Redis 是一个包含多种数据结构、支持网络、基于内存、可选持久性的 键值对 存储数据库

使用场景

  • Redis 的 常见 的、 核心 的使用场景是:作为数据缓存(cache)。

优点

  • 性能很好

    由于是全内存操作,所以读写性能很好,可以达到 10w/s 的频率。

  • 支持多种数据类型

    Redis 支持 Set、ZSet、List、Hash、String 这五种数据类型

  • 持久化存储

    Redis 使用 RDB 和 AOF 做数据的持久化存储。主从数据同时生成rdb文件,并利用缓冲区添加新的数据更新操作做对应的同步。

    RDB:RDB在指定的时间间隔内将内存中的数据集快照写入磁盘

    AOF:AOF以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。

缺点

  • 内存容量问题
  • 持久化存储的性能损耗问题
  • 重启速度可能慢

安装并配置Redis

  • 安装 Redis 并启动

    sudo docker pull redis:latest
    sudo docker run  --name redis -p 6379:6379 -d --restart=always redis:latest redis-server --appendonly yes --requirepass "423414a."
    

高并发用户注册

  • Cookie 可以在浏览器(或者说客户端)中保存数据,重要的使用场景就是判断用户是否登录,以辨别用户身份。

  • Cookie 有大小限制,Session无限制(取决于服务端的硬件)。

  • Session 存储于服务端,Session 是共享的,可以让两个页面都获取到。

  • Session 放在应用服务器的内存中,服务器一旦出问题比如重启,所有的 Session 都会丢失,并且一些网站往往是分布式架构,有很多服务器,Session 放在一台服务器中,那么请求分发到其它服务器的时候,就读不到Session 了

    解决方案:将 Session 缓存到 Redis 里,多台服务器可以共享 Session ,并且 Redis 相对稳定,缓存不易丢失

SpringBoot 集成 Redis

  • 引入依赖:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
  • 配置:

    application.properties 中增加 Redis 相关的配置:

    # Redis服务器地址
    spring.redis.host=
    # Redis 服务器端口号
    spring.redis.port=6379
    # Redis 服务器密码
    spring.redis.password=
    

注册性能优化

  • 注入 RedisTemplate 实例:

    RedisTemplateSpring Data Redis 提供给用户的最高级的抽象客户端,用户可直接通过 RedisTemplate 进行多种操作。

     @Autowired
        private RedisTemplate redisTemplate;
    
  • 用户模型实现序列化接口:

    要实现把用户数据缓存在 Redis 里的功能,用户模型必须实现序列化接口。用户模型实例在网络上传输,必须能够序列化。

    public class UserDO implements Serializable {
    
    }
    

    Serializable 接口中没有定义任何方法,相当于一个空接口,意义主要是 标识 此对象能够被序列化。

  • 从缓存中获取实例:

    要读写缓存 Value ,就要调用 redisTemplate.opsForValue() ,然后再调用 get() 方法根据参数 Key 取得缓存值。

  • 将实例放入缓存:

    向 Redis 存数据,同样需要调用 redisTemplate.opsForValue(),再调用 set() 方法,set() 方法第一个参数是 Key,第二个参数是 Value。

    redisTemplate.opsForValue().set(userName, userDO1);
    
  • 修改缓存:

    基于 Key - Value 的 Redis 无法修改 Value 对象的某一个属性,所谓修改就是重新存入值,新的值覆盖旧值

  • 删除缓存:

    删除缓存其实是按照 Key 删除数据

    redisTemplate.delete(userName);
    

用户 Session

  • 引入依赖:

    删除 旧的依赖项

    <dependency>
      <groupId>org.springframework.session</groupId>
      <artifactId>spring-session-core</artifactId>
    </dependency>
    

    再引入新的依赖项

    <!-- spring session 支持 -->
    <dependency>
      <groupId>org.springframework.session</groupId>
      <artifactId>spring-session-data-redis</artifactId>
    </dependency>
    <dependency>
      <groupId>org.redisson</groupId>
      <artifactId>redisson-spring-boot-starter</artifactId>
      <version>3.13.0</version>
    </dependency>
    
  • 修改 Session 配置类:

    @Configuration
    @EnableRedisHttpSession(maxInactiveIntervalInSeconds = 300)//注解参数设置 Session 数据的过期失效时间
    public class SpringHttpSessionConfig {
        @Bean
        public CookieSerializer cookieSerializer() {
            //该方法定制 Cookie 中的 Session 信息内容如何写
        }
    }
    

缓存穿透

  • 在某种攻击下,Redis 失去了缓存的意义,称之为 缓存穿透

  • 第一次从数据库查询不到数据时,仍然把这个空结果进行缓存。不过,要设置过期时间,注意过期时间不要太长,推荐不超过五分钟。

    // 只 new 实例但不设置任何属性,相当于一个空对象
    userDO = new UserDO();
    redisTemplate.opsForValue().set(userName, userDO, 5, TimeUnit.MINUTES);
    

    当用户第二次访问的时候,无论账户是否正确,Redis 中都缓存了数据,避免再次查询数据库

乱码问题

  • Java 程序存入 Redis 数据时,会把数据序列化,而 Java 默认的序列化方式,是把内容变成字节码,计算机能识别,人就识别不了,看起来像乱码。

  • 解决办法:重置序列化方式

    加一个 config 类,类中注入 RedisTemplate 实例,然后通过方法重置序列化方式:

    @Bean
    public RedisTemplate redisTemplateInit() {
      		//设置序列化Key的实例化对象
            redisTemplate.setKeySerializer(new StringRedisSerializer());
    
            RedisSerializer<Object> ser = new GenericJackson2JsonRedisSerializer();
    
            //设置序列化Value的实例化对象
            redisTemplate.setValueSerializer(ser);
            redisTemplate.setHashKeySerializer(ser);
            redisTemplate.setHashValueSerializer(ser);
            redisTemplate.afterPropertiesSet();
            return redisTemplate;
    }
    

    Redis 数据的 Key 用字符串(StringRedisSerializer)的序列化/反序列化方式,Value 用 JSON(GenericJackson2JsonRedisSerializer)的序列化/反序列化方式。

商品类目系统

插入类目数据

  • 字符串是 Redis 的最基础的数据类型,Redis 的数据 Key 就是字符串。

  • Redis List(列表)是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。

  • 添加 List 数据

    从头部添加元素,使用 leftPush() 方法:

    // 第一个对象,演示代码省略 set 各属性值
    Category category = new Category();
    redisTemplate.opsForList().leftPush("categoryList", category);
    
    // 第二个对象,演示代码省略 set 各属性值
    category = new Category();
    redisTemplate.opsForList().leftPush("categoryList", category);
    

    从尾部添加元素,使用 rightPush() 方法:

    Category category = new Category();
    redisTemplate.opsForList().rightPush("categoryList", category);
    

    整个列表是 Value,列表中的数据元素不能称为 Value

    Java 系统会先把对象进行序列化,然后存入 Redis 。Redis 会把相同 Key 的数据组织到一个数据结构 List 中

    无论 leftPush() 还是 rightPush() ,其实都有返回值,返回值类型是 Long

    返回的含义是,执行完数据添加操作以后,列表的长度。

查询类目数据

  • 查询列表长度

    Long size = redisTemplate.opsForList().size("categoryList");
    
  • 根据索引查询

    Category category = (Category)redisTemplate.opsForList().index("categoryList", index);
    

    index() 方法用于根据索引查询数据,第一个参数是数据 Key,第二个参数是索引。

  • 范围查询

    List<Category> categoryDatas = redisTemplate.opsForList().range("categoryList", 0, 1);
    

    range() 方法用于根据索引查询一批数据:

    • 第一个参数是数据 Key
    • 第二个参数是起始索引(包含)
    • 第三个参数是结束索引(包含)

    索引用负数也是成立的: -1 表示倒数第一个元素,-2 表示倒数第二个元素。如果想查询整个列表的所有数据,第三个参数填 -1range("categoryList", 0, -1)

修改类目数据

  • 修改 Redis 数据,就不像关系型数据库那样,可以只修改某几个字段。.修改 Redis 列表数据,就是把数据对象重新放入列表中。

  • 语法:

    // 演示代码省略 set 各属性值
    Category category = new Category();
    redisTemplate.opsForList().set("categoryList", 0, category);
    

    调用 set() 方法即可完成修改,必须根据索引重新放入数据,所以第二个参数是索引,第三个参数是新的对象。

  • 如何知道索引

    // 查询所有的数据
    List<Category> categoryDatas = redisTemplate.opsForList().range("categoryList", 0, -1);
    long index = 0;
    for (Category cat : categoryDatas) {
        if (StringUtils.equals(cat.getId(), "gcl_001")) {
            break;
        }
        index++;
    }
    
  • 对象存入 Redis 的时候会被序列化,但是时间类型的序列化,要额外处理。主要是由于在 config 类中配置的序列化工具类 GenericJackson2JsonRedisSerializer 不支持 LocalDateTime ,所以,在模型的时间属性上,需要加注解:

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
        @JsonDeserialize(using = LocalDateTimeDeserializer.class)
        @JsonSerialize(using = LocalDateTimeSerializer.class)
    
    • @JsonSerialize 注解用于指定 序列化 的工具类,推荐 LocalDateTimeSerializer
    • @JsonDeserialize 注解用于指定 反序列化 的工具类,推荐 LocalDateTimeDeserializer
    • @JsonFormat用于指定输出时间格式。

删除类目数据

  • Redis 可以从头部和尾部插入数据,也可以从头部和尾部开始删除数据。但是不能按照索引位置删除数据。

  • 语法:

    从头部删除数据:

    Category cat = (Category)redisTemplate.opsForList().leftPop("categoryList");
    

    从尾部删除数据:

    Category cat = (Category)redisTemplate.opsForList().rightPop("categoryList");
    

    两个方法的返回值,都是当前从列表被删除的数据

高并发商品抢购

Redis事务

  • 事务(Transaction),是指将一个业务逻辑作为一个整体一起执行。事务其实就是打包一组操作(或者命令)作为一个整体,在事务处理时将顺序执行这些操作,并返回结果,如果其中任何一个环节出错,所有的操作将被取消。Redis 的事务可以保证只有在执行完事务中的所有命令后,才会继续处理此客户端的其他命令。

  • redis 事务四大指令: MULTI、EXEC、DISCARD、WATCH

    • WATCH 用于客户端并发情况下,为事务提供一个锁(CAS,Check And Set) 可以用 watch 命令来监控一个或多个变量,如果在执行事务之前,某个监控项被修改了,那么整个事务就会终止执行
    • MULTI 开启一个事务;
    • EXEC 执行一个事务;
    • DISCARD 取消一个事务;
  • 事务基础框架代码

    try {
        redisTemplate.execute(new SessionCallback<List<Object>>() {
            @Override
            public List<Object> execute(RedisOperations operations) throws DataAccessException {
    
            }
        });
    } catch (Exception e) {
        LOG.error("redisTemplate.execute() error. ", e);
    }
    

    这个写法基本上就是固定了的。具体的事务就写在方法参数 SessionCallback 接口的 execute() 方法体内部。

    方法参数 RedisOperations 提供了事务四大指令操作。

  • 监听对象

    在开启事务前,先选择一个要监听的对象,即 Redis 中的某个 Key。

    operations.watch() 方法传入待监视的 Redis 数据 Key。

    这个 Key 必须在 Redis 中存在,否则会抛异常。

  • Redis 事务三阶段

    Redis 中的事务从开始到结束要经历 3 个阶段:

    image-20220422233254892
    1. 开启事务:operations.multi() 开启事务时不需要参数。

    2. 命令入列:所谓命令入列,就是指读写 Redis 数据的业务逻辑。在开启事务后,就可以写具体业务逻辑的代码了。

    execute() 方法中,Redis 的操作不再使用 redisTemplate.opsForValue(),而是使用 operations.opsForValue() ,这样系统才知道是同一个事务中的操作。

    1. 执行事务:operations.exec() 用于执行事务,返回值是 List 列表,存放了每个事务执行结果的标记。事务开启后执行的每个操作,如果成功则放入 true 值作为标记,操作失败则不放入结果标记。因为事务是要么每个操作都成功,要么都失败,所以一般来说可以简单处理,不用判断 operations.exec() 方法返回值列表中的每个元素是否都为 true,只要判断返回值列表长度大于 0 则表示执行成功。

    2. 取消事务:在 execute(RedisOperations operations) 方法抛异常时,会自动取消事务。如果需要手动取消事务,只需要用:operations.discard();

分布式锁

  • synchronized 属于 本地锁,只能解决一台服务器并发问题。如果我们需要一个能锁所有服务器的锁,这就是分布式锁

  • Reddissin 客户端:

    Reddissin 客户端不仅可以提供基本的Redis功能,还可以提供一些高级服务:远程调用 分布式锁 分布式对象、容器

    在 SpringBoot 中集成 Redission:

    依赖

       	<dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.13.0</version>
        </dependency>
    
  • Redis 分布式锁实现:

    1. 先注入 redissonClient 实例
    @Autowired
    private RedissonClient redissonClient;
    
    1. 取得锁
    RLock rLock = redissonClient.getLock("productId-1-lock");
    //方法参数是 字符串 类型的自定义锁名称
    
    1. 上锁
    rLock.lock();
    
    1. 解锁
    rLock.unlock();
    

    完整实现

    try {
      rLock.lock();
      // 抢购业务逻辑
    } catch (Exception e) {
        LOG.error("some error. ", e);
    } finally {
        rLock.unlock();
    }
    

过期处理

  • 设置 Redis List 的过期时间:

    redisTemplate.expire("category", 60, TimeUnit.MINUTES);
    

    redisTemplate.expire() 是一个通用方法,可以为任何数据类型设置过期时间

  • 删除策略

    惰性删除:每次查询或写键时,都会检查取得的键是否过期。如果过期就删除该删,否则就返回该键。

    定期删除:每隔一段时间,程序就对数据库进行检查,删除里面的过期键。至于要删除多少过期键,以及检查多少数据库,则由算法决定

    定时删除:在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除操作。

分布式ID

  • 数据库分表后每个表的数据id会按自己的节奏来自增,这样会造成 ID 冲突,这时就需要用分布式ID来负责生成唯一 ID。

  • 与日期相关的订单号

    根据当天时间及订单生成的序号来生成一个唯一的 ID

    //格式化格式为年月日
    DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMdd");
    //获取当前时间
    String now = LocalDate.now().format(dateTimeFormatter);
    //通过redis的自增获取序号
    RAtomicLong atomicLong = redissonClient.getAtomicLong(now);
    atomicLong.expire(1, TimeUnit.DAYS);
    long number = atomicLong.incrementAndGet();
    //拼装订单号
    String orderId = now + "" + number;
    

玩家排行榜

Redis的ZSet数据结构:

  • ZSet插入:

    redisTemplate.opsForZSet().add(key, value, score);
    

    向指定key中添加元素,按照score值由小到大进行排列

    若集合中对应元素已存在,则会被覆盖,包括score

  • ZSet查询:

    //0和-1代表查询该键的所有值默认是按升序排序
    Set tv = redisTemplate.opsForZSet().rangeWithScores("TV", 0, -1)
    
    //reverseRangeWithScores是根据score降序排序
    Set tv = redisTemplate.opsForZSet().reverseRangeWithScores("TV", 0, -1);
    
  • 遍历返回值 Set 集合:

    Set<TypedTuple<PersonalRecord>> datas = redisTemplate.opsForZSet().rangeWithScores("integralRank", 0, -1);
    
    // 遍历
    datas.forEach(data -> {
        // 存入的对象
        PersonalRecord pr = data.getValue();
        // 对应的分数
        Double score = data.getScore();
    });
    

    查询返回 Set 集合中的元素类型是 TypedTuple ,用于整合查询结果对象(通过 add() 方法放入 ZSet 中的对象)及其对应的score,封装在一起,便于读取。

    通过 getValue() 可以取得放入 ZSet 的元素对象,通过 getScore() 可以取得元素的分数。

Redis 的 Hash 数据结构:

  • Redis Hash 是一个字符串类型的 field(字段) 和 value(值) 的映射表。整个 Value 就是键值对映射结构,通过 key 和 field 取得所需的值。
img
  • 增加:

    redisTemplate.opsForHash().put("integralRankUser", userDO.getUserName(), userDO);
    

    put() 方法第一个参数是 Key;第二个参数是 field ;第三个参数就是具体的值。

  • 修改:

    当 field 相同时,再次 put() 会覆盖原来 field 的值。

  • 读取:

    根据 Key 和 field 精确查询:

    UserDO userDO = (UserDO)redisTemplate.opsForHash().get("integralRankUser", userName);
    

    根据 Key 和 一批 field 批量查询:

    List<String> userNames = new ArrayList<>();
    userNames.add(userName);
    List<UserDO> users = redisTemplate.opsForHash().multiGet("integralRankUser", userNames);
    
  • 删除:

    redisTemplate.opsForHash().delete("integralRankUser", userName);
    

    delete() 方法支持变长参数,所以想删除多个 field 时,只需要传入多个 field 即可

Redis的Set数据结构:

  • Set 与 ZSet 相比,由于缺少了 Score ,所以无法排序。

  • 新增数据操作:

    可以调用 add() 方法批量增加数据

    redisTemplate.opsForSet().add("ranks", personalRecord1, personalRecord2);
    

    add() 方法第一个参数是 Key;后面就是 Java 实例对象。

  • 删除数据操作:

    redisTemplate.opsForSet().remove("ranks", personalRecord);
    

    删除与添加是对应的,添加的是自定义对象,删除的时候也要传入相同的自定义对象。

  • 修改数据操作:

    不存在修改这个说法。或者说,修改等同于:先删除旧数据,再加入新数据

  • 基本查询:

    Set<PersonalRecord> datas = redisTemplate.opsForSet().members("ranks");
    
  • Set 多用于集合间的操作,推荐 Set 存储简单的数据,比如 Java 的字符串或数字,而不要在 Set 中存入复杂的 Java 自定义对象。

  • 多集合操作:

    求并集:

    List<String> keys = new ArrayList<>();
    keys.add("ranks1");
    keys.add("ranks2");
    keys.add("ranks3");
    Set<Long> unionDatas = redisTemplate.opsForSet().union(keys);
    

    求交集:

    Set<Long> interDatas = redisTemplate.opsForSet().intersect(keys);
    

    求差集:

    Set<Long> diffDatas = redisTemplate.opsForSet().difference("ranks1", otherkeys);
    

    difference() 方法返回第一个集合与其他集合之间的差异,也可以认为说第一个集合中独有的元素。

常见面试题

Redis 为什么这么快

  1. 完全基于内存,绝大部分请求是纯粹的内存操作。
  2. 数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的
  3. 采用单线程。避免了不必要的上下文切换和竞争条件,也不存在 多线程切换 消耗 CPU 资源;不用去考虑各种 的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
  4. 使用多路I/O复用模型,非阻塞IO
  5. 使用底层模型不同

常见数据结构

  • List:适用于聚合同一类数据
  • ZSet:适用于排行榜之类的根据指定数值排序的数据
  • Hash:适用于同一类数据中,需要根据一个 关键字 查询的场景。
  • Set:适合多集合运算。

缓存穿透、雪崩与击穿

缓存穿透

Key 对应的数据在数据源并不存在,每次针对此 Key 的请求从缓存获取不到,请求都会到数据源,从而可能压垮数据源。

解决方案:

  1. 简单解决法:缓存空对象结果,但注意过期时间不能过长。
  2. 系统解决法:采用布隆过滤器,将所有可能存在的数据都缓存,那么一个一定不存在的数据会被拦截。
缓存雪崩

当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力。

解决方案:

  1. 简单解决法:缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
  2. 严谨解决法:用加锁或者队列的方式来保证不会有大量的线程同时对数据库进行读写,从而避免缓存失效时大量的并发请求落到底层存储系统上。
缓存击穿

Key 对应的数据存在,但在 Redis 中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

雪崩强调的是大量的数据缓存过期出现的问题,而击穿侧重于描述少量 Key (热点数据)过期时遇到大量并发请求是出现的问题。

解决方案:

使用锁机制,在缓存失效的时候(判断拿出来的值为空),不是立即去数据库中查询,而是先加锁,在锁中查询数据库并回设缓存。目的就是防止大并发请求在短时间内请求数据库。

Key过大要如何处理

单个key存储的value很大

如果value是java对象,可以把每个属性作为一个 file 存入 hash 结构的缓存中,而不是整个对象序列化成字符串存入 Redis,这样读写对象,相当于操作 Redis 的 field ,可以有效缓解操作大 Value 带来的性能损耗。

如果value是Java 的大容器对象(如List、Map),建议不要直接缓存大容器对象,而是使用对应的 Redis 结构。避免每次查询完整的数据。

hash,set,zset,list 中存储过多的元素

把大容器拆分多个小容器。以 Hash 为例,大 Hash 拆分成多个小 Hash,每个小 Hash 称为桶。

固定一个桶的数量,比如 10000。每个小桶的 Key 带一个数字后缀。

每次存取的时候,先在 Java 程序中计算field的hash值,模除 10000, 确定了该field落在哪个key上。


Comment