【数据库学习】Redis和IO模型

在这里插入图片描述

1,概念

为了达到内存访问的速度,又能控制成本像磁盘一样低廉。

  1. Redis 安装
    默认端口:6379
  2. redis英文官网
  3. redis中文官网

1)场景

  1. 性能。
    执行时间久且结果不频繁变动的SQL,适合将运行结果放入缓存。
  2. 并发。
    在大并发的情况下,所有的请求直接访问数据库,数据库会出现连接异常。这个时候,就需要使用redis做一个缓冲操作,让请求先访问到redis,而不是直接访问数据库。

2)常用shell命令

redis常用命令在redis安装目录的src目录以下,如果没有安装到usr/bin路径下,那么使用的时候需要带上路径参数,如./redis-cli

常用shell命令

命令说明备注
./redis-cli访问6379默认端口的redis-h ip 默认本机ip;
-p port 默认端口为6379;
-a psw 密码;
--raw 中文乱码处理
exit退出
clear清屏幕
Flushall清空整个 Redis 服务器的数据删除所有数据库的所有 key
redis-cli -h查看帮助
help查看帮助
help set查看set命令帮助
select 8进入8号库默认进入0号库。redis有0-15共16个库,互相隔离
EXPIRE key seconds设置key的过期时间,以秒计
PEXPIRE key milliseconds设置key的过期时间,以毫秒计
EXPIREAT key timestamp设置key的过期时间,时间参数是 UNIX 时间戳(unix timestamp)
PERSIST key移除 key 的过期时间,key 将持久保持
PTTL key返回 key 的剩余的过期时间,以毫秒计
TTL key返回 key 的剩余的过期时间,以秒计0 :立即过期
-1 :最小为0,小于0会处理成0
DEL keyName删除key
EXISTS keyName判断key是否存在
GET keyName读取key
keys *查看所有key
keys apple*列出匹配的key
type k1查看value基础数据类型

3)基础数据类型

类型多,且每种类型都有很多api支持,客户端代码会很轻盈。–>计算向数据移动(redis数据保持不动,client请求数据时,redis通过函数计算获取到少量数据返回给client,减少网络开销。)

1>String(字符串、byte)

  1. 支持:字符类型、数值类型、bitmap
    string 类型是二进制安全的(传输时只有字节流、没有字符流)。意思是 redis 的 string 可以包含任何数据,并且直接以二进制形式存储。比如jpg图片或者序列化的对象、c语言的结束字符\0等。
    ==》redis客户端要统一编码,不然读和存数据不一致。
    二进制安全的意义:数据安全。读取必须编码格式相同才可以读取,单纯的01字符不容易在传输过程中被篡改、破译,如果被攻击也能及时检测出来。
  2. string 类型的值最大能存储 512MB。
  3. redis是C编写的,没有使用C的String(以\0结尾,这样就不能存\0了),而是自己实现了SDS(简单动态字符串),通过len来确定字符串长度及结尾。
a)字符类型(embstr)

不区分大小写。数值、bitmap通用。

字符串命令说明备注
set k v
get knil表示不存在的值
set key value nxnx表示不存在就set,存在则返回nil常用语分布式锁的实现,只能新建
set key value xxxx表示存在才能set,不存在返回nil只能更新
append k v如果key存在则给之前的value追加数据
setrange k offset v如果key存在给之前的value从offset开始覆盖vSET key1 "Hello World"
SETRANGE key1 6 "Redis"
“Hello Redis”
getrange k 0 3截取字符串[0:3]
正向索引:从左到右依次为0 1 2;
反向索引:从右到左依次为-1 -2 -3
GETRANGE mykey 0 -1 表示取出所有
getset k vset k v并返回旧的value减少1次io通信
strlen k获取存储的value的值长度根据当前编码方式返回的数据不同。
比如“中”在utf-8中是3个自己,gbk中是2个字节。
mset k1 v1 k2 v2同时设置k1=v1, k2=v2
mget k1 k2同时取出k1 k2的值
msetNx k1 k2set k v nx 多个值原子操作,如果一个k设置失败则都失败
object encoding k1查看字符串类型的具体编码类型(embstr、int、raw)
b)数值类型(int)

场景:一般做一些复杂的计数功能的缓存。
如:抢购、秒杀、点赞、评论数、好友数等数量的控制(精准但是不要求可靠性),规避并发下对数据库事务的操作,完全由redis内存操作替代。

int支持范围:0-225

数值命令说明备注
incr kvalue++
incr k 5value+5相当于 incrby k 5
decr kvalue–
decr k 5value+5
d)bitmap(raw 二进制)
bitmap命令说明备注
setbit k offset v给k对应的value设置偏移量offset位置的值为v,并返回之前的值;
偏移量通过二进制位计(ASCII)算,而不是字节;
setbit k 12 1 设置第12位为1
getbit k offset获取第offset位置的值正反向索引按二进制位来算
bitop and/or destKey k1 k2取出k1 k2的值并进行与、或操作然后塞入目标key中
bitcount k1 startIndex endIndex统计bit=1总共出现的次数范围是字节索引的:startIndex endIndex
bitpos k1 bit startIndex endIndex查找k1的值的二进制中bit(0或者1)的数量,范围是字节索引的:startIndex endIndex,返回vaule的二进制索引偏移量只找第一个

场景:

  1. 统计用户登录天数,且窗口随机。==》速度快、存储小
# 假设每一天记一个二进制位,366天只需要365/8=46个字节存储一个用户的登录。
setbit sean 1 1
...
setbit sean 7 1
...
setbit sean 33 1
...
strlen sean  # 总共占用字节数  8位1字节,strlen算的是字节数
bitcount sean -2 -1 #最后2周1的个数即最后2周用户登录的次数  最后2个字节刚好14位
  1. 统计活用户。
    区分用户:僵尸用户、忠诚用户、忠诚用户
# 比如618三天用户登录过的才算活跃用户,统计它的数量并 去重

setbit 20230601 1 1
setbit 20230602 1 1
setbit 20230602 7 1  # 7是用户id
#618活跃用户数量
bitop or destKey 20230601 20230602   
bitcount destKey 0 -1  #活用户为2

2>hashes(散列、字典)

a)特性
  1. redis的哈希值是字符串字段和字符串之间的映射,是表示对象的完美数据类型。
  2. 哈希中的字段数量没有限制,所以可以在你的应用程序以不同的方式来使用哈希。
b)场景

这里value存放的是结构化的对象,比较方便的就是操作其中的某个字段。
举例:单点登录中用于存储用户信息,以cookieId作为key,设置30分钟为缓存过期时间,能很好的模拟出类似session的效果。

c)使用

操作示例

d)存储结构

k-map
在这里插入图片描述

3>lists(列表)

双向链表。

a)特性
  1. 排序为插入的顺序,使用链表实现(增删快,查找慢)。
  2. 列表的最大长度为2^32-1。
b)场景

最新消息排行等功能(比如朋友圈的时间线) ;
简单的消息队列的功能:

  1. 可以用列表获取最新的内容(像帖子,微博等),用ltrim很容易就会获取最新的内容,并移除旧的内容。
  2. 用列表可以实现生产者消费者模式,生产者调用lpush添加项到列表中,消费者调用rpop从列表中提取,如果没有元素,则轮询去获取,或者使用brpop等待生产者添加项到列表中。
  3. 利用lrange命令,做基于redis的分页功能,性能极佳,用户体验好。
c)使用

操作示例

4>sets(集合)

a)特性
  1. 无序的字符串集合,集合中的值是唯一的,无序的。
  2. Redis 中 集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。集合中最大的成员数为 2的32次方 - 1 (4294967295, 每个集合可存储40多亿个成员)。
  3. 使用@EnableCaching开启声明式缓存支持,这样就可以使用基于注解的缓存技术。注解缓存是一个对缓存使用的抽象,通过在代码中添加下面的一些注解,达到缓存的效果。
b)场景
  1. 可以对集合执行很多操作,测试元素是否存在,对多个集合执行交集、并集和差集等等。
    如:共同好友;
    如:好友推荐:根据tag求交集,大于某个阈值就可以推荐。
  2. 我们通常可以用集合存储一些无关顺序的,表达对象间关系的数据,例如用户的角色,可以用sismember很容易就判断用户是否拥有某个角色。
  3. 在一些用到随机值的场合是非常适合的,可以用 srandmember/spop 获取/弹出一个随机元素。
  4. 利用唯一性,统计访问网站的所有独立ip
d)使用

操作示例

  1. add 添加
redisTemplate.opsForSet().add("setValue","A","B","C","B","D","E","F"); 
  1. 删除
//弹出变量中的元素: pop(K key)  弹出并删除
    Object popValue = redisTemplate.opsForSet().pop("setValue");  
//批量移除变量中的元素: remove(K key, Object... values),返回移除变量中的元素个数
		long removeCount = redisTemplate.opsForSet().remove("setValue","E","F","G");  
		//members(K key) 取值
    Set set = redisTemplate.opsForSet().members("setValue");  
    //获取变量中值的长度
    long setLength = redisTemplate.opsForSet().size("setValue");  
    //随机获取变量中的元素
    Object randomMember = redisTemplate.opsForSet().randomMember("setValue"); 
    //随机获取变量中指定个数的元素
    List randomMembers = redisTemplate.opsForSet().randomMembers("setValue",2);  
    //检查给定的元素是否在变量中
    boolean isMember = redisTemplate.opsForSet().isMember("setValue","A");  
    //匹配获取键值对:
    //ScanOptions.NONE为获取全部键值对;
    //ScanOptions.scanOptions().match("C").build()匹配获取键位map1的键值对,不能模糊匹配。
    Cursor<Object> cursor1 = redisTemplate.opsForSet().scan("setValue", ScanOptions.NONE);  
    Cursor<Object> cursor = redisTemplate.opsForSet().scan("setValue", ScanOptions.scanOptions().match("C").build());  
    while (cursor.hasNext()){  
        Object object = cursor.next();  
        System.out.println("通过scan(K key, ScanOptions options)方法获取匹配的值:" + object);  
    }  
    //通过集合求差值:difference(K key, Collection<K> otherKeys)
    //通过给定的key求2个set变量的差值:difference(K key, K otherKey)
    //将求出来的差值元素保存:differenceAndStore(K key, K otherKey, K destKey)
    //将求出来的差值元素保存:differenceAndStore(K key, Collection<K> otherKeys, K destKey)
    List list = new ArrayList();  
    list.add("destSetValue");  
    Set differenceSet = redisTemplate.opsForSet().difference("setValue",list);  
    System.out.println("通过difference(K key, Collection<K> otherKeys)方法获取变量中与给定集合中变量不一样的值:" + differenceSet);  
    //获取去重的随机元素。distinctRandomMembers(K key, long count)
    set = redisTemplate.opsForSet().distinctRandomMembers("setValue",2);  
    System.out.println("通过distinctRandomMembers(K key, long count)方法获取去重的随机元素:" + set);  
    //获取2个变量中的交集。intersect(K key, K otherKey);intersect(K key, Collection<K> otherKeys)  
    set = redisTemplate.opsForSet().intersect("setValue","destSetValue");  
    System.out.println("通过intersect(K key, K otherKey)方法获取交集元素:" + set);
		//获取2个变量交集后保存到最后一个参数上。intersectAndStore(K key, K otherKey, K destKey)
		//intersectAndStore(K key, Collection<K> otherKeys, K destKey)
		redisTemplate.opsForSet().intersectAndStore("setValue","destSetValue","intersectValue");  
    set = redisTemplate.opsForSet().members("intersectValue");  
    System.out.println("通过intersectAndStore(K key, K otherKey, K destKey)方法将求出来的交集元素保存:" + set); 
    //获取2个变量的合集。 union(K key, K otherKey);union(K key, Collection<K> otherKeys);
    //获取2个变量合集后保存到最后一个参数上。unionAndStore(K key, K otherKey, K destKey);unionAndStore(K key, Collection<K> otherKeys, K destKey)
    set = redisTemplate.opsForSet().union("setValue","destSetValue");  
    System.out.println("通过union(K key, K otherKey)方法获取2个变量的合集元素:" + set);  
  1. 修改
//转移变量setValue的元素值A到目的变量destSetValue
boolean isMove = redisTemplate.opsForSet().move("setValue","A","destSetValue");  
if(isMove){  
    set = redisTemplate.opsForSet().members("setValue");  
    System.out.print("通过move(K key, V value, K destKey)方法转移变量的元素值到目的变量后的剩余元素:" + set);  
    set = redisTemplate.opsForSet().members("destSetValue");  
    System.out.println(",目的变量中的元素值:" + set);  
} 

5>zset(sorted sets 有序集合)

将Set中的元素增加一个权重参数score,元素按score有序排列。

a)特性
  1. 不重复的有序集合
  2. 支持范围查询
    跳表。
  3. 查询元素score权重:O(1)
    有序集合中的每个元素都关联了一个浮点值,称为分数。==》可以做topN操作
    通过哈希表组织索引(score有序)。
b)场景

1、排行榜
2、带权重的消息队列

c)使用

操作示例

d)原理:跳表

3)场景

  1. 消息队列
    Redis支持发布订阅模式和Stream,可以作为轻量级消息队列使用,用于异步处理任务和处理高并发请求。
  2. 排行榜
    利用有序集合和列表结构,设计实时排行榜。
  3. 分布式锁
  4. 地理位置应用
    Redis支持GEO,支持地理位置定位和查询。
  5. 分布式限流
    Redis提供了令牌桶和漏桶算法的实现。
  6. 分布式session管理
    保证多台服务器之间的会话同步;
  7. 布隆过滤器
    Redis提供了Bloom Filter数据结构的实现,可以高效的检测一个元素是否存在于集合中。

4)通信协议:RESP(REdis Serialization Protocol)

  1. 简单高效、易于解析。
  2. 基于TCP的协议。
    以命令名称作为第一个参数;
    请求和响应都以行结束符\r\n作为分隔符。
    包含:参数个数,参数长度,参数数据。

5)CAP

redis是AP。
redis的一致性模型是最终一致性,某个时间点的数据不是最新的。另外,在分布式设计中采用的是异步复制,导致节点之间数据同步延迟和不一致的可能性。

CAP理论:
一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)、分区容错性(Partitiontolerance)这三项中的两项。

2,原理

1)事务

  1. 原子性(Atomic)
    不提供回滚,只保证原子性。
    1)为了保证高性能,牺牲回滚。
    2)原子性的保证:命令作为一个Lua脚本执行,检查每一个事务命令是否正确,不正确就不执行。脚本作为一个独立的事务,由其它命令过来会暂存,等当前脚本执行完毕再执行。执行时如果报错,不会回滚,将会影响后续命令。

  2. 一致性(Consistency)
    如果执行一半也可以恢复。

  3. 隔离性(Isolation)
    单线程的,串行执行。

  4. 持续性(永久性,Durability)
    RDB、AOF

事务实现:

1>事务开始(MULTI命令)

MULTI命令将客户端状态的flags属性中打开REDIS_MULTI标识。
REDIS_MULTI标识之后表示一个事务开始。

2>命令入队

根据发送来的指令执行不同的操作。

  1. 如果是MULTI、EXEC、WATCH、DISCARD命令,立即执行;
  2. 其他命令,入事务队列。
    检查命令是否正确,不正确直接关闭对应客户端的REDIS_MULTI标识,返回错误信息给客户端。
    放入队列,返回QUEUED给客户端。
    如果执行过程中出现逻辑错误,继续执行,全部执行完。==》为了性能不做逻辑检查

WATCH命令
乐观锁,提供check and set(CAS)行为。
监控一个或多个key,一旦有1个变更,之后的事务就不再执行。直到EXEC命令停止监控。
UNWATCH命令:放弃key的监控。

DISCARD命令
情况事务队列,放弃事务。

3>事务执行(EXEC命令)

客户端发送EXEC命令,服务器执行EXEC逻辑。

  1. 如果客户端不包含REDIS_MULTI,或者包含REDIS_DIRTY_CAS、REDIS_DIRTY_EXEC命令,直接取消事务执行。
  2. 否则遍历客户端事务队列,FIFO执行所有命令。最后返回结果。

4>缓存的并发竞争key的问题

通过客户端去set一个key,由于redis cluster使用分片,多个key都不一定在同一个结点上,因此事务十分鸡肋。

处理并发竞争key的方案:

  1. 如果对这个key操作,不要求顺序
    加锁,谁抢到谁干活。
  2. 如果对这个key操作,要求顺序
    1)时间戳机制
    客户端A在set完数据之后保存时间戳(valueA 3:00),客户端2如果必须在A的后面执行,发现A的时间戳比现在晚,那么不执行。
    2)其他方法,比如利用队列,将set方法变成串行访问也可以。

2)持久化机制

1>RDB(redis Database,数据快照)

redis快的主要原因是内存存储,持久化机制:数据快照。

a>原理

在指定的时间间隔内将内存中的数据集快照写入磁盘。通过fork一个子进程,将当前内容写入dump.rdb文件。==》先将数据集写入临时文件,写入成功之后再替换之前的文件,用二进制压缩文件。

b>优点
  1. 性能最大化,fork子进程完成写操作,让主进程继续处理命令 ==》IO最大化。
  2. 整个redis只有一个dump.rdb文件,方便持久化、备份。
  3. 数据量大时,比AOF启动效率更高。
c>缺点
  1. 数据安全性低
    RDB间隔一段时间才做持久化工作,容易丢失数据。
  2. 如果数据量很大,可能导致fork子进程长时间占用cpu,导致整个服务器停止服务几百毫秒甚至几秒。

2>AOF(Append Only File,推荐!)

如果两个都配了,优先使用AOF。

a>原理

以日志形式处理服务器每一次的写、删除操作,查询不会记录;==》类似mysql的binlog日志。

b>优点
  1. 数据安全;
    每次修改的数据都会记录到磁盘中间。提供了三种同步策略:每秒同步、每修改同步、不同步,同步都是异步完成,性能也很高。
  2. append写文件,即使中途宕机也不会影响已存在的内容。
  3. AOF的rewrite模式,定期重写文件,对文件进行压缩。
c>缺点
  1. 相比RDB,文件存储更大,恢复更慢。
  2. 数据集大时,启动慢。
  3. 运行效率比RDB低。

3> 文件存储形式

  1. redis通过key来分文件夹。
    如key:abc:mdr:save 在redis中会自动划分文件夹abc/mdr,并将save作为key存储在abc/mdr文件夹下。
    删除的时候删除根key可以做到批量删除。

3)过期策略以及内存淘汰机制

redis是key-value数据库,可以设置key的过期时间,缺省时表示永不过期。
redis采用的是定期删除+惰性删除策略。

1>惰性删除

每次获取key时,redis检查key的过期了就删除。
节省cpu资源,但会占用大量内存。

内存淘汰机制:长期占用大量内存需要采用内存淘汰机制。
redis.conf中有一行配置:# maxmemory-policy volatile-lru

  1. noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。(不推荐)
  2. allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。(推荐使用)
  3. allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。(不推荐)
  4. volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。这种情况一般是把redis既当缓存,又做持久化存储的时候才用。(不推荐)
  5. volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。(不推荐)
  6. volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。(不推荐)

2>定时过期

定时器到了一定时间就删除key。
cpu占用多,节省内存。

3>定期删除(折中方案)

redis默认每隔100ms检查过期key,有则删除。==》不是全盘扫描,随机抽取进行检查。
通过调整扫描间隔时间和每次扫描耗时,达到cpu和内存资源最优的平衡效果。

4)IO模型

对于Linux来说 “everything is a file”,包括外部设备都看作文件夹来操作。

fd(File descriptor,文件描述符)
非负整数,本质是一个索引值。
open系统调用文件时,内核向进程返回一个文件描述符;
read、write文件时作为参数传入fd即可确定具体的文件;
fd范围:0-OPEN_MAX-1
特殊fd:
fd=0(STDIN_FILENO,标准输入);
fd=1(STDOUT_FILENO,标准输出);
fd=2(STDERR_FILENO,标准错误)。

1>BIO(Blocking IO,阻塞IO)

早期的Socket是blocking的,fd到了read命令才能返回,cpu要等待处理完成才能处理下一个。==》cpu利用率低
在这里插入图片描述

2>NIO(nonblock IO,同步非阻塞IO)

socket开始支持nonblock,此时可以单线程请求内核了(减少线程切换),处理完一个fd再处理下一个。==>问题:大批量的请求需要轮询很多次请求内核,内核态用户态频繁切换成本很大
在这里插入图片描述

3>多路复用的NIO

内核增加了select系统调用命令,一个线程批量处理多个tcp连接,每接收到一个fd(writefds(写)、readfds(读)、和 exceptfds(异常))就返回进程,然后查询获取到就绪的fd进行IO操作。==》问题:用户态内核态数据(fd)来回拷贝浪费资源
在这里插入图片描述

Linux中常见的IO多路复用技术包括:

IO多路复用技术版本吧优点缺点
select最原始只能监听1024个fd;
返回的fd通过轮询筛选出就绪fd
pollfd无限制;监听的fd越多复杂度越高;
返回的fd通过轮询筛选出就绪fd
epolllinux2.4.5fd无限制;
优化了就绪fd查询复杂度O(1),更高效;
epoll(Linux内核的可扩展I/O事件通知机制)
  1. 每次注册新事件调用epoll_ctl时,把所有fd拷贝进内核,而不是在epoll_wait时重复拷贝。==》保证每个fd只拷贝一次;
    通过内核态和用户态之间的共享空间(红黑树、链表)来存之前需要来回拷贝的fd(和0拷贝不同)。
  2. 通过epoll_wait查询就绪fd,如果有直接使用o(1),不需要遍历。
  3. 支持的fd上限是最大可打开文件的数目(句柄一般默认4096);可以通过命令查看:cat /proc/sys/fs/file-max

5>零拷贝(zero-copy,sendfile)

在这里插入图片描述

传统IO 发送一个文件需要4次IO:
一、read() 函数:2次
读取硬盘数据到内核缓冲区(read buffer);
从read buffer拷贝到用户进程的页内存;
二、write() 方法:2次
将数据从用户进程的页内存拷贝到网络缓冲区(socket buffer);
把socket buffer中的数据输出到网卡进行传输

零拷贝

直接调用sendfile方法,在内核中读取文件并发送,不需要用户态和内核态的切换。
==》kafka:用了零拷贝,直接从网卡读数据,并写入文件(mmap)。消费者读数据也通过sendfile获取

5)线程模型(单线程、I/O多路复用)

![在这里插入图片描述](https://img-blog.csdnimg.cn/2765240e51204d2796979c654421c3e7.png

  1. 单进程单线程;
    数据的读写是单线程,省去了上下文切换时间;
    6.0引入多线程处理网络线程并发请求。读写还是单线程所以不会有并发问题。
    多个client请求kernel,redis直接通过epoll多路复用方式按顺序处理每一个请求,而内存操作非常快io操作非常慢。
  2. 纯内存操作;
    数据存储在内存中。
  3. IO多路复用机制
    可以同时处理多个客户端连接。

1>文件事件处理器(file event headler)

redis 基于 reactor 模式开发的高性能网络事件处理器。

采用 IO多路复用机制同时监听多个 socket,根据 socket 上的事件来选择对应的事件处理器来处理这个事件。
如果被监听的 socket 准备好执行 accept、read、write、close 等操作的时候,与操作对应的文件事件就会产生,这时候文件事件处理器就会调用之前关联好的事件处理器来处理这个事件。

文件事件处理器的结构包含 4 个部分:

  1. 多个socket
  2. IO多路复用程序
  3. 文件事件分派器
  4. 事件处理器
    1)命令请求处理器:写数据到 redis
    2)命令回复处理器:客户端要从 redis 读数据
    3)连接应答处理器:客户端要连接 redis

6)缓存雪崩、缓存穿透、缓存击穿

1>缓存雪崩问题

缓存同一时间大面积的失效(或者redis故障导致重启、第一次启动服务),后面的所有请求都怼到数据库上,数据库短期内承受大量请求而崩掉。

解决方案:

  1. 缓存过期时间设置为随机值,避免集体失效。
  2. 给每个缓存加上缓存标记,若缓存失效则更新缓存数据。==》耗性能
  3. 缓存预热
    服务器启动前把数据一次性写入缓存。预防第一次启动服务导致缓存雪崩。
  4. 使用互斥锁
    key失效了需要去数据库查数据时,添加互斥锁, 查完再释放。
    让同一个key的请求排队,但是吞吐量明显下降了。
  5. 双缓存。我们有两个缓存,缓存A和缓存B。缓存A的失效时间为20分钟,缓存B不设失效时间。自己做缓存预热操作。然后细分以下几个小点
    I 从缓存A读数据库,有则直接返回
    II A没有数据,直接从B读数据,直接返回,并且异步启动一个更新线程。
    III 更新线程同时更新缓存A和缓存B。

2>缓存穿透

缓存和数据库中都不存在数据,导致所有的请求都怼到数据库上,导致数据库崩了。一般是黑客攻击方式。

解决方案:

  1. 接口增加校验,从业务上直接拦截非法值。
  2. 数据库没有的数据存储为key-null,缓存有效期设置为30s。
  3. 采用布隆过滤器
    将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被布隆过滤器过滤掉。

3>缓存击穿

缓存不存在数据但数据库中有(一般是缓存过期),由于并发用户多数据库同时请求同一条数据导致数据库请求压力激增。

解决方案:

  1. 互斥锁
    缓存失效的时候,先去获得锁,得到锁了,再去请求数据库。没得到锁,则休眠一段时间重试
  2. 设置热点数据永不过期

7)缓存和数据库双写一致性问题

一致性问题是分布式常见问题,还可以再分为最终一致性和强一致性。
如果对数据有强一致性要求,不能放缓存。我们所做的一切,只能保证最终一致性。另外,我们所做的方案其实从根本上来说,只能说降低不一致发生的概率,无法完全避免。因此,有强一致性要求的数据,不能放缓存。

策略:先处理数据库再同步redis。

  1. 增:写库成功后,将返回的对象存入redis。
  2. 删:数据库删除成功后,删除redis。
  3. 查:查redis,没查到,去数据库查询。
  4. 改:数据库修改成功后,再修改。
  5. 另外,对于前端接收到的数据,应该进行数据校验,确保数据的合法性。

上述策略再多请求并发执行的情况下,依然会造成脏数据问题。新的策略如下:
6. 查:查缓存,没查到,去数据库查询。
7. 更新(增删改):先更新数据库,成功后再删除缓存。–《Cache-Aside pattern》
这种策略基本上避免了脏数据问题,因为可能存在删除缓存失败的问题,提供一个补偿措施即可,例如利用消息队列重试删除。《缓存的正确使用方式,你都会了吗?》中提供的方法如下图所示:
在这里插入图片描述
8. 因为可能存在删除缓存失败的问题,提供一个补偿措施即可,例如利用消息队列。

延迟双删策略:
9. 读取缓存时,如果缓存不存在则读取数据库
10. 更新之前,先del缓存
11. 更新数据库
12. 删除缓存

8)虚拟内存机制

redis是内存存储的,通过虚拟内存机制将部分不常用的数据存储到磁盘,从而节省内存。可以通过参数控制这个阈值。

9)大key问题

1>概念

  1. value比较大;
  2. 占内存比较多;
  3. 元素数量比较多。

2>big key识别

redis-cli -bigkeys

没有绝对的衡量标准。

  1. String类型的Value,超过10M;
  2. Set、List类型的Vlue,成员超过1w;
  3. Hash类型的Value,成员超过1k,但是Values体积超过1000M。

3>影响

  1. 影响性能
    读取慢。
  2. 占用内存
  3. 内存空间不均匀
    集群分片存储,big key所在节点内存消耗大;
  4. 搜索困难
  5. Redis备份、恢复、迁移困难
  6. 过期key删除耗时

4> 解决方案

  1. 删除;
  2. 设置缓存TTL;
  3. 拆key:
    如根据日期拆分;
    将大key分布在不同的服务器中。‘
  4. 迁移至单独的数据库中。

10)热key问题

1>概念

redis中同一个key被大量访问,就会导致流量过于集中,使得很多物理资源无法支撑,如网络带宽、物理存储空间、数据库链接等。

2>热key识别

  1. 通过JD框架的hotkey等工具识别热key,阈值需要结合业务和服务器性能不断的调优。
  2. redis 4.0.3提供了redis-cli的热key发现功能。
    执行redis-cli -hotkeys

3>解决方案

通过事前预测和事中解决:

  1. 热点key拆分;
    通过key的后缀将一个key拆成多个并分布到不同结点。
  2. 多级缓存;
    通过缓存的方式尽量减少系统交互,使得用户请求可以提前返回。==》缓存在浏览器、距离用户最近的CDN中、Redis、服务器本地缓存等多级缓存。
    提升用户体验的同时,减少系统压力。

CDN (内容分发网络) 指的是一组分布在各个地区的服务器。这些服务器存储着数据的副本,因此服务器可以根据哪些服务器与用户距离最近,来满足数据的请求。

  1. 热key备份;
  2. 限流

4,集群方案

0)安装及部署

1>安装

# wget http://download.redis.io/releases/redis-6.0.8.tar.gz
# tar -xzvf redis-6.0.8.tar.gz
# cd redis-6.0.8
# make   之后出现redis-server目录

2>启动

./redis-server ../redis.conf

3>文件说明

文件(夹)说明
redis-serverredis 服务程序
redis-cli客户端程序
redis.confredis配置
redis-benchmark
redis-check-aof
redis-check-rdb

redis.conf配置项说明(修改后需要重启):

配置项说明备注
port 6379监听端口
daemonize no默认不是以守护进程的方式运行。使用 yes 启用守护进程
pidfile /var/run/redis.pid当 Redis 以守护进程方式运行时,Redis 默认会把 pid 写入 /var/run/redis.pid 文件,可以通过 pidfile 指定
loglevel notice指定日志记录级别,Redis 总共支持四个级别:debug、verbose、notice、warning,默认为 notice
logfile /opt/redis.log日志记录方式,默认为标准输出,如果配置 Redis 为守护进程方式运行,而这里又配置为日志记录方式为标准输出,则日志将会发送给 /dev/null
databases 16设置数据库的数量,默认数据库为0,可以使用SELECT 命令在连接上指定数据库id
save <seconds> <changes>指定在多长时间内,有多少次更新操作,就将数据同步到数据文件,可以多个条件配合默认内容:
save 900 1
save 300 10
save 60 10000
rdbcompression yes指定存储至本地数据库时是否压缩数据,默认为 yes,Redis 采用 LZF 压缩,如果为了节省 CPU 时间,可以关闭该选项,但会导致数据库文件变的巨大
dbfilename dump.rdb指定本地数据库文件名
dir /opt/data/redis/本地数据库存放目录
requirepass foobared设置 Redis 连接密码,默认关闭。
masterauth 当 master 服务设置了密码保护时,slave 服务连接 master 的密码
slaveof 设置当本机为 slave 服务时,设置 master 服务的 IP 地址及端口,在 Redis 启动时,它会自动从 master 进行数据同步
maxclients 128设置同一时间最大客户端连接数,默认无限制,Redis 可以同时打开的客户端连接数为 Redis 进程可以打开的最大文件描述符数,如果设置 maxclients 0,表示不作限制。达到最大限制会返回:max number of clients reached
maxmemory 指定 Redis 最大内存限制。达到最大内存后,Redis 会先尝试清除已到期或即将到期的 Key;如果仍然到达最大内存设置,将无法再进行写入操作,但仍然可以进行读取操作。Redis 新的 vm 机制,会把 Key 存放内存,Value 会存放在 swap 区
appendonly no指定是否在每次更新操作后进行日志记录,Redis 在默认情况下是异步的把数据写入磁盘,如果不开启,可能会在断电时导致一段时间内的数据丢失。因为 redis 本身同步数据文件是按上面 save 条件来同步的,所以有的数据会在一段时间内只存在于内存中。默认为 no
appendfsync everysec指定更新日志条件,共有 3 个可选值:
no:表示等操作系统进行数据缓存同步到磁盘(快);
always:表示每次更新操作后手动调用 fsync() 将数据写到磁盘(慢,安全);
everysec:表示每秒同步一次(折中,默认值)

1)主从模式

增量copy:

  1. 复制offset
    主从都维护一个复制偏移量offset。
  2. 复制积压缓冲区
    master维护,固定长度、FIFO队列。如果复制offset超过这个长度,只能进行全量数据。
  3. 服务器运行id(runid)
    每个redis节点都有runid,每次请求:主节点runid保存在从结点。
    断线重连后,如果runid与当前不一致,说明发生了master的重新选举,需要全量复制数据。

2)哨兵模式(sentinel) ==》保证高可用,不保证零丢失。

基于主从模式进行升级。

哨兵本身支持高可用,也是一个分布式集群。
即使部分哨兵挂掉了,也可以正常工作。

主要功能有:

  1. 集群监控
    监控master、slave是否在正常工作。
  2. 消息通知
    如果某个redis实例故障了,通知给管理员。
  3. 故障转移
    故障时进行主从切换。
    故障转移时,需要大部分的哨兵都同意当前master宕机。==》分布式选举
  4. 配置中心
    如果主从切换了,通知所有的client客户端新的master地址。

3)Redis Cluster(推荐,服务端分片)

3.0版本开始使用,将slot(槽,16384个)哈希分布在集群的每个结点,请求发送到任意结点之后,该结点发起转向指令,将查询请求发送到正确的结点。
所有节点互为主从,互相备份。
同一分片,多个节点之间数据不保证强一致性。
扩容:需要将旧结点数据迁移一部分到新结点。哈希函数:对16384取模,平均分配到每个结点。

使用:
每个redis开发两个端口号,比如一个是6379,另一个就是16379(加1w);其中6379用于客户端访问,16379用来进行节点间通信(cluster bus,用二进制协议gossip),用来进行故障检测、故障转移、故障切换、配置更新。
优点:

  1. 无中心,支持动态扩容。
  2. 具备sentinel监控和自动Failover(故障转移)能力
  3. 客户端连接一个可用结点即可
  4. 高性能 直接连接redis即可,不需要连接代理。

缺点:

  1. 运维复杂,扩容需要人工干预(槽的导出与导入)。
  2. 只能使用0号数据库。(redis中默认有11个数据库)
  3. 不支持批量操作。(pipeline操作,批量操作可能导致高耗时)
  4. 分布式逻辑和存储模块耦合。

4)redis sharding(客户端分片)

Redis Cluster出来之前用的比较多,由客户端进行分片,服务端的每个结点互相独立。
缺点:

  1. 扩容非常复杂,不支持动态增删结点。
  2. 结点之间不能共享,资源浪费。

5,分布式锁

1)概念

分布式锁在分布式系统、集群里实现多个进程互斥且可见,实现单一结点单线程执行。

2)setNX(redis)

redis单线程,天然可做分布式锁。

//expire 设置超时时间
//value设置线程标识 是同一线程才允许del
set(key,value,expire)
//锁续期:执行时间太长,利用redisson进行续期,也可以每次自己续期。

SETNX命令:
向Redis中添加一个key,
key不存在时才添加并返回1,存在则不添加返回0。
这个命令是原子性的。
通过删除命令释放锁。

常见问题处理:

  1. 死锁问题
    一个线程长期持有锁导致其它线程都阻塞。==》设置超时时间

  2. 锁误删问题
    锁设置线程标识,是当前线程才允许删除锁。==》必须保证判断和删除的逻辑是原子的,不然还会误删。
    1)利用Lua脚本: 会将key存储在KEYS数组中,会将value存储在ARVG数组中。下标从1开始

if (redis.call('get',KEYS[1])==ARGV[1]) then
    return redis.call('DEL',KEYS[1])
end
    return 0

2)JAVA:通过execute()方法执行Lua本,DefaultRedisScript加载脚本

private static final DefaultRedisScript<Long> DEFAULT_REDIS_SCRIPT;
    static {
        DEFAULT_REDIS_SCRIPT=new DefaultRedisScript<Long>();
        DEFAULT_REDIS_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        DEFAULT_REDIS_SCRIPT.setResultType(Long.class);
    }
     public void unLock() {
        stringRedisTemplate.execute(DEFAULT_REDIS_SCRIPT, Collections.singletonList(RedisConstants.KEY_PREFIX + name),ID_PREFIX+ Thread.currentThread().getId());
    }
  1. 重入问题

重入问题
获得锁的线程可以再次进入到相同的锁的代码块中,可重入锁的意义在于防止死锁,比如HashTable这样的代码中,他的方法都是使用synchronized修饰的,假如他在一个方法内,调用另一个方法,那么此时如果是不可重入的,不就死锁了吗?所以可重入锁他的主要意义是防止死锁,我们的synchronized和Lock锁都是可重入的。

  1. 不可重试

不可重试
是指目前的分布式只能尝试一次,合理的情况是:当线程在获得锁失败后,应该能再次尝试获得锁。

  1. 主从一致性
    如果Redis提供了主从集群,当向集群写数据时,主机需要异步的将数据同步给从机,如果在同步过去之前,主机宕机了,就会出现死锁问题。
    使用redLock避免此类问题:redLock获取锁时从每个节点都获取锁,如果半数锁都获得成功才能获得锁。(主从切换可能会导致主节点数据丢失)。redisson中有redLock的实现。

6,对比Memcached

  1. 常见的缓存服务器;
  2. 只支持简单的k-v存储;
  3. 不支持持久化;
  4. 数据分片:手动分片。
    redis分片是哈希槽分片,支持数据的自动分片和负载均衡。
  5. 其它功能都少很多

7,Stream

redis5.0版本新加的数据结构,主要用于处理有序的、可追溯的消息流。

  1. 有序:每个消息由唯一id,按消息发布时间排序。
  2. 消费:Stream支持消息添加、读取、删除、订阅;支持消费组,可以让多个消费者并发地处理消息流。
    支持竞争式消费、共享式消费两种模式。
  3. 持久化:消息可以持久化、主备复制、记录每个客户端访问位置(客户端+偏移量)。
    5.0之前不支持。
  4. 场景:消息队列、日志收集、实时数据处理和聊天室应用等。

1)延迟消息

开启过期监听配置,监听key过期事件通知。==》key不一定失效就立刻删除,延迟可能超过预期。
可靠的方法:Redission。
RDelayedQueue:基于zset结构实现的延迟队列,允许指定延迟时长将元素放到目标队列中。

10,参考文献

runoob.com:redis教程
缓存的正确使用方式,你都会了吗?

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值