Redis
1 NoSql介绍
2007年10月30日,北京奥运会门票面向境内公众第二阶段预售正式启动。上午一开始,公众提交申请空前踊跃。上午9时至10时,官方票务网站的浏览量达到了800万次,票务呼叫中心热线从9时至10时的呼入量超过了380万人次。由于瞬间访问数量过大,技术系统应对不畅,造成很多申购者无法及时提交申请,为此北京奥组委票务中心对广大公众未能及时、便捷地实现奥运门票预订表示歉意。
奥运会门票预售系统开放第一天,上午9点正式开始售票到中午12点,3个小时内,票务网站被浏览次数达到2000万次。
12306:一个在奔溃中的网站。
原因:海量用户和高并发。
性能瓶颈:瓷片IO性能低下。
扩展瓶颈:数据关系复杂,扩展性差,不便于大规模集群。
解决思路:降低磁盘IO次数–内存存储;去除数据间关系,仅存储数据。
NoSQL:即 Not-Only SQL( 泛指非关系型的数据库),作为关系型数据库的补充。
Nosql 作用:应对基于海量用户和海量数据前提下的数据处理问题。 常见 Nosql 数据库: Redis 、 memcache 、HBase 、 MongoDB。
2 Redis简介
Redis (REmote DIctionary Server) 是用 C 语言开发的一个开源的高性能键值对(key-value)数据库
特征:
- 数据间没有必然的关联关系
- 内部采用单线程机制进行工作
- 高性能:
官方提供测试数据,50个并发执行100000 个请求,读的速度是110000 次/s,写的速度是81000次/s。 - 多数据类型支持:
(1)字符串类型 string
(2)列表类型 list
(3)散列类型 hash
(4)集合类型 set
(5)有序集合类型 sorted_set - 持久化支持:
可以进行数据灾难恢复
3 Redis下载安装及基本使用
- Redis下载解压
- Redis启动
执行redis-server.exe:
- 客户端连接
执行redis-cli.exe:
- 基本操作
信息添加:set key value
信息查询:get key,如果不存在,返回空(nil)
4 Redis数据类型
redis是一个Map,其中所有的数据都是采用key:value的形式存储。数据类型指的是存储的数据的类型,也就是value部分的类型,key部分永远都是字符串。String在Redis内部存储默认就是一个字符串,当遇到增减类操作incr、decr时会转成数值型进行操作。redis所有的操作都是原子性的,采用单线程处理所有任务,命令是一个一个执行的,因此无需考虑并发带来的影响。
4.1 String类型
通常存储字符串,如果字符串以整数的形式展示,可以作为数字操作使用。
- 添加/修改数据: set key value
- 获取数据: get key
- 删除数据: del key
- 添加/修改多个数据:mset key1 value1 key2 value2…
- 获取多个数据:mget key1 key2…
- 追加信息(存在追加,否则新建):append key value
- 数值数据增加1:incr key
- 数据数据增加指定的值:incrby key increment
- 数值数据增加指定的小数:incrbyfloat key increment
- 数值数据减少1:decr key
- 数值数据减少指定的值:decrby increment
- 按秒设置数据的时效性:setex key seconds value
- 按毫秒设置数据的时效性:psetex key milliseconds value
4.2 Hash类型
对一系列存储的数据进行编组,典型应用存储对象信息,一个存储空间保存多个键值对数据,Hash类型:底层使用哈希表结构实现数据存储。Hash结构优化:如果field数亮较少,存储结构优化为数组结构,如果field数量较多,存储结构优化为HashMap结构。
- 添加/修改数据:hset key field value
- 获取数据:hget key field
- 获取所有field:hgetall key
- 删除数据:hdel key field1 field2…
- 添加/修改多个数据:hmset key field1 value1 field2 value2…
- 获取多个数据:hmget key field1 field2
- 获取哈希表中字段的数量:hlen key
- 获取哈希表中是否存在指定的字段:hexists key field
- 获取哈希表中所有字段名或字段值:hkeys key/hvals key
- 设定指定字段的数值数据增加指定的值:hincrby key field increment / hincrbyfloat key float increment
注意事项:
(1)Hash类型下的value只能存储String,不允许存储其他数据类型,不存在嵌套现象,如果数据未获取到,对应的值为nil。
(2)每个hash可以存储2的32次方-1个键值对。
(3)hgetall操作可以获取全部属性,,如果内部field过多,遍历整体数据集效率会很低,有可能会成为数据访问的瓶颈。
4.3 List类型
保存多个数据,底层使用双向列表存储结构实现。
- 添加/修改数据:lpush/rpush key value1 value2…
- 获取数据:
lrange key startIndex stopIndex
lindex key index
llen key - 获取并移除数据:lpop/rpop key
- 规定时间内获取并移除数据(list阻塞数据获取):
blpop/brpop key1 [key2] timeout
brpoplpush source destination timeout - 移除指定数据:lrem key count value
从左删除count个value
List中的数据都是String类型,数据总量是优先的,最多2的32次方-1个元素。
List具有所有的概念,但是操作数据时通常以队列的形式进行入队和出队操作,或者以栈的形式进行入栈和出栈的操作。
获取全部数据操作结束索引为-1。
List可以对数据进行分页操作,通常第一页的信息来自原list,第2页及更多的信息通过数据加载。
4.4 Set类型
能够存储大量数据,在查询方面提供更高的效率,存储结构与Hash结构完全相同,仅存储键,不存储值(nil),并且键是不允许重复的。
- 添加数据:sadd key member1 membe2…
- 获取全部数据:smembers key
- 删除数据:srem key member1 member2…
- 获取集合数据总量:scard key
- 判断集合中是否包含指定数据:sismember key member
- 随机获取集合中指定数量的数据:srandmember key [count]
- 随机获取集合中指定数量的数据并将数据移出集合:spop key [count]
Set还可以进行交、并、差的操作,红色部分为交、并、差操作的结果集。
- 求两个集合的交、并、差
sinter key1 [key2]
sunion key1 [key2]
sdiff key1 [key2] - 求两个集合的交、并、差并存储到指定集合中
sinterstore destination key1 [key2]
sunionstore destionation key1 [key2]
sdiffstore destination key1 [key2] - 将指定数据从原始集合中移动到目标集合中
smove source destination member
4.5 sorted_set类型
可以保存排序的数据,根据自身的特征进行排序,在Set的基础上增加可排序字段。
- 添加数据:zadd key score1 member1 [score2 member2]
- 获取全部数据:
zrange key start stop [withscores]
zreverange key start stop [withscores] - 删除数据:zrem key member [member…]
- 按条件获取数据
zrangebyscore key min max [withscores] [limit offset count]
zreverangebyscore key min max - 条件删除数据
zremrange key start stop
zremrangebyscore key min max
注意:min和max用于限定排序的条件,start和stop用于限定索引,offset和count用于限定开始位置和查询数量 - 获取集合数据总量:scard key / scount key min max
同时也支持交并操作,这里不再赘述。
5 Redis通用指令
- 删除指定key:del key
- 判断key是否存在:exists key
- 获取key的类型:type key
- 为key设定有效期:
expire key sceonds
pexpire key milliseconds - 获取key有效时间:ttl/pttl key
- 按通配符查询key:keys pattern
- 为key改名:rename/renamenx key newkey
- 对所有key排序:sort
- 切换数据库:select index
redis为每个服务提供16个数据库,编号从0.-15,每个数据库之间的数据相互独立。 - 数据移动:move key db
- 数据清除(慎用):flushdb、flushall
6 Jedis
Java语言连接Redis服务
6.1 Jedis的hello jedis程序
(1)首先引入依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
(2)编写测试方法
@Test
public void testJedis(){
//1.连接redis
Jedis jedis = new Jedis("127.0.0.1", 6379);
//2.操作redis
jedis.set("begin","hello jedis");
String name = jedis.get("begin");
System.out.println(name);
//3.关闭连接
jedis.close();
}
(3)打印结果
6.2 Jedis测试List
(1)编写测试方法
@Test
public void testList(){
//1.连接redis
Jedis jedis = new Jedis("127.0.0.1", 6379);
//2.操作redis
jedis.lpush("nums","1","2","3");
jedis.rpush("nums","0");
List<String> list1 = jedis.lrange("nums", 0, -1);
for(String s : list1){
System.out.print(s+ " ");
}
System.out.println();
System.out.println(jedis.llen("nums"));
//3.关闭连接
jedis.close();
}
(2)打印结果
6.3 Jedis测试Hash
(1)编写测试方法
@Test
public void testHash(){
//1.连接redis
Jedis jedis = new Jedis("127.0.0.1", 6379);
//2.操作redis
jedis.hset("student","s1","梁朝伟");
jedis.hset("student","s2","刘德华");
jedis.hset("student","s3","张国荣");
Map<String, String> hash = jedis.hgetAll("student");
System.out.println(hash);
System.out.println(jedis.hlen("student"));
//3.关闭连接
jedis.close();
}
(2)打印结果
7 Redis持久化
什么是持久化?
持久化就是利用永久性存储介质对数据进行保存,可以用来进行数据恢复,持久化可以防止数据的丢失,确保数据安全性。
Redis两种持久化方案:
(1)RDB:数据(快照)
将当前数据状态进行保存,快照形式,存储数据结果,存储格式简单,关注点在数据。
(2)AOF::过程(日志)
将数据的操作过程进行保存,日志形式,存储操作过程,存储格式复杂,关注点在数据的操作过程。
7.1 RDB
命令执行:
执行命令者:用户(Redis操作者)
执行时间:及时(随时)
执行事件:保存数据
7.1.1 save方式
手动执行一次保存操作,注意:因为是单线程执行序列,save指令的执行会阻塞当前redis服务器,知道当前RDB过程完成为止,有可能会造成长时间阻塞,线上环境不建议使用。
7.1.2 bgsave方式
由客户端发起指令,Redis服务器控制指令在合理的时间保存数据,在后台执行。bgsave指令是对save阻塞问题做的优化,Redis内部所有涉及RDB操作都采用bgsave的方式,save可以直接放弃使用。
7.1.3 save second changes方式
满足限定时间内key的变换数量达到指定数量即进行持久化。second:监控时间范围;changes:监控key的变化量。在conf文件中进行配置。实际执行的还是bgsave操作。
7.1.4 save和bgsave异同
7.1.5 RDB优点
- RDB是一个紧凑压缩的二进制文件,存储效率高
- RDB内部存储的Rdis在某个时间点的快照,非常适用于数据备份,全量复制等场景
- RDB数据恢复的速度要比AOF快很多
- 应用:服务器中没X小时执行bgsave,并将RDB文件拷贝到远程服务器中,用于灾难恢复
7.1.6 RDB缺点
- RDB无论是执行指令还是利用配置,无法做到实时持久化,具有较大可能性会丢失数据
- bgsave每次运行都要执行fork操作创建子进程,要牺牲掉一些性能
- Redis的众多版本中未进行RDB文件格式的版本统一,有可能出现各版本服务之间的数据格式无法兼容现象。
7.2 AOF
AOF(append only file)持久化:以独立日志的方式记录每次写命令,重启时再重新执行AOF文件中命令达到恢复数据的目的,记录数据产生的过程。AOF的主要作用是解决了持久化的实时性,目前已经是Redis持久化的主流方式。
7.2.1 AOF写数据的三种策略
- always(每次)
每次写操作都同步到AOF文件中,数据零误差,性能较低,不建议使用。 - everysec(每秒)
每秒将缓冲区的指令同步到AOF文件中,,数据准确性较高,性能较高,建议使用,也是默认配置在系统突然宕机的情况下丢失1秒的数据。 - no(系统控制)
由系统控制每次同步到AOF文件的周期,整体过程不可控
7.2.2 AOF重写
重写是将对同一个数据的若干个命令结果转换为最终结果数据对应的指令进行记录。可以降低磁盘占用量,提高持久化效率和数据恢复效率。
7.2.3 RDB与AOF异同
8 Redis事务
Redis指令执行过程中,多条连续执行的指令被干扰、打断、插队,会导致信息不准确。Redis事务就是一个命令执行的队列,将一些列预定义命令包装成一个整体(一个队列),当执行时,一次性按照添加顺序依次执行,中间不会被打断或干扰,也就是一个队列中一次性、顺序性、排他性的执行一系列命令。
假如现在客户端1执行了set name ‘xiaoming’,接着客户端2执行了set name ‘xiaohua’,接着客户端1执行了get name则得到的是’xiaohua’,但是客户端1预期是要得到‘xiaoming’,这样就不准确了。
8.1 事务的基本操作
- 开启事务 :multi
设定事务的开启位置,此指令执行后,后续的所有指令均加入到事务中 - 执行事务:exec
设定事务的结束位置,同时执行事务。与multi成对出现,成对使用
注意:加入事务的命令暂时进入到任务队列中,并没有立即执行,只有执行exec命令才开始执行。 - 取消事务:discard
终止当前事务的定义,发生在multi之后exec之前。 - 事务的工作流程
8.2 锁
对key添加监视,如果在exec执行前监视的key发生了变化,终止事务执行
- 监视key:watch key1 key2…
- 取消对所有key的监视:unwatch
如上,客户端1监视user3,然后开启事务,创建队列,在exec前,客户端2改变了user3的值,客户端1exec取消执行。
8.3 分布式锁改良
虽然Redis是单线程的,但是多个客户端对同一数据进行操作时,使用watch已经不能解决问题,因为watch监视只有数据不被改变的情况下,事务才会执行,一旦改变事务失效,因此需要用分布式锁来解决。
- 对key进行加锁,value可以任意:setnx lock-key value
利用setnx的返回值特征,有值则犯规设置失败,无值则返回设置成功,失败的没有控制权,成功的有控制权,操作完毕释放锁,del lock-key - 死锁问题解决
假如对某个key加了锁,但是忘记释放锁,如果其他客户端也来对该key加锁,则会造成一直等到的情况,因此可以在上锁的时候设置一个超时时间,超过一定时间则自动释放锁
expire lock-key second
pexpire lock-key milliseconds
9 Redis删除策略
9.1 数据删除简介
- Redis中的数据特征
Redis是一种内存级数据库,所有数据均放在内存中,内存中的数据可以通过TTL指令来获取期状态
x :具有时效性的数据
-1 :永久有效的数据
-2 :已经过期的数据或被删除以及未定义的数据
过期的数据真的删了吗,这就是删除策略要回答的问题。 - 数据删除策略种类
(1)定时删除
(2)惰性删除
(3)定期删除
时效性数据的存储结构
数据删除策略的目标是在内存占用与CPU占用之间寻找一种平衡,顾此失彼都会造成整体Redis性能的下降,甚至引发服务器宕机或内存泄漏。
9.2 数据删除策略
- 定时删除(时间换空间)
创建一个定时器,当key设置有过期时间,由定时器任务立即执行对键的删除操作,
优点:解约内存,到时就删除,快速释放掉不必要的内存占用
缺点:CPU压力很大,会影响Redis服务器响应时间和指令吞吐量
总结:利用处理器性能获取存储空间(以时间换空间) - 惰性删除(空间换时间)
数据到达过期时间,不做处理,等待下次访问该数据时如果未过期放回数据,如果过期了删除返回不存在。在每次get时都会调用expireIfNeed()判断。
优点:解约CPU性能,发现必须删除的时候才删除
缺点:内存压力很大,会出现长期占用内存
总结:用存储空间换取处理器性能(以空间换时间) - 定期删除(折中)
周期性轮询Redis库中的时效性数据,采用随机抽取的策略,采用过期数据占比的方式控制删除频度。
特点:CPU性能占用设置有峰值,检测频度可自定义设置;内存压力不大,长期占用的冷数据会被持续清理
Redis启动服务器初始化时,读取配置文件server.hz的值,默认为10,每分钟执行server.hz次serverCron()–>databasesCron()–>activeExpireCycle()。activeExpireCycle()对每个expires[x]逐一进行检测,每次执行250ms/server.hz,对某个expires[x]检测时,随机挑选W个key检测:
(1)如果key超时,删除key
(2)如果一轮中删除的key的数量>w25%,循环该过程
(3)如果一轮中删除的key的数量<=w25%,检查下一个expires[x],0-15循环。
(4)w取值ACTIVE_CYCLE_PER_LOOP
(5)参数current_db记录activeExpireCycle()进入那个expires[x]执行
(6)如果activeExpireCycle()执行时间到,下次从current_db继续向下执行。
总结:周期性抽查存储空间(随机抽查,重点抽查)
9.3 删除策略对比
9.4 逐出算法
当新数据进入Redis时,如果内存不足怎么办?
Redis使用内存存储数据,在执行一个命令前,会调用freeMemoryIfNeeded()检测内存是否充足,如果内存不满足新加入数据的最低存储要求,Redis要临时删除一些数据为当前指令清理存储空间,清理数据的策略称为逐出算法。
逐出数据的过程不是100%能够清理出足够的可使用的空间,如果不成功则反复执行,当对所有数据尝试完毕后,如果不能达到内存清理的要求,将出现错误信息:
影响逐出算法的想关配置:
- 最大可使用内存:maxmemory
占用物理内存的比例,默认值为0,表示不限制,生产环境中根据需要设置,通常设置50%以上 - 每次选取待删除数据的个数:maxmemory-samples
选取数据时并不会全库扫描,导致严重的性能消耗,降低读写性能,因此采用随机获取数据的方式作为待检测删除数据 - 逐出策略:maxmemory-plicy
达到最大内存后,对被挑选出来的数据进行删除的策略
逐出策略有以下:
1、检测易失数据(可能会过期的数据server.db[i].expires)
(1)volatile-lru:挑选最近最少使用的数据淘汰
(2)volatile-lfu:挑选最近使用次数最少的数据淘汰
(3)volatile-ttl:挑选将要过期的数据淘汰
(4)volatile-random:任意选择数据淘汰
2、检测全库数据(所有数据server.db[i].dict)
(1)allkeys-lru:挑选最近最少使用的数据淘汰
(2)allkeys-lfu:挑选最近使用次数最少的的数据淘汰
(3)allkeys-random:随机淘汰
10 高级数据类型
10.1 Bitmaps
10.2 HyperLogLog
10.3 GEO
11 主从复制
互联网“三高”架构:高并发、高性能、高可用
11 主从复制工作流程
12 哨兵模式
12.1 哨兵简介
12.2 启用哨兵
12.2 哨兵工作原理
13 企业级解决方案
13.1 缓存预热
缓存预热就是系统启动前,提前将相关的缓存数据直接加载到缓存系统。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!
13.2 缓存雪崩
13.3 缓存击穿