6.Redis

redis介绍

属于非关系型数据库NoSQL (Not noly SQL)不仅仅是SQL,为什么需要NoSQL,主要应对以下问题,传统关系型数据库力不从心

  • High performance -高并发读写
  • Huge Storage-海量数据的高效率存储和访问
  • High Scalablility && High Availability 高可扩展性和高可用性

NoSQL 产品 MongoDB Redis等等 Redis目前主流,NoSQL的特点

  • 易扩展
  • 灵活的数据模型
  • 大数据量,高性能
  • 高可用
  • 高性能键值对内存数据库,支持的键值数据类型

应用场景

  • 缓存
  • 任务队列
  • 网站访问统计
  • 数据过期处理
  • 应用排行榜
  • 分布式集群架构中的session分离
  • 分布式锁

redis为什么快

  • 运行在内存中 redis是C语言 cpu 内存 磁盘
  • 采用多路复用I/O模型
  • 内置很多高效的数据类型(算法)
  • redis的主线程是单线程,没有多线程上下文切换的性能消耗

通用操作

keys *、keys ?、exists 、rename 、expire 、ttl 、type 、select 1

基本数据类型

image

String

  • 字符串类型
  • 常用指令:set、get、setget、del、incr、incrby、decr、decrby、append

List

  • 列表类型可以两头插入,索引始终从左向右计算,从0开始,有序可重复,可做消息队列
  • 常用指令:lpush、lpushx、rpush、rphshx、lange、lpop、rpop、llen、lrem、lset、linsert..before、linsert..after、rpoplpush

Hash

  • 类似于java中的 Map<String,Map<String,String>>
  • 常用指令:hset、hget、hmset、hmget、hgetall、hdel、hincrby、hexists、hlen、hkeys、hvals

Set

  • 同java中的set,无序不重复、可以统计差集、交集、并集,可以跟踪一些唯一性数据
  • 常用指令:sadd、smembers、srem、sismember、sdiff、sinter、sunion、scard、srandmember、sdiffstore 、sinterstore、sunionstore

Zset

  • sorted-set,是有序的set集合,每一个元素包含了评分和值,插入元素的时候需要带评分,默认评分从小到大排列,可用于实时排行
  • 常用指令:zadd、zrange、zrange...withscores、zscore、zrem 、zrevrange、zrangebyscore...limit、zremrangebyrank、zincrby、zcount

高级数据类型

Bitmaps

image

布隆过滤器

image

image

image

image

实现

Guava实现

dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>28.1-jre</version>
</dependency>
/*单机下无Redis的布隆过滤器:使用Google的Guava的BloomFilter*/
public class GuavaBF {
    public static void main(String[] args) {
        long expectedInsertions = 100000;
        double fpp = 0.00005;

        BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), expectedInsertions, fpp);

        bloomFilter.put("10081");
        bloomFilter.put("10082");
        bloomFilter.put("10083");
        bloomFilter.put("10084");
        bloomFilter.put("10085");
        bloomFilter.put("10086");

        System.out.println("123456:BF--"+bloomFilter.mightContain("123456"));//false
        System.out.println("10086:BF--"+bloomFilter.mightContain("10086"));//true
        System.out.println("10084:BF--"+bloomFilter.mightContain("10084"));//true

    }
}

Google的实现

/*仿Google的布隆过滤器实现,基于redis支持分布式*/
public class RedisBloomFilter {

    public final static String RS_BF_NS = "rbf:";
    private int numApproxElements; /*预估元素数量*/
    private double fpp; /*可接受的最大误差*/
    private int numHashFunctions; /*自动计算的hash函数个数*/
    private int bitmapLength; /*自动计算的最优Bitmap长度*/

    @Autowired
    private JedisPool jedisPool;

    /**
     * 构造布隆过滤器
     * @param numApproxElements 预估元素数量
     * @param fpp 可接受的最大误差
     * @return
     */
    public RedisBloomFilter init(int numApproxElements,double fpp){
        this.numApproxElements = numApproxElements;
        this.fpp = fpp;
        /*位数组的长度*/
        //this.bitmapLength = (int) (-numApproxElements*Math.log(fpp)/(Math.log(2)*Math.log(2)));
        this.bitmapLength=128;
        /*算hash函数个数*/
        //this.numHashFunctions = Math.max(1, (int) Math.round((double) bitmapLength / numApproxElements * Math.log(2)));
        this.numHashFunctions=2;
        return  this;
    }

    /**
     * 计算一个元素值哈希后映射到Bitmap的哪些bit上
     * 用两个hash函数来模拟多个hash函数的情况
     *     * @param element 元素值
     * @return bit下标的数组
     */
    private long[] getBitIndices(String element){
        long[] indices = new long[numHashFunctions];
        /*会把传入的字符串转为一个128位的hash值,并且转化为一个byte数组*/
        byte[] bytes = Hashing.murmur3_128().
                hashObject(element, Funnels.stringFunnel(Charset.forName("UTF-8"))).
                asBytes();

        long hash1 = Longs.fromBytes(bytes[7],bytes[6],bytes[5],bytes[4],bytes[3],bytes[2],bytes[1],bytes[0]);
        long hash2 = Longs.fromBytes(bytes[15],bytes[14],bytes[13],bytes[12],bytes[11],bytes[10],bytes[9],bytes[8]);

        /*用这两个hash值来模拟多个函数产生的值*/
        long combinedHash = hash1;
        for(int i=0;i<numHashFunctions;i++){
            //数组下标
            indices[i]=(combinedHash&Long.MAX_VALUE) % bitmapLength;
            combinedHash = combinedHash + hash2;
        }

        System.out.print(element+"数组下标");
        for(long index:indices){
            System.out.print(index+",");
        }
        System.out.println(" ");
        return indices;
    }

    /**
     * 插入元素
     *
     * @param key       原始Redis键,会自动加上前缀
     * @param element   元素值,字符串类型
     * @param expireSec 过期时间(秒)
     */
    public void insert(String key, String element, int expireSec) {
        if (key == null || element == null) {
            throw new RuntimeException("键值均不能为空");
        }
        String actualKey = RS_BF_NS.concat(key);

        try (Jedis jedis = jedisPool.getResource()) {
            try (Pipeline pipeline = jedis.pipelined()) {
                for (long index : getBitIndices(element)) {
                    pipeline.setbit(actualKey, index, true);
                }
                pipeline.syncAndReturnAll();
            } catch (Exception ex) {
                ex.printStackTrace();
            }
            jedis.expire(actualKey, expireSec);
        }
    }

    /**
     * 检查元素在集合中是否(可能)存在
     *
     * @param key     原始Redis键,会自动加上前缀
     * @param element 元素值,字符串类型
     */
    public boolean mayExist(String key, String element) {
        if (key == null || element == null) {
            throw new RuntimeException("键值均不能为空");
        }
        String actualKey = RS_BF_NS.concat(key);
        boolean result = false;

        try (Jedis jedis = jedisPool.getResource()) {
            try (Pipeline pipeline = jedis.pipelined()) {
                for (long index : getBitIndices(element)) {
                    pipeline.getbit(actualKey, index);
                }
                result = !pipeline.syncAndReturnAll().contains(false);
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
        return result;
    }

    @Override
    public String toString() {
        return "RedisBloomFilter{" +
                "numApproxElements=" + numApproxElements +
                ", fpp=" + fpp +
                ", numHashFunctions=" + numHashFunctions +
                ", bitmapLength=" + bitmapLength +
                '}';
    }
}

Redisson实现

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.12.5</version>
        </dependency>
 /*Redisson底层基于位图实现了一个布隆过滤器,使用非常方便*/
    @Test
    public void testBloomFilter(){
        Config config = new Config();
        config.useSingleServer().setAddress("redis://localhost:6379");

        //构造Redisson
        RedissonClient redisson = Redisson.create(config);
        RBloomFilter<Object> bloomFilter = redisson.getBloomFilter("userList");
        // 初始化布隆过滤器:预计元素为100000000L,误差率为3%
        bloomFilter.tryInit(100000000L,0.03);
        // 将号码10081~10086插入到布隆过滤器中
        bloomFilter.add("10081");
        bloomFilter.add("10082");
        bloomFilter.add("10083");
        bloomFilter.add("10084");
        bloomFilter.add("10085");
        bloomFilter.add("10086");
        // 判断下面号码是否存在布隆过滤器中
        System.out.println("123123:BF--"+bloomFilter.contains("123123"));//false
        System.out.println("10086:BF--"+bloomFilter.contains("10086"));//true
        System.out.println("10084:BF--"+bloomFilter.contains("10084"));//true
    }

缓存问题

缓存原则

1、热点数据必须放入缓存中;
2、对于缓存场景,推荐使用本地缓存与分布式缓存相结合的方案;
3、优先读取本地缓存,以本地缓存为主,分布式缓存为辅;
4、为所有缓存设置过期时间,本地缓存的过期时间应控制在秒级;
5、本地缓存应同时设置容量驱逐和时间驱逐两种策略;
6、缓存键值对应业务逻辑,避免不同场景使用相同的键;
7、缓存列表数据时,仅缓存第一页,且缓存数量不超过20;
8、避免并发更新缓存,以防缓存击穿;
9、空数据也应放入缓存中,防止缓存穿透;
10、读取数据时,先从缓存中读取,若无则从数据库中读取;
11、写入数据时,先写入数据库,再更新缓存。

缓存穿透

客户端请求的数据缓存和数据库中都不存在,永远不会有缓存生效,请求直接到达数据库

解决方案

  • 缓存空对象,实现简单,会有多余的空对象
  • 布隆过滤器,存在误判的情况
/*
* 缓存数据详情
* 1.根据id去redis中查询
* 2.如果缓存中没有,查询数据库,放缓存
*
* 出现的问题:如果用户查询一个数据库和缓存都没有的数据,缓存无效,这些数据都会直达数据库,缓存穿透
* 1.缓存一个空对象:可以解决缓存穿透问题,但是缓存中会出现很多垃圾数据
* 2.布隆过滤器:可以把所有的书籍id放在bitmap数据中,用户查询可以立刻筛选出这个id是否在数据库的表中,如果不存在可以
*/
@Override
    public ResponseUtil queryById(Long id) {
        TBook cacheBook = (TBook) redisTemplateInit.opsForHash().get(ConstantUtil.REDIS_BOOK_Detail, id + "");
        if (cacheBook != null) {
            //缓存中存在,直接返回缓存的结果
            return ResponseUtil.get(ResponseEnum.OK, cacheBook);
        }
        //如果缓存中没有,去数据库查询
        TBook book = tBookMapper.selectByPrimaryKey(id);
        //缓存到数据库
        redisTemplateInit.opsForHash().put(ConstantUtil.REDIS_BOOK_Detail, id + "", book == null ? new TBook() : book);
        //设置不同的过期时间,解决缓存雪崩问题
        redisTemplateInit.expire(ConstantUtil.REDIS_BOOK_Detail, 30 + RandomUtil.randomInt(10), TimeUnit.DAYS);
        return ResponseUtil.get(ResponseEnum.OK, book);
    }

缓存雪崩

同一时间大量的缓存key失效,或者redis宕机,导致大量的请求达到数据库

解决方案

  • 给不同的key添加不同的过期时间
  • redis集群提高服务可用性
  • 给业务添加多级缓存,redis缓存、本地缓存等

缓存击穿

当前key是一个热点key(例如一个秒杀活动),并发量非常大,在缓存失效的瞬间,有大量线程来重建缓存,造成后端负载加大

解决方案

查询缓存未命中时,去查询数据库的代码前加互斥锁(可以用redis分布式锁),从数据库查询数据并且写入缓存完毕后释放锁

数据一致性

对缓存对应的数据库数据,发生了增删改操作,会导致数据库数据和redis缓存数据不一致的问题

解决方案

  • 先删除缓存,再操作数据库
    • 先删除缓存再修改数据库数据,可能出现修改之前,其他线程查询了该数据并存入了旧数据在缓存中
  • 先操作数据库,再删除缓存
    • 如果缓存中没有数据,修改了数据库数据,没有缓存可以删除,此时另一条线程查询该条数据,在放入缓存之前查询到的是修改之前的数据,然后将老数据放入了缓存,后续查到的缓存都是脏数据
  • 延迟双删
    • 删除缓存
    • 修改数据库
    • 延迟一段时间 (目的是等待其他操作存入缓存)
    • 再删除缓存
    public void updateBook(Book book){
        // 1.先删除缓存
        redisTemplateInit.delete("bookDetail:"+book.getId());
        // 2.修改数据库数据
        // bookMapper.updateBook(book);
        // 3.延迟 100-200ms 时间过长会脏数据的可能性更大
        // Thread.sleep(200);
        // 4.再次删除缓存
        redisTemplateInit.delete("bookDetail:"+book.getId());
    }
  • 基于 Binlog的异步缓存更新(Canal)

image

此方案通过监听 MySQL 的 Binlog(操作日志),实现数据库变更时自动推送消息到缓存层更新数据,解耦业务逻辑和缓存维护。

1.MySQL 触发数据变更写入 Binlog
2.Canal 订阅 Binlog 并解析变更事件
3.Canal 推送到 MQ(如 Kafka)
4.消费者订阅 MQ,更新缓存

  • 通过 MQ 串行化写请求实现强一致

    适用于订单、库存等强一致业务场景。所有写操作不再直接写库,而是通过 MQ 队列串行消费,消费者统一处理更新数据库 + 清理缓存。

    1.应用将写操作封装为消息发送到 MQ
    2.消费者串行消费消息
    3.消费者更新数据库后删除缓存

  • 分布式锁辅助方案(Redisson)

    在高并发修改同一资源(如用户余额)时,通过 Redisson 提供的可重入锁限制并发更新,确保操作串行化

    1. 加锁(分布式锁)
    2. 执行更新数据库与缓存删除操作
    3. 解锁
RLock lock = redissonClient.getLock("user:lock:1");
lock.lock();
try {
    updateDB();
    redis.del();
} finally {
    lock.unlock();
}1

image

image

  • 改库删缓存是基础,延迟双删是加强版
  • Canal 和 MQ 提供企业级异步一致保障
  • Redisson 锁则是并发控制的利器

redis删除策略

数据删除策略

定时删除

创建一个定时器,当key设置有过期时间,且过期时间到达时间,由定时器任务立即执行对键的删除操作

  • 优点:节约内存,到时就删除,快速释放掉不必要的内存占用
  • 缺点:CPU压力很大,无论CPU此时负载量多高,均占用CPU,会影响rdis服务器响应时间和指令吞吐量
  • 总结:用处理器性能换取存储空间(拿时间换空间)

惰性删除

不主动删除过期键,每次从数据库访问key 时,都检测 key 是否过期,如果过期则删除该 key。

  • 优点:对CPU时间友好
  • 缺点:过期键占用的内存不会及时释放,造成了一定的内存浪费

定期删除

每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key。

  • 优点:通过限制删除的时长和频率,减少因为删除对cpu的占用,同时也能删除一部分过期键释放内存
  • 缺点:
    • 内存清理没有定时删除好,同时没有惰性删除使用的资源少
    • 难以确定删除操作执行的时长和频率,如果执行的太频繁,和定时删除一样;如果执行的太少,又和惰性删除一样,占用的key不能及时释放

淘汰策略

当 Redis 内存超出物理内存限制(maxmemory)时,内存的数据会开始和磁盘产生频繁的交换(swap),使 Redis 的性能急剧下降。Redis 提供了配置参数 maxmemory 来限制内存超出期望大小。当实际内存超出 maxmemory 时, Redis 提供了几种可选策略(maxmemory-policy)来让用户自己决定该如何腾出新的空间以继续提供读写服务。

  • volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
  • volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
  • volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
  • allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
  • allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
  • allkeys-lfu:淘汰所有 key 中 “热度” 最低的 key。
  • volatile-lfu:淘汰设置了过期时间的 key 中 “热度” 最低的 key。
  • no-enviction(驱逐):禁止驱逐数据

volatile-xxx 策略只会针对带过期时间的 key 进行淘汰, allkeys-xxx 策略会对所有的key 进行淘汰。

redis持久化

RDB

Redis DataBase 给内存数据做快照

  • 手动命令:save命令,阻塞主线程,bgsave命令,创建子进程,专门写入RDB文件,避免了主线程的阻塞

  • 配置文件:满足save配置 触发bgsave

  • 自动机制:关闭程序时会自动保存

  • 配置文件中配置 save "" 可关闭RDB持久化

    image

弊端:如果在两次快照的时间片期间数据修改了,并且服务宕机,可能会导致新的数据没有持久化到RDB文件

image

AOF

Append only File 记录每一次写操作

缓冲重写手动命令:bgrewraiteaof

缓冲重写配置文件:auto-aof-rewrite-percentage 100(上一次重写的比例) auto-aof-rewrite-min-size 64mb(触发重写的最小体积)

image

重启加载流程

image

锁相关

为什么需要加锁

  • 对共享数据进行一个非原子性操作的时候,由于多线程竞争导致的数据不一致(线程不安全问题),使用同步解决线程的安全性问题
  • 某一块代码同一时刻只想让一个线程进入

锁的分类

悲观锁

  • jdk锁
    • synchronized
      • 作用:让方法、代码块变成同步,同一时刻只有一个线程进入
      • 特点:自动获取、自动释放、适合简单的同步需求,代码简洁且不易出错。
      • 原理:当线程进入同步代码时,必须先获取共享对象的监视器锁(互斥锁),如果一个线程已经获得监视器锁,其他线程必须等待,直到锁被释放,线程执行完成会释放锁,让其他资源尝试获取
    • Lock:java锁的接口,常用实现类如:ReentrantLock(可重入锁)、ReadWriteLock(读写锁,通过I/O写入时需要加锁)
      • 使用:
        • 声明锁对象
        • 加锁
          • lock()方法 加锁,其他线程加不了锁就等待,
          • trylock(指定时间,单位) 线程尝试获取锁,能获取到返回true 获取不到返回false不等待
        • 释放锁
  • 分布式锁
    • redis实现分布式锁

乐观锁

  • 版本号机制

    给数据加一个版本号,修改的时候 where id = #{id} and version = #{version},携带修改之前查询到的版本号,并且每一次修改该数据,再版本号上加1,如果之前没有人修改过数据,版本号没有变化,可以修改成功,如果这条数据已经被人修改过,那么版本号不匹配修改会失败,可以进行重试。mybatis-plus自带乐观锁机制

  • CAS操作

    CAS 是乐观锁的一种常见实现,它是通过原子操作来保证数据的一致性。CAS 操作包含三个参数:

    预期值(expected value):当前变量的值。
    新值(new value):想要更新的值。
    目标变量(target variable):目标值,也就是需要更新的变量。

    CAS 会首先比较当前变量的值是否等于预期值,如果相等,则将目标变量更新为新值;如果不相等,则说明数据已经被修改,CAS 操作失败,通常会重试。

    CAS 是一个原子操作,不会被中断,因此可以避免多线程访问共享资源时带来的数据不一致问题。

思维导图

image

posted @ 2025-05-23 09:40  icui4cu  阅读(8)  评论(0)    收藏  举报