Redis基础

什么是Redis

Redis全称Remote Dictionary Server远程词典服务器,是一个基于内存的开源的高性能的键值型NoSQL数据库

特性:

  • k-v型,value支持多种不同数据结构,功能丰富
  • 单线程,每个命令具备原子性
  • 低延迟、速度快(基于内存、IO多路复用、良好的编程)
  • 支持数据持久化
  • 支持主从集群、分片集群

Redis数据结构

Redis是一个key-value数据库,key一般是String类型,但是value类型多种多样。

String 字符串

Redis 的 string 可以包含任何数据,比如 jpg图片或者序列化的对象(java 中对象序列化函数 serialize),内部实现,其本质是一个byte数组

struct sdshdr {  
      long len; //buf数组的长度  
      long free; //buf数组中剩余可用字节数  
      char buf[]; //存储实际字符串内容  
} 

String 采用预分配冗余空间的方式来减少内存的频繁分配,内部为当前字符串实际分配的空间 capacity 一般要高于实际字符串长度 len。当字符串长度小于 1M 时,扩容都是加倍现有的空间,如果超过 1M,扩容时一次只会多扩 1M 的空间。需要注意的是字符串最大长度为 512M。

常用命令

设置/获取 键值对

>set name zjm
"OK"
>get name
"zjm"

批量设置/批量获取 键值对

>mset name1 maimai name2 xiaomai
"OK"
>mget name1 name2
1. "maimai"
2. "xiaomai"

自增/自减/按步长自增/按步长自减

如果 value 值是一个整数,可以使用自增/自减命令

>set age 19
"OK"
>incr age
"20"
>decr age
"19"
>incrby age 1
"20"
>decrby age 1
"19"

过期和set命令扩展

可以对 key 设置过期时间,到点自动删除,这个功能常用来控制缓存的失效时间。不过这个 [自动删除] 的机制是比较复杂的。setnx是一个原子命令

> set name codehole
> get name
"codehole"
> expire name 5 # 5s 后过期
... # wait for 5s
> get name
(nil)
> setex name 5 codehole # 5s 后过期,等价于 set + expire
> get name
"codehole"
... # wait for 5s
> get name
(nil)
> setnx name codehole # set 和 expire 原子执行,因为 name 不存在就执行创建成功
(integer) 1
> get name
"codehole"
> setnx name holycoder #  set 和 expire 原子执行,因为 name 存在 set 创建不成功
(integer) 0 
> get name
"codehole"

List 列表

Redis的列表相当于java里面的LinkedList
列表中的每个元素都使用双向指针,串起来可以同时支持前向后向遍历。

在这里插入图片描述

右边进左边出: 队列

队列是先进先出的数据结构,常用消息排队和异步逻辑处理,它会确保元素的访问顺序性。

>rpush books python java golang 
>llen books
>lpop books

右边进右边出: 栈

栈是先进后出的数据结构,根队列正好相反。拿Redis的列表数据结构来做栈栈使用的业务场景并不多见。

>rpush books python java golang
>rpop books

Hash 哈希

hash的底层存储有两种数据结构

  • ziplist:如果hash对象保存的键和值字符串长度都小于64字节且hash对象保存的键值对数量小于512,则采用这种
  • dict(字典):其他情况采用这种数据结构

hash 结构也可以用来存储用户信息,不同于字符串一次性需要全部序列化整个对象,hash 以对用户结构中的每个字段单独存储。这样当我们需要获取用户信息时可以进行部分获取。而以整个符串的形式去保存用户信息的话就只能一次性全部读取,这样就会比较浪费网络流量。

hash 也有缺点,hash 结构的存储消耗要高于单个字符串,到底该使用 hash 还是字符串,需要根据实际情况再三权衡。

> hset books java "thinks in java" # books 是 key,Java 是 hash中的 key 
                                   # 如果 字符串包含空格 要用引号括起来
(integer) 1
> hset books golang "concurrency in go" 
(integer) 1
> hgetall books  # entries(),key 和 value 间隔出现  
1) "java"
2) "thinks in java"
3) "golang"
4) "concurrency in go"
> hlen books
(integer) 2
> hget books golang
"concurrency in go"
> hset books golang "learning go programming" # 因为是更新操作,所以返回 0
(integer) 0
> hget books golang    
"learning go programming"
> hmset books java "effective java" golang "modern golang
programming"  # 批量 set
OK

Set 集合

set 是一个无序的、自动去重的集合数据类型,Set 底层用两种数据结构存储

  • intset:如果元素个数少于默认值512且元素可以用整型,则用这种数据结构

  • dict(字典):其他情况采用这种数据结构

当集合中最后一个元素移除之后,数据结构自动删除,内存被回收。 set 结构可以用来存储活动中奖的用户 ID,因为有去重功能,可以保证同一个用户不会中奖两次。

> sadd books python
(integer) 1
> sadd books python # 重复
(integer) 0
> sadd books golang
(integer) 1
> smembers books  # 注意顺序,和插入的并不一致,因为 set 是无序的
1) "golang"
2) "python"
> sismember books python # 查询某个 value 是否存在,相当于 contains(o)
(integer) 1
> sismember books rust
(integer) 0
> scard books # 获取长度相当于 count()
(integer) 2   
> spop books # 弹出一个
"python"

Sorted Set(zset) 有序集合

zset为有序(有限score排序,score相同则元素字典排序),自动去重的集合数据类型,其底层实现为 字典(dict) + 跳表(skiplist),当数据比较少的时候用 ziplist 编码结构存储。

由于SortedSet的可排序特性,经常用于实现排行榜这样的功能

  • ziplist :如果有序集合保存的所有元素的长度小于默认值64字节且有序集合保存的元素数量小于默认值128个,则采用这种数据结构
  • 字典(dict) + 跳表(skiplist):其他情况采用这种数据结构
> zadd books 9.0 "think in java"
(integer) 1
> zadd books 8.9 "java concurrency"    
(integer) 1
> zadd books 8.6 "java cookbook"    
(integer) 1
> zrange books 0 -1 # 按 score 排序列出,参数区间为排名范围   
1) "java cookbook"
2) "java concurrency"
3) "think in java"
> zrevrange books 0 -1 # 按 score 逆序列出,参数区间为排名范围 
1) "think in java"
2) "java concurrency"
3) "java cookbook"
> zcard books   # 相当于 count()
(integer) 3   
> zscore books "java concurrency" # 获取指定 value 的 score 
"8.9000000000000004" # 内部 score 使用 double 类型进行存储,所以存在小数点精度问题
> zrank books "java concurrency" # 排名
(integer) 1
> zrangebyscore books 0 8.91 # 根据分值区间遍历 zset 
1) "java cookbook"
2) "java concurrency"
> zrangebyscore books -inf 8.91 withscores # 根据分值区间 (-∞, 8.91] 遍历 zset,同时返回分值。inf 代表 infinite,无穷大的意思。 
1) "java cookbook"
2) "8.5999999999999996"
3) "java concurrency"
4) "8.9000000000000004"
> zrem books "java concurrency" # 删除 value
(integer) 1
> zrange books 0 -1
1) "java cookbook"
2) "think in java"    

过期策略与内存淘汰机制

Redis中同时使用了惰性过期定期过期两种过期策略。

假设Redis当前存放30万个key,并且都设置了过期时间,如果你每隔100ms就去检查这全部的key,CPU负载会特别高,最后可能会挂掉。
因此,redis采取的是定期过期,每隔100ms就随机抽取一定数量的key来检查和删除的。
但是呢,最后可能会有很多已经过期的key没被删除。这时候,redis采用惰性删除。在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间并且已经过期了,此时就会删除。
惰性过期

只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。

定期过期

每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。

expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。

但是,如果定期删除漏掉了很多过期的key,然后也没走惰性删除。就会有很多过期key积在内存内存,直接会导致内存爆的。或者有些时候,业务量大起来了,redis的key被大量使用,内存直接不够了。难道redis直接这样挂掉?不会的!Redis用8种内存淘汰策略保护自己

内存淘汰策略

volatile-lru:当内存不足以容纳新写入数据时,从设置了过期时间的key中使用LRU(最近最少使用)算法进行淘汰;
allkeys-lru:当内存不足以容纳新写入数据时,从所有key中使用LRU(最近最少使用)算法进行淘汰。
volatile-lfu:4.0版本新增,当内存不足以容纳新写入数据时,在过期的key中,使用LFU算法进行删除key。
allkeys-lfu:4.0版本新增,当内存不足以容纳新写入数据时,从所有key中使用LFU算法进行淘汰;
volatile-random:当内存不足以容纳新写入数据时,从设置了过期时间的key中,随机淘汰数据;。
allkeys-random:当内存不足以容纳新写入数据时,从所有key中随机淘汰数据。
volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的key中,根据过期时间进行淘汰,越早过期的优先被淘汰;
noeviction:默认策略,当内存不足以容纳新写入数据时,新写入操作会报错。

Redis持久化机制

RDB

RDB全称Redis Database Backup File(Redis数据备份文件),也被叫做Redis数据快照。简单来说就是把内存中的所有数据都记录到磁盘中。当Redis实例故障重启后,从磁盘读取RDB快照文件,恢复数据。快照文件称为RDB文件,默认是保存在当前运行目录。

在这里插入图片描述

bgsave原理

bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据。完成fork后读取内存数据并写入 RDB 文件。

fork采用的是copy-on-write技术:

  • 当主进程执行读操作时,访问共享内存;
  • 当主进程执行写操作时,则会拷贝一份数据,执行写操作。

RDB 的优点

  • 适合大规模的数据恢复场景,如备份,全量复制等

RDB的缺点

  • RDB执行间隔时间长,两次RDB之间写入数据有丢失的风险
  • fork子进程、压缩、写出RDB文件都比较耗时

AOF

AOF(append only file) 持久化,采用日志的形式来记录每个写操作,追加到文件中,重启时再重新执行AOF文件中的命令来恢复数据。它主要解决数据持久化的实时性问题。默认是不开启的。

AOF文件重写

Redis 提供了 bgrewriteaof 指令用于对 AOF 日志进行瘦身。其原理就是开辟一个子进程对内存进行遍历转换成一系列 Redis 的操作指令,序列化到一个新的 AOF 日志文件中。序列化完毕后再将操作期间发生的增量 AOF 日志追加到这个新的 AOF 日志文件中,追加完毕后就立即替代旧的 AOF 日志文件了,瘦身工作就完成了
在这里插入图片描述

如图,AOF原本有三个命令,但是set num 123 和 set num 666都是对num的操作,第二次会覆盖第一次的值,因此第一个命令记录下来没有意义。

所以重写命令后,AOF文件内容就是:mset name jack num 666

Redis也会在触发阈值时自动去重写AOF文件。阈值也可以在redis.conf中配置。

Redis 4.0 混合持久化

重启 Redis 时,我们很少使用 rdb 来恢复内存状态,因为会丢失大量数据。我们通常使用 AOF 日志重放,但是重放 AOF 日志性能相对 rdb 来说要慢很多,这样在 Redis 实例很大的情况下,启动需要花费很长的时间。
  Redis 4.0 为了解决这个问题,带来了一个新的持久化选项——混合持久化。将 rdb 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是自持久化开始到持久化结束的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小。
在这里插入图片描述
  于是在 Redis 重启的时候,可以先加载 rdb 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。

缓存常见问题

缓存穿透

描述:

缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。

解决方案:

  1. 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;

  2. 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击

  3. 使用布隆过滤器

缓存击穿

描述:

缓存击穿是指缓存中没有但数据库中有的单条数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力

解决方案:

  1. 设置热点数据永远不过期。

  2. 加互斥锁。读数据库时需要获取锁,一条请求拿到锁之后读取数据并更新缓存,为了防止那些抢锁失败线程重新获取到锁后又进行读数据库操作,这里采用双重检验锁方式。
    加互斥锁伪代码如下:

public String getData(String key) {
    while (true) {
        // 从缓存读取数据
        String result = getDataForRedis(key);
        if(result != null) {
            return result;
        }
        // 尝试获取锁
        if(tryLock(this)) {
            try {
                // 再次从缓存读取数据,这里想想为什么?
                // 有可能当前线程在尝试获取锁之后,
                // 其他线程已经取到数据并将其缓存到Redis上
                result = getDataForRedis(key);

                if (result != null) {
                    return result;
                }
                // 从数据库获取数据
                result = getDataForMySql(key);

                if (result != null) {
                    // 添加到 redis 缓存
                    setDataToRedis(key, result);
                    // 返回
                    return result;
                }
            }finally {
                unLock();
            }
        }else {
            // 获取锁失败,睡眠
            Thread.sleep(30);
        }
    }
}

缓存雪崩

描述:

缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至 down机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

解决方案:

  1. 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。

  2. 设置热点数据永远不过期。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值