自拟!Redis死知识!!!

乐云一
  • 笔记
  • note
About 3849 wordsAbout 13 min

自拟!Redis死知识!!!

Redis基础

使用概念

redis是基于内存存储的数据库,读写速度十分之快,所以多用于缓存方向。

但在分布式业务中,也可用于分布式锁,或作为消息队列存储,进行消耗和接受。

此外,redis可存储很多中数据类型,又因为key - value Hash对、set、zset、list,存储String的原因,使其数据结构很丰富。 emo

指令

包括set、get、exists、decrease、setex等等

127.0.0.1:6379> set key value #设置 key-value 类型的值
OK
127.0.0.1:6379> get key # 根据 key 获得对应的 value
"value"
127.0.0.1:6379> exists key  # 判断某个 key 是否存在
(integer) 1
127.0.0.1:6379> strlen key # 返回 key 所储存的字符串值的长度。
(integer) 5
127.0.0.1:6379> del key # 删除某个 key 对应的值

/////////////////////批量
127.0.0.1:6379> mset key1 value1 key2 value2 # 批量设置 key-value 类型的值
OK
127.0.0.1:6379> mget key1 key2 # 批量获取多个 key 对应的 value


/////////////////////计数
127.0.0.1:6379> set number 1
OK
127.0.0.1:6379> incr number # 将 key 中储存的数字值增一
(integer) 2
127.0.0.1:6379> get number
"2"
127.0.0.1:6379> decr number # 将 key 中储存的数字值减一
(integer) 1
127.0.0.1:6379> get number
"1"

/////////////////////设置过期
expire key 60 
setex key 60 value

////////////////////list操作
rpush list value # 添加value
lpop list   # 左边弹出
rpop list   # 右边弹出
lrange list 0 1  # 查看下标 0 1 元素


//////////////////// map 操作
hmset key  value # 设置
hexists key # 判断
hgetall key # 获取key下所有value
hget key # 获取

////////////////////set 操作
sadd set key value # 设置
smembers set key   #查看
scard set key      # 查看key长度
sismember set key  # 检查存在
 
 ///////////////// sorted set操作
 zadd ...
 zcard...
 
 ///////////////// bitmap
 setbit mykey 7 1 # 设置7位位置  1
 setbit mykey 7 0  # 设置7为位置 0
 bitcount mykey  #  统计mekey 7位置 1的数目

单线程模型

Redis基于Reactor[反应堆模式],由文件事件处理器单线程运行。

通过IO多路复用[即时监听通知io请求的线程,监听多个套字节,不需要额外创建多余线程监听]程序监听大量客户端请求。

其文件事件处理器包含四部分监听请求:

1、创建多个socket

2、IO多路复用程序[接受多个套字节]

3、事件分派器

4、事件处理器

不使用多线程的原因

redis作为缓存组件,使用多线程容易造成死锁,上下文不兼容问题,极大影响效率。

单线程更加容易维护。

即使使用多线程也不会很明显的加大redis的性能,redis的性能靠机械的网络和内存支持。

6.0之后可以支持多线程,但是只针对大键的删除,而且这些删除命令在通过多线程向主线程请求后,依然会走单线程的删除操作。

删除策略

1、惰性删除,懒删除模式,只有当有操作指令到key时,才判断key是否过期,过期则删除

2、定时删除,每隔一段时间,抽取一批key进行判断删除。

1对cpu好,2对内存好,redis使用的1+2的结合方式。

内存淘汰机制

当key大量推荐,即将导致内存溢出,Out Of memory时,触发淘汰机制

1、设置过期时间,最少使用 的,淘汰

2、设置过期时间,将要过期的,淘汰

3、设置过期时间,随机挑选,淘汰

4、最少使用的,淘汰

5、随机挑选,淘汰

6、不淘汰,抛出内存溢出异常。

持久化机制

将数据写入硬盘中,即使redis挂掉,再次重启时也能恢复数据

快照持久化

快照,快速拍张照的意思。

当内存达到快照触发值时,Redis创建快照,获取存储在内存中的数据在某个时间点的副本,对副本进行备份。

默认方式,可在Redis.conf配置文件中,修改时间范围,内存达到多少时进行快照操作。

AOF持久化

目前主流的持久化方案,需要appendonly yes 手动开启。

当执行redis指令时,会将该命令写入内存缓存中,然后根据配置决定同步到硬盘的时间

在 Redis 的配置文件中存在三种不同的 AOF 持久化方式,它们分别是:

appendfsync always    #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
appendfsync everysec  #每秒钟同步一次,显式地将多个写命令同步到硬盘
appendfsync no        #让操作系统决定何时进行同步

建议使用第二种,每秒同步,不会让性能感到不适,而且即使系统崩溃,也能获取到最后一秒的备份数据。

Redis事务

和数据库事务特性'ACID'不同

偶遇redis事务基于 multi [开始] Exec[执行] Discard[禁止执行] 控制,所以相当于预输入指令,然后统一执行的概念。

所以无法保证其事务的原子性,因为没有回滚操作。

总之redis事务是: Redis 事务提供了一种将多个命令请求打包的功能。然后,再按顺序执行打包的所有命令,并且不会被中途打断。

Redis作消息队列

Redis5.0之后新增数据结构 Steam

使用

XADD key * user english msg Hello
创建一条key  id为*[redis自动定义] user = english msg = hello 的队列
XREAD streams key 0
//读取key的第0条开始的所有消息[全部]
XREAD block 1000 streams key $
//阻塞队列读取模式 当有key 的消息时立马读取,否则阻塞

缓存穿透

缓存被穿透,大量请求到key上,但是这个key不存在,所有请求都穿透了缓存直接去请求数据库DB。

解决方法:

1、进行数据校验,让数据不能操作出不符合key设置范围的值

2、缓存一个无效的key,设置过期时间。如果出现无效key出现,则设置一个缓存值并设置过期时间。但是弊端就是如果大量请求,每次请求的key都不同,但都是无效key。则会消耗大量的内存。

3、布隆过滤器,特殊结构。将所有可能存在的请求值放在过滤器中,判断用户给的key是否存在。不存在,则直接返回参数错误信息。存在的话,再走缓存 - 数据库流程。

但是布隆过滤器可能存在误判情况,但是只要他返回错误信息,则说明这个key一定参数非法。

缓存雪崩

缓存大面积失效,像雪崩一下崩塌,导致redis无用,请求全都打在数据库DB上。多出现在并发量突然特别大的时候,缓存失效的一瞬间,并发起来吓死数据库,比如秒杀。

解决方法:

1、redis集群

2、减少并发,分流。

3、设置缓存永不失效

Redis读写策略

Cache Aside Pattern旁路缓存模式

平时使用的模式,向更新数据库,然后删除缓存。

读:有存储就读缓存,没有就读DB、顺便设置缓存。

缺点:在写操作很频繁的业务中,会频繁删除缓存,减低命中率.

优化:在更新DB后,删除缓存,设置一个时间较短的缓存。

Read/Write Through Pattern 读写穿透

很少见,将redis当前主要存储源,由redis完成对DB数据库的更新、删除、新增操作

Write Behind Pattern 异步缓存写入

同上

Redis与数据库

数据库与redis缓存一致性问题

  1. 更新缓存,更新数据库。

    没用,两者之一有一环失败都会出现问题

  2. 更新数据库,更新缓存

    同上,也无法保证数据的绝对一致性。

    并且在并发的环境下,以上两种方案更是会存在丢失修改的情况发生。

  3. 删除缓存,更新数据库

    不行,在读-写服务一起操作同一数据库时,会出现:

    服务A删除缓存,服务B读缓存发现没有,去读数据库设置缓存值2,服务A更新数据库3 。此时数据库值为3,缓存为2 不一致。

  4. 更新数据库,删除缓存

    不行,在读写中:

    服务A读缓存,不存在读数据库值为1。 服务B写数据库,更新数据库值为2,删除缓存。 服务A写入缓存1. 此时数据库为2,缓存未1 。 但是理论上本例很难发生,除非更新数据库+删除缓存的服务B,比读数据库+写入缓存的服务A ,快很多很多。并且在并发上发生。而且因为数据库一般有加锁操作,写操作比读操作会慢很多。

    这么看来【更新数据库,删除缓存】的方案,是可以在很大程度上保证数据一致性的。

  5. 消息队列

    解决第一方案和第二方案,保证第二步成功

    引入消息队列,完美的解决了,更新缓存失败的问题。

    消息队列,在一致性问题上有几个特性。

    一,保证消息存在,即使系统崩溃,消息成功消费前都不会丢失

    二,保证消息成功被接受,如果接受失败则会一直等待消费者继续请求。

    所以在数据库操作后,将缓存操作放入消息中发送队列。可以保证缓存

  6. 订阅数据库日志

    在操作数据库后,不进行缓存操作。只在数据库变更日志BinLog中进行本次数据库操作的登记。

    而程序内则订阅这个日志,当日志更新时,取里面的内容解析进行缓存的更新。

  7. 延迟双删

    顾名思义,在数据库更新完数据库后,删除缓存,休眠一段时间,然后再次删除缓存。

    这样做的目的是,防止在数据库更新完缓存后2,删除缓存后;有服务B进入读数据库,因为主从库同步的问题,且都是写操作,所以服务B很快的查询到了从库的旧值1,并且设置缓存。因为读操作很快,所以只需要休眠大于【主从复制】的时间和B线程在读到旧值写入缓存时间的时间,再一次删除缓存就行。

总结: 不管哪种方案,都无法做到数据库与缓存的强一致性的问题。因为在不牺牲性能的前提下,都很难去控制和判断DB操作和写缓存的时间。

如果消耗性能,则可以通过很多方面达到。比如设置分布式锁、读-写锁、甚至排他锁等等

分布式锁

分布式锁使用原因:

当一个服务被拆分成多个微服务,A\B\C\D时。

由于本地锁是加在本地单个服务,所以在分布式下,A服务修改数据库CODE=10,B服务不受本地锁影响,修改数据库CODE=9。

那么数据库CODE则在9和10之间飘忽不定,与期望结果不一致。

这时候就需要使用分布式锁,在各微服务之间共享同一锁场景。

Redisson

使用方法

redisson的使用都是通过RedissonClient对象进行具体操作,

所以在项目初始化时,需要注入一个装配好了的RedissonClient到ioc容器中。

    @Bean(destroyMethod="shutdown") // 服务停止后调用 shutdown 方法。
    public RedissonClient redisson() throws IOException {
        // 1.创建配置
        Config config = new Config();
        // 集群模式
        // config.useClusterServers().addNodeAddress("127.0.0.1:7004", "127.0.0.1:7001");
        // 2.根据 Config 创建出 RedissonClient 示例。
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        return Redisson.create(config);
    }
   

使用场景

创建可重入锁Lock

可重入锁,和ReentrantLock用法一样;属于用锁的线程阻塞其他线程,其他线程等待锁释放。

当拿到lock的线程A服务挂掉时,由看门狗机制释放锁。

看门狗: 当拿锁的线程挂掉了,默认设置中,是启动一个定时任务,每隔十秒重新给锁设置过期时间,过期时间为30秒。而因为锁的有效期为30秒,所以30秒后将自动解锁。

创建可重入读写锁RReadWriteLock

读写锁,指:读锁为共享锁,写锁为排他锁,所以读-读操作不受阻塞,其余和写相关操作均被阻塞。

RReadWriteLock rwlock = redisson.getReadWriteLock("anyRWLock");
// 最常见的使用方法
rwlock.readLock().lock(); //读锁
// 或 
rwlock.writeLock().lock(); //写锁
​```

信号量RSemaphore

信号量,停车位。

配合redis使用步骤:

定义一个停车场缓存,key:park value:3

指有3个车位,有三个线程能进入。

然后使用 park.release() 释放, **park.acquire()**站位

Redis分布式操作

使用方法

使用SETNX:

相当于,请求进入A服务,使用SETNX设置KEY.设置成功,进入业务,直到业务成功删除KEY。

请求进入B服务,一样使用SETNX设置KEY,设置失败,重复设置直到设置成功,进入业务。

缺点

服务A占据锁的时候,服务挂掉,就直接造成死锁了。

解决方案也就是,设置key的过期时间,但是这个时间根据业务来说非常难确定。

优化方案

既然不好确定过期时间,则可以在服务A获取到锁后,再进行对锁设置过期时间。

那么这个时间就可以完全根据业务需求进行判断。

但是因为两者都涉及到了设置过期时间问题,那么就有一个很大的漏洞。

当获取锁 与 设置过期时间之间发生异常时。

锁就无法正确的设置过期时间,那么一样会造成死锁问题。

那么要保证锁正确的设置过期时间,也只要将锁的获取与锁的过期时间设置,绑定为原子操作即可。

最优解决

以上不管那种形式,都是设置过期时间。

会发现都绕不开一个问题,这个过期时间与实际业务的时间长度无法衡量判断。

会造成A服务业务还没做完,锁就过期了,被服务B拿到锁,在服务B业务进行时,A就把这个锁强制删除了。

所以为了避免这种问题,则需要在每一把锁上顶上一个唯一记号。

A服务只能删除A服务的锁。

但是即使这样,也无法保证一个问题:

在A服务获取A服务锁值期间,B服务获取锁的值是否一定和A服务锁不同?

最最优解决

既然发现A服务查询A锁,和删除A锁不是一个原子操作。

那么只需要使用redis的脚本即可:

String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);

Last update:
Contributors: leyunone
Comments
  • Latest
  • Oldest
  • Hottest
Powered by Waline v2.14.7