Redis

131 阅读11分钟

Redis 是一个key-value的nosql数据库,先存到内容中,再根据一定的策略持久化到磁盘,断电也不会丢失数据 可用于缓存,事件发布或订阅,高速队列,分布式锁等场景。该数据库使用C语言编写,支持网络,提供字符串,哈希,列表,队列,集合结构直接存取,基于内存,可持久化。

Redis为什么这么快? (一)纯内存操作 (二)单线程操作,避免了频繁的上下文切换 (三)采用了非阻塞I/O多路复用机制

I/O多路复用: 1 传统的并发模型,每个I/O流都有一个新的线程管理。 2 I/O多路复用。只有单个线程,通过跟踪每个I/O流的状态,来管理多个I/O流。(很多个网络I/O复用一个或少量的线程来处理这些连接) redis提供了select、epoll、evport、kqueue等多路复用函数库。

在这里插入图片描述
在这里插入图片描述
阻塞式I/O和I/O复用,两个阶段都阻塞,那区别在哪里呢?就在于第三节讲述的Selector,虽然第一阶段都是阻塞,但是阻塞式I/O如果要接收更多的连接,就必须创建更多的线程。I/O复用模式下在第一个阶段大量的连接统统都可以过来直接注册到Selector复用器上面,同时只要单个或者少量的线程来循环处理这些连接事件就可以了,一旦达到“就绪”的条件,就可以立即执行真正的I/O操作。这就是I/O复用与传统的阻塞式I/O最大的不同。也正是I/O复用的精髓所在

可以使用Redis做分布式锁: 分布式锁:当多个进程不在同一个系统中,用分布式锁控制多个进程对资源的访问。 有这样一个情境,线程A和线程B都共享某个变量X。 如果是单机情况下(单JVM),线程之间共享内存,只要使用线程锁就可以解决并发问题。 如果是分布式情况下(多JVM),线程A和线程B很可能不是在同一JVM中,这样线程锁就无法起到作用了,这时候就要用到分布式锁来解决 使用分布式锁流程如下: 1 获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。 2 获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。 3 释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。

更改数据库数据的时候,要是想要更新缓存。可以使用实时同步(直接加一个切点或者调用更新方法)或者分批同步(按时间戳刷新数据)

6.3 为什么要用 redis 而不用 map/guava 做缓存? 缓存分为本地缓存和分布式缓存。以 Java 为例,使用自带的 map 或者 guava 实现的是本地缓存,主要的特点是轻量以及快速,生命周期随着 jvm 的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。 使用 redis 或 memcached 之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持 redis 或 memcached服务的高可用,整个程序架构上较为复杂。

定期删除+惰性删除。 定期删除:redis默认是每隔 100ms 就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删 除。注意这里是随机抽取的。为什么要随机呢?你想一想假如 redis 存了几十万个 key ,每隔100ms就遍历所 有的设置过期时间的 key 的话,就会给 CPU 带来很大的负载! 惰性删除 :定期删除可能会导致很多过期 key 到了时间并没有被删除掉。所以就有了惰性删除。假如你的过期 key,靠定期删除没有被删除掉,还停留在内存里,除非你的系统去查一下那个 key,才会被redis给删除掉。这 就是所谓的惰性删除,也是够懒的哈!

支持事务:ACID 按顺序地串行化执行而不会被其他命令插入,不许加塞。 开启事务MULTI,之后的操作都会放到队列中。 事务提交EXEC 放弃事务DISCARD 全体连坐:意思是如果在事务操作中有一个命令还未提交就已经报错,那么提交之后,全部都是错。 冤头债主:和上面的稍有区别,它是在事务中不报错,但是提交之后会有一条命令报错,则其他事务提交成功。 watch监控(乐观锁)WATCH KEY,事物期间有人更改,事务提交失败。一旦执行完事务,监控会被释放。

Redis2.2版本之后还支持CAS

消息队列(发布/订阅) 可以一次性订阅多个消息,SUBSCRIBE 队列1 队列2 队列3 订阅多个 :PSUBSCRIBE 主题名:如“new* 这样发送new1 new 4 new 13都可以收到消息” 发送消息:PUBLISH 频道 “消息”

主从复制 一主二从: 1.info replication 查看主从信息 2.配从不配主,当机器平等时,配置从服务器(即连接master服务器):Slaveof 新主库IP 新主库端口 3.从机只要SLAVEOF 主机有的全部复制 4.读写分离,只有主机可以写,从机只能读。 5.主机down掉,从机全部待命 6.主机回来,从机自动连接。 7.从机down,需要重连master,除非配置文件有相关设置。 薪火相传:(延时) 1.上一个Slave可以是下一个Slave的Master,Slave同样可以接收其他Slaves的连接和同步请求,那么该Slave作为了链条中下一个Slave的master。可以有效减轻master的写压力 2.中途变更转向:会清除之前的数据,重新建立拷贝最新的。 3.Slaveof 新主库IP 新主库端口 反客为主: 1.Slaveof no one 源主机恢复后,无slave。 哨兵模式: 1.反客为主的自动模式 2.需要配置 新建文档sentinel.conf 3.如果源主机恢复,逆转,变成slave

缓存会出现的问题: 1/缓存和数据库双写一致性的问题 (一)如果对数据有强一致性要求,不能放缓存。 (二)先更新数据库,再删缓存,因为可能存在删除缓存失败的问题,提供一个补偿措施即可,例如利用消息队列。 2/缓存雪崩:redis中很多键突然失效,造成访问都去数据库查询,造成数据库查询压力大。 (一)给缓存的失效时间,加上一个随机值,避免集体失效。 (二)使用互斥锁,但是该方案吞吐量明显下降了。 (三)双缓存。我们有两个缓存,缓存A和缓存B。缓存A的失效时间为20分钟,缓存B不设失效时间。自己做缓存预热操作。然后细分以下几个小点 I 从缓存A读数据库,有则直接返回 II A没有数据,直接从B读数据,直接返回,并且异步启动一个更新线程。 III 更新线程同时更新缓存A和缓存B。 3/缓存穿透:同一时间查询的key是缓存中没有的,造成数据库压力大。 (一)利用互斥锁,缓存失效的时候,先去获得锁,得到锁了,再去请求数据库。没得到锁,则休眠一段时间重试 (二)采用异步更新策略,无论key是否取到值,都直接返回。value值中维护一个缓存失效时间,缓存如果过期,异步起一个线程去读数据库,更新缓存。需要做缓存预热(项目启动前,先加载缓存)操作。 (三)提供一个能迅速判断请求是否有效的拦截机制,比如,利用布隆过滤器,内部维护一系列合法有效的key。迅速判断出,请求所携带的Key是否合法有效。如果不合法,则直接返回。 4/缓存的并发竞争问题 如果是单机版可以考虑使用事务操作。 集群版: (1)如果对这个key操作,不要求顺序 这种情况下,准备一个分布式锁,大家去抢锁,抢到锁就做set操作即可,比较简单。 (2)如果对这个key操作,要求顺序 假设有一个key1,系统A需要将key1设置为valueA,系统B需要将key1设置为valueB,系统C需要将key1设置为valueC. 期望按照key1的value值按照 valueA–>valueB–>valueC的顺序变化。这种时候我们在数据写入数据库的时候,需要保存一个时间戳。假设时间戳如下:系统A key 1 {valueA 3:00} 系统B key 1 {valueB 3:05}系统C key 1 {valueC 3:10} 假设这会系统B先抢到锁,将key1设置为{valueB 3:05}。接下来系统A抢到锁,发现自己的valueA的时间戳早于缓存中的时间戳,那就不做set操作了。以此类推。 (3)也可使用队列

Redis的数据结构:(内存数据库) K-V数据库---非关系型数据库 只能通过“键”来查询“值”,存储结构简单,所以读写效率高

Jedis

Jedis jedis = new Jedis("ip",端口);
jedis.set("key","value");
jedis.get("k1");
Set<String> sets = jedis.keys("*");
//事务
Transaction transaction = jedis.multi();
transaction.exec();
//transaction.discard();//回滚
//监控
jedis.watch("key");
jedis.unwatch("key");
//......
//主从复制(注意get set可能会程序速度太快,在set完之后get可能为空)
Jedis jedis_S = new Jeds("","");
jedis_S.slaveof("主机ip","主机端口");

这里说一下为啥数据量少的时候用压缩列表? 压缩列表像是数组的升级版,只是占用的内存会更少,但因为数据需要连续的空间,所以当数据量大的时候,就需要使用链表了。

./redis.server redis.conf ./redis.cli

String:字符 set key1 val1 get key1 incr key1 decr key1 keys *

Hash:字典(压缩列表,散列表) 压缩列表:(节省内存空间,即它存储的数据大小不一致) 当存储的数据量比较小的时候,列表就可以采用压缩列表的方式实现。 具体需要同时满足下面两个条件: 1.列表中保存的单个数据(有可能是字符串类型的)小于 64字节; 2.列表中数据个数少于 512 个。 散列表: Redis使用MurmurHash2这种运行速度快,随机性好的哈希算法作为哈希函数。 支持动态扩容,缩容。当装载因子大于1的时候,将散列表扩大为原来大小的2倍左右。当装载因子小于0.1时,缩小为字典数据个数的2倍左右。

hset hash1 field1 1 hset hash1 field1 2 hget hash1 field1 hkyes hash1 显示hash1的所有key hvals hash1 显示hval的所有val hgetall hash1 得到所K-V hdel hash filed1

List:双向循环链表,压缩列表 压缩列表:(节省内存空间,即它存储的数据大小不一致) 当列表中存储的数据量比较小的时候,列表就可以采用压缩列表的方式实现。 具体需要同时满足下面两个条件: 1.列表中保存的单个数据(有可能是字符串类型的)小于 64字节; 2.列表中数据个数少于 512 个。 lpush list1 1 2 3 4 5 rpush list1 a b c d e lrange list1 0 -1 遍历查看 lpop list1/rpop list1 左取和右取 取出来之后就没了

Set:无序不重复集合(有序数组,散列表) 同时满足下面这样两个条件的时候,Redis 就采用有序数组,来实现集合这种数据类型。 1.存储的数据都是整数; 2.存储的数据元素个数不超过512个。 sadd set1 a b a b c d 重复会覆盖 srem set1 a 移除元素啊 smembers set1 显示所有元素 sadd seta a b c d e sadd setb c d e f g sdiff seta setb 差集 sdiff setb seta 差集 sinter seta setb 并集 sunion seta setb 交集

Sorted set:有序集合(压缩列表,跳表) 用来存储一组数据,并且每个数据会附带一个得分。通过得分的大小,我们将数据组织成跳表这样的数据结构,以支持快速地按照得分值、得分区间获取数据。 使用压缩列表来实现有序集合的前提,需满足两个条件: 1.所有数据的大小都要小于 64 字节; 2.元素个数要小于 128 个。

zadd zset1 1 a 3 b 2 c 5 d 分数排序(升序) 结果应该是 a c b d
zrange zset1 0 -1 升序显示所有元素 zrevrange zset1 0 -1 降序显示所有元素 zrevrange/zrange zset1 0 -1 withscores 分数也显示

定时: expire key1 100 ttl key1

redis集群配置 ./redis-trib.rb create --replicas 1 192.168.8.128:7001 192.168.8.128:7002 192.168.8.128:7003 192.168.8.128:7004 192.168.8.128:7005 192.168.8.128:7006

redis连接任一个服务器 redis01/redis-cli -p 7004 -c