SQL VS NoSQL

SQL NoSQL
数据结构 结构化 非结构化
数据关联 关联的 无关联
查询方式 SQL查询 非SQL
事务特性 ACID BASE
存储方式 磁盘 内存
扩展性 垂直 水平
使用场景 数据结构固定且对安全性,一致性要求较高 数据结构不固定,对一致性安全性要求不高,对性能有一定要求

特征:

  1. 键值型,值支持多种不同的数据结构,功能丰富
  2. 单线程,每个命令具有原子性。现在多线程仅仅在网络连接请求方面,内部核心命令依然是单线程的
  3. 低延迟,速度快
  4. 支持数据持久化
  5. 支持主从集群,分片集群
  6. 支持多语言

为什么Redis单线程,但是速度快?

  1. 基于内存(最重要的原因)
  2. IO多路复用
  3. 使用C语言,良好的编码

Redis安装

使用Docker安装:

  1. docker-data/redis/中执行命令wget http://download.redis.io/redis-stable/redis.conf下载config
  2. 修改权限
  3. 修改配置信息
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    bind 127.0.0.1          # 注释掉这行,表示其他机器也可以链接
    protected-mode no # 默认为yes,表示只允许本机的回环连接
    daemonize no # 默认为no,不守护进程,不是docker部署需要改为yes
    # 如果是Docker部署不需要改为yes,因为docker run -d本身就是后台启动
    requirepass xxxxxx # 密码
    appendonly yes # 持久化
    port 6379 # 默认,监听的端口
    dir . # 默认,表示当前目录即为工作目录
    database 16 # 默认,表示数据库数量
    maxmemory 512mb # 表示redis能使用的最大内存
    # logfile "redis.log" # 日志文件,默认为空,表示不记录日志,存储在dir指定的目录中
  4. 运行Redis: (网上的方法99%都是有问题的)以redis:7.2.4-alpine3.19为例
1
docker run --name redis -p 6379:6379 -v /{USER_DIR}/docker-data/redis/redis.conf:/usr/local/etc/redis/redis.conf -v /{USER_DIR}/docker-data/redis:/data -d redis:7.2.4-alpine3.19 redis-server /usr/local/etc/redis/redis.conf --appendonly yes --requirepass xxxxxx

数据结构

通用命令:

  1. KEYS: 查看所有符合模板的key,不建议在生产环境上使用,查询会阻塞其他线程
  2. DEL: 删除一个 or 多个指定的key
  3. EXISTS: 判断key是否存在
  4. EXPIRE: 给key设置有效期(秒),到期自动删除
  5. TTL: 查看key的剩余有效期,-1表示永久有效, -2表示过期

基本类型

  1. String

    • 字符串底层都是字节数组的形式存储的,最大不能超过512M
    • 格式又可以分为string, int, float
    • 但是对于int, float,都转为二进制的形式存储
    • Redis的key允许多个单词形成层级结构,使用:分隔,即项目名:业务名:类型:id

    常用命令:

    1. SET: 添加或修改已经存在的一个String类型键值对
    2. GET: 根据key获取String类型的value
    3. MSET, MGET: 批量添加和批量获取多个String类型键值对
    4. INCR: 对int自增1
    5. INCRBY: 让整型的key自增并指定步长,比如incrby num 2表示让num自增2
    6. INCRBYFLOAT: 让float自增指定步长
    7. SETNX: 添加一个String类型键值对,前提是没有key,否则不执行
    8. SETEX: 添加一个String类型键值对,并指定有效期,setex key 有效期 value
  2. Hash

    • value是一个无序字段
    • String将对象序列化以后存储,修改某个字段不方便。比如value为{name: Jack, age: 21}
    • Hash每个字段独立存储,可以单独处理

    常用命令:

    1. HSET key field value: 添加或修改已经存在的一个Hash类型键值对
    2. HGET key field: 根据key获取Hash类型的value
    3. HMSET, HMGET: 批量添加和批量获取多个Hash类型键值对
    4. HGETALL: 获取一个Hash类型中所有的field
    5. HKEYS: 获取Hash类型中所有的field
    6. HVALS: 获取Hash类型中所有的value
    7. HINCRBY: 让Hash的key自增并指定步长
    8. HSETNX: 添加一个Hash类型key的field,前提是没有field,否则不执行
  3. List

    • 可以看作双向链表,有序、元素可以重复、插入删除速度快,但是查询速度一般
    • 朋友圈点赞,或评论列表

    常用命令:

    1. LPUSH key element ...: 左侧插入一个 or 多个元素
    2. LPOP key: 左侧移除并返回第一个元素,没有就返回nil
    3. RPUSH key element ...: 右侧插入一个 or 多个元素
    4. RPOP key: 右侧移除并返回第一个元素,没有就返回nil
    5. LRANGE key start end: 返回一段角标范围内的所有元素
    6. BLPOP, BRPOP: 在没有元素时等待指定的时间,而不是直接范围nil
  4. Set

    • 无序,元素不重复,查找快,支持交、并、差集

    常用命令:

    1. SADD key member: 向Set中添加一个或者多个元素
    2. SREM key member: 移除Set中指定的元素
    3. SCARD key: 返回Set中元素的个数
    4. SISMEMBER key memeber: 判断一个元素是否存在于Set
    5. SMEMBERS: 获取Set中所有的元素
    6. SINTER key1 key2: 求key1 和 key2的交集
    7. SDIFF key1 key2: 求key1 和 key2的差集
    8. SUNION key1 key2: 求key1 和 key2的并集
  5. SortedSet

    • 可排序集合,每个元素都带有score属性,底层是实现一个跳表和哈希表
    • 可排序,不重复,查询速度快。用于实现排行榜

    常用命令:

    1. ZADD key score member: 添加一个 or 多个元素,如果已经存在则更新score
    2. ZREM key member: 移除指定的元素
    3. ZSCORE key member: 获取指定元素的score
    4. ZRANK key member: 获取指定元素排名
    5. ZSCARD key: 返回元素的个数
    6. ZCOUNT key min max: 统计score在给定范围内元素的个数
    7. ZINCRBY key increment member: 让指定元素自增,步长为incrememnt
    8. ZRANGE key min max: 按照score排序后,获取指定排名范围内的元素
    9. ZRANGEBYSCORE key min max: 按照score排序后,获取指定score范围内的元素
    10. ZINTER key1 key2: 求key1 和 key2的交集
    11. ZDIFF key1 key2: 求key1 和 key2的差集
    12. ZUNION key1 key2: 求key1 和 key2的并集
    13. Z后面添加REV即可实现降序

特殊类型

  1. GEO
  2. BitMap
  3. HyperLog

缓存更新策略

内存淘汰 超时剔除 主动更新
说明 不用自己维护,利用内存淘汰机制,内存不足时自动淘汰部分数据,下次查询时再更新 给缓存数据添加TTL时间,到期后自动删除缓存,下次查询时更新缓存 编写业务逻辑,修改数据库的同时更新缓存
一致性 一般
维护成本
  • 对于低一致性需求,使用内存淘汰机制。比如店铺类型这种修改频率很少的内容
  • 对于高一致性需求,使用主动更新,并且将超时剔除作为兜底方案。比如店铺的详情、优惠券查询缓存

主动更新策略

  1. Cache Aside Pattern: 由缓存调用者在更新数据库的同时更新缓存
    • 可控性更高,用的更多
  2. Read/ Write Through Pattern: 缓存与数据库整合为一个服务,由服务来维护一致性。调用者调用该服务,无需关心缓存一致性问题
  3. Write Behind Caching Pattern: 调用者只操作缓存,由其他线程异步将缓存数据持久化到数据库,最终保持一致
    • 需要维护异步线程
    • 一致性难以保证

Cache Aside Pattern

  1. 删除缓存和更新缓存的选择
    • 更新缓存:每次更新数据库都要更新缓存,如果对应的是写多读少的环境,则会产生大量的无效写操作
    • 删除缓存:每次更新数据库时都让缓存失效,查询时再更新缓存,延迟加载,有效更新更多,因此用的更多
  2. 如果保证缓存与数据库的操作同时成功或失败
    • 单体系统,将缓存与数据库放在一个事务中
    • 分布式系统,就需要利用TCC分布式事务方案
  3. 先操作缓存还是操作数据库(线程安全问题)
    • 初始缓存和数据库内容均为10

    • 先删除缓存再操作数据库:

      • 线程1删除缓存,线程1更新数据库=20

      • 线程2查询缓存未命中,查询数据库得到20,并写入缓存。正常

      • cachesqlright

      • 线程1删除缓存

      • 线程2查询缓存未命中,查询数据库得到10,并写入缓存

      • 线程1更新数据库=20。异常(概率高)

      • cachesqlerror

    • 先操作数据库再删缓存

      • 线程2更新数据库=20,再删除缓存

      • 线程1查询缓存未命中,查询数据库得到20,并写入缓存。正常

      • sqlcacheright

      • 假设缓存失效了,线程1查询缓存未命中,查询数据库得到10

      • 线程2更新数据库=20,删除缓存

      • 线程1写入缓存=10。异常(概率低)

      • sqlcacheerror

  • 因此,先操作数据库再删缓存用的更多一些,可以加上超时剔除策略兜底
    • 读操作:
      • 缓存命中则直接返回
      • 缓存未命中则查询数据库,写入缓存,设定超时时间
    • 写操作:
      • 先写数据库,然后删除缓存
      • 确保数据库与缓存操作的原子性

缓存穿透

  • 客户端请求的数据在缓存和数据库中都不存在,则永远不会生效,请求永远会打到数据库中

解决办法

  1. 缓存空对象
    • 优点: 实现简单,维护方便
    • 缺点:
      • 额外内存消耗,可以设置短的TTL进行过期删除
      • 可能造成短期的不一致性,也可以通过短的TTL缓解
  2. 布隆过滤器
    • 客户端请求的时候先请求布隆过滤器,如果存在则放行,不存在则直接拒绝
    • 布隆过滤器判断不存在说明一定不存在,判断存在但是不一定真的存在,如果实际上不存在,则又发生了穿透
    • 优点: 内存占用小,没有多余的key
    • 缺点:
      • 实现复杂
      • 存在误判
  3. 增加id的复杂度,避免被猜测规律
  4. 数据基础格式校验
  5. 热点参数限流

缓存雪崩

  • 同一时段大量的缓存同时失效,或者Redis服务宕机,导致大量请求到达服务器,带来巨大压力

解决办法

  1. 给不同的key添加TTL随机值
  2. 利用Redis集群提高可用性
  3. 给缓存业务添加降级限流策略
  4. 给业务添加多级缓存

缓存击穿(热点key问题)

  • 被高并发访问并且缓存重建业务比较复杂的key失效了,无数请求访问会在瞬间给数据库带来巨大的冲击

解决办法

  1. 互斥锁
    • 只有获取锁的人才能重建缓存,其他人就休眠一会再重试
    • 会出现互相等待的问题,只有一个在获取数据,其他都在等待
    • lock
  2. 逻辑过期
    • 不设置TTL,只设置逻辑过期时间,其他用户发现逻辑过期了,就开启独立新线程,查询数据库数据,重置逻辑时间
    • 其他人在没有得到互斥锁的时候直接返回旧缓存数据
    • logicexpire
方案 优点 缺点
互斥锁 乜有额外内存消耗;保证一致性;实现简单 线程需要等待,性能受影响;有死锁风险
逻辑过期 线程无需等待,性能较好 不能保证一致性;有额外内存消耗;实现复杂
  • 因此互斥锁和逻辑过期两种方案实际上是一致性和可用性之间的取舍