Redis(二)

212 阅读40分钟

Redis

一、数据库简介

1.技术发展

可以将技术分类。

  • 解决功能性的问题:Java、Jsp、RDBMS、Tomcat、HTML、Linux、JDBC、SVN
  • 解决扩展性的问题:Struts、Spring、SpringMVC、Hibernate、MyBatis
  • 解决性能问题:NoSQL、Java线程、Hadoop、Nginx、MQ、ElasticSearch

2.解决CPU以及内存压力

思考:应该将Session存在哪里?

方案1:存在cookie里

  1. 不安全
  2. 网络负担效率低

方案2:存在文件服务器或者数据库里

  1. 存在大量的IO效率问题

方案3:session复制

  1. session数据冗余
  2. 节点越多浪费越大

方案4:缓存数据库

  1. 完全在内存里,速度快
  2. 数据结构简单

3.解决IO压力

目前一部分数据库打破了传统关系型数据库以业务逻辑为依据的存储模式,而针对不同的数据结构类型改为以性能最为优先的存储方式。

缓存数据库:减少io的读操作

文档数据库

列式数据库

二、NoSQL数据库

1.概述

​ NoSQL意为不仅仅是SQL,泛指非关系型的数据库。

​ NoSQL不依赖业务逻辑方式存储,而是以简单的key-value模式存储。因此大大增加了数据库的扩展能力。

特点

  • 不遵循SQL标准
  • 不支持ACID
  • 远超于SQL的性能。

2.NoSQL的适用场景

  • 对数据高并发的读写
  • 海量数据的读写
  • 对数据高可扩展性的
  • 用不着sql和用了sql 也不行的情况,可以考虑使用Nosql

3.NoSQL的不适用场景

  • 需要十五支持
  • 基于sql的结构化查询,处理复杂的关系,需要即席查询

4.NoSQL数据库(多作为缓存数据库)

  1. Memcache,很早出现的NoSQL数据库,数据都在内存中,一般不持久化,作为缓存数据库辅助持久化的数据库
  2. Redis,目前比较流行的数据库,几乎覆盖了Memcached的绝大部分功能,数据都在内存中,支持持久化,主要用作备份恢复。除了简单的key-value模式,还支持多种数据结构的存储,比如string,list,set,hash,zset等。
  3. Cassandra,是免费开源的NoSQL数据库,它的设计目的在于管理由大量商用服务器构建起来的庞大集群上的海量数据集(数据量通常达到PB级别)。最大的优势是对写入和读取的操作进行规模调整,而且强调主机群的设计思路,能够以相对只管的方式简化个集群的创建和扩展流程。

5.文档型数据库

  1. MongoDB,高性能、开源。模式自由的文档行数据库。数据都在内存中,如果内存不足,把不常用的存到硬盘。虽然是key-value模式,但是对value(尤其是json)提供了丰富的查询功能。支持二进制数据以及大型对象。可以根据数据的特点代替RDBMS,成为独立的数据库。或者配合RDBMS,存储特定的数据库。

6.行式存储数据库

按行存,如mysql

7.列式数据库

比如统计年龄的平均值比较快,但是要查某人的全部信息就比较慢。

8.Hbase

​ HBase是Hadoop项目中的数据库。它用于需要对大量的数据进行随机、实时的读写操作的场景中。

​ HBase的目标就是处理数据量非常庞大的表,可以用普通的计算机处理超过十亿行数据,还可以处理有数百万列元素的数据表。

9.图关系型数据库

如Neo4j

主要应用方向:社会关系,公共交通网络,地图以及网络拓扑(n*(n-1)/2)

三、Redis概述

  1. Redis是一个开源的key-value存储系统
  2. 和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set——有序集合)和hash(哈希类型)
  3. 这些数据类型都支持push/pop、add/remove以及取交集并集差集以及更丰富的操作,而且这些操作都是原子性的。
  4. 在这个基础上,Redis支持各种不同方式的排序
  5. 和memcached一样,为了保证效率,数据都是缓存在内存中的。
  6. 区别是Redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件。
  7. 并且在此基础上实现了master-slave(主从)同步。

1.应用场景

①配合关系型数据库做高速缓存

  • 高频次,热门访问的数据,降低数据库IO
  • 分布式架构,做session共享

②多样的数据结构存储持久化数据

场景方法
最新的n个数据通过List实现按自然时间排序的数据
排行榜,TOP N利用zset
计数器,秒杀原子性,自增方法INCR、DECR
去除大量数据中的重复数据利用set
构建队列利用list
发布订阅消息系统pub/sub模式
时效性的数据,比如手机验证码Expired过期

四、安装注意点

先下载最新的gcc编译器【c语言编译环境】,然后下载redis安装包,解压安装。

后台启动

进入redis.conf文件【最好是从解压文件中拷贝一份在修改】

设置daemonize no改成yes

启动命令:redis-server 【redis.conf文件路径】

用客户端访问

命令:redis-cli

关闭

redis-cli shutdown

关闭指定端口: redis-cli -p 6379 shutdown

五、Redis相关知识

​ Redis是单线程+多路io复用技术。

**多路复用:**是指使用一个线程来检查多个文件描述符(Socket)的就绪状态,比如select和poll函数,传入多个文件描述符,如果有一个文件描述符就绪,就返回,否则阻塞直至超时。得到就绪状态后进行真正的操作可以在同一个线程里执行,也可以启动线程执行(比如使用线程池)

串行 vs 多线程+锁(memcached)vs 单线程+多路IO复用(Redis)

与Memcache有三点不同:

  • 支持多数据类型
  • 支持持久化
  • 单线程+多路IO复用

六、常用五大数据类型

​ redis是k-v数据库,因此,使用五种数据类型都是以key的操作为基础的。

关于key的操作:

  • keys * ——查看当前库所有key(匹配 key *1)
  • exist key ——判断某个key是否存在
  • type key ——查看你的key是什么类型
  • del key ——删除指定的key数据
  • unlink key ——根据value选择非阻塞删除
  • expire key 10 ——给key设置过期时间(单位:秒)
  • ttl key ——查看还有多少秒过期,-1表示永不过期,-2表示已经过期
  • select ——切换数据库
  • dbsize ——查看数据库的key的数量
  • flushdb ——清空当前库
  • flushall ——通杀全部库

非阻塞删除:

仅将keys从keyspace元数据中删除,真正的删除会在后续异步操作。

1.字符串(string)

​ string是redis最基本的类型,可以理解为与Memcached一模一样的类型,一个key对应一个value。

​ String类型是二进制安全的。意味着redis的string可以包含任何数据。比如jpg图片或者序列化的对象。

​ string类型是redis最基本的数据类型, 一个redis中字符串value最多可以是512M

常用命令

get  <key> 查询对应键值

append <key><value> 将给定的<value> 追加到原值的末尾

strlen <key> 获得值的长度

setnx <key><value> 只有在 key 不存在时  设置 key 的值

 

incr <key>

将 key 中储存的数字值增1

只能对数字值操作,如果为空,新增值为1



decr <key>

将 key 中储存的数字值减1
只能对数字值操作,如果为空,新增值为-1



incrby / decrby <key><步长>将 key 中储存的数字值增减。自定义步长。


mset  <key1><value1><key2><value2>  ..... 
同时设置一个或多个 key-value对  


mget  <key1><key2><key3> .....
同时获取一个或多个 value  


msetnx <key1><value1><key2><value2>  ..... 
同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在。
原子性,有一个失败则都失败


getrange  <key><起始位置><结束位置>
获得值的范围,类似java中的substring,前包,后包


setrange  <key><起始位置><value><value>  覆写<key>所储存的字符串值,从<起始位置>开始(索引从0开始)。


setex  <key><过期时间><value>
设置键值的同时,设置过期时间,单位秒。


getset <key><value>
以新换旧,设置了新值同时获得旧值。

redis的操作具有原子性。但redis的组合操作就不能保证整体具有原子性了。

**原子操作:**在多线程中,不会被线程调度机制打断的操作。redis单命令的原子性主要得益于redis的单线程。

java的i++等操作不是原子操作。

数据结构

string的数据结构为简单动态字符串。是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配。

<1M 时 扩容都是加倍,

`>1M 时 扩容一次最多只会扩1M

最大长度512M。

2.列表(list)

特点:单键多值

redis列表是简单的字符串列表,按照插入顺序排序。可以添加一个元素带列表的头部(左边)或者尾部(右边)。

底层实际上是个双向链表,对两端的操作性能很高,通过索引下标来操作中间节点性能比较差。

常用命令

lpush/rpush  <key><value1><value2><value3> .... 从左边/右边插入一个或多个值。


lpop/rpop  <key>从左边/右边吐出一个值。值在键在,值光键亡。


rpoplpush  <key1><key2><key1>列表右边吐出一个值,插到<key2>列表左边。


lrange <key><start><stop>
按照索引下标获得元素(从左到右)


lrange mylist 0 -1   0左边第一个,-1右边第一个,(0-1表示获取所有)


lindex <key><index>按照索引下标获得元素(从左到右)


llen <key>获得列表长度 


linsert <key>  before <value><newvalue><value>的后面插入<newvalue>插入值


lrem <key><n><value>从左边删除n个value(从左到右)


lset<key><index><value>将列表key下标为
index的值替换成value

数据结构

List的数据结构为快速链表quickList

首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构式ziplist,也就是压缩列表。

他将所有元素紧挨着一起存储,分配的是一块连续的内存。

当数据量比较多的时候才会改成quicklist。

因为普通的链表需要的附加指针空间太大,会比较浪费空间。

Redis将链表和ziplist结合起来组成了quicklist。也就是将多个ziplist使用双向指针穿起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。

3.集合(set)

redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择。并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。

redis 的 set是string类型的无序集合它的底层其实是一个value为null的hash表,所以添加,删除,查找的复杂度都是O(1)

时间复杂度

一个算法,随着数据的增加,执行时间的长短,如果是O(1),数据增加,查找数据的时间算法不变。

常用命令

sadd <key><value1><value2> ..... 
将一个或多个 member 元素加入到集合 key 中,已经存在的 member 元素将被忽略


smembers <key>
取出该集合的所有值。


sismember <key><value>
判断集合<key>是否为含有该<value>值,有1,没有0


scard<key>
返回该集合的元素个数。


srem <key><value1><value2> ....
删除集合中的某个元素。


spop <key>
随机从该集合中吐出一个值。


srandmember <key><n>
随机从该集合中取出n个值。不会从集合中删除 。


smove <source><destination>value
把集合中一个值从一个集合移动到另一个集合


sinter <key1><key2>
返回两个集合的交集元素。


sunion <key1><key2>
返回两个集合的并集元素。


sdiff <key1><key2>
返回两个集合的差集元素(key1中的,不包含key2中的)

数据结构

set数据结构是dict字典,字典是用哈希表实现的。

Java中HashSet的内部实现使用的是HashMap,只不过所有的value都指向同一个对象。redis的set结构也是一样,他的内部也是用hash结构,所有的value都指向同一个内部值。

4.哈希(hash)

redis hash是一个键值对集合。

redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。

类似于Java里面的Map<String,Object>

问题

使用户ID为查找的key,存储的value用户对象包含姓名,年龄,生日等信息。

1.如果用普通的key/value结构来存储。

主要有两种存储方式:

①以用户id为key,将用户信息序列化,存储到value中。

缺点:每次修改用户的某个属性需要先反序列化,改好后再序列化回去。开销比较大

②以用户id+字段值为key,以字段值为value。

缺点:用户id冗余。

2.使用redis 的hash

key-hash[key,value]

将用户id存为key,字段信息在hash集合中,以字段标签为key,字段数据为value。

此时只要通过用户id和字段/属性标签就可以操作对应属性数据了,既不需要重复存储数据,也不会带来序列化和并发修改控制的问题了。

常用命令

hset <key><field><value><key>集合中的  <field>键赋值<value>


hget <key1><field><key1>集合<field>取出 value 


hmset <key1><field1><value1><field2><value2>... 
批量设置hash的值


hexists<key1><field>
查看哈希表 key 中,给定域 field 是否存在。 


hkeys <key>
列出该hash集合的所有field


hvals <key>
列出该hash集合的所有value


hincrby <key><field><increment>
为哈希表 key 中的域 field 的值加上增量 1   -1


hsetnx <key><field><value>
将哈希表 key 中的域 field 的值设置为 value ,当且仅当域 field 不存在 .

数据结构

hash类型对应的数据结构是两种:

ziplist(压缩列表),hashtable(哈希表)

当field-value长度比较短而且个数比较少时,使用ziplist,否则使用hashtable。

5.有序集合(zset)

redis有序集合zset和普通集合set飞铲相似,是一个没有重复元素的字符串集合。

不同之处是有序集合的每个成员都关联了一个评分(score),这个评分被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以重复了

因为元素是有序的的,所以用户可以很快的根据评分(score)或者次序(position)来获取一个范围中的元素。

访问有序集合的中间元素也是非常快的,因此能够使用有序集合作为一个没有重复成员的智能列表。

常用命令

zadd <key><score1><value1><score2><value2>…
将一个或多个 member 元素及其 score 值加入到有序集 key 当中。


zrange <key><start><stop> [WITHSCORES] 
返回有序集 key 中,下标在<start><stop>之间的元素带WITHSCORES,可以让分数一起和值返回到结果集。



zrangebyscore key minmax [withscores] [limit offset count]
返回有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。有序集成员按 score 值递增(从小到大)次序排列。 


zrevrangebyscore key maxmin [withscores] [limit offset count]        
同上,改为从大到小排列。 


zincrby <key><increment><value>   
为元素的score加上增量



zrem <key><value>
删除该集合下,指定值的元素



zcount <key><min><max>
统计该集合,分数区间内的元素个数 


zrank <key><value>
返回该值在集合中的排名,从0开始。

使用场景

利用zset实现一个文章访问量的排行榜。

数据结构

SortedSet(zset)是redis提供的一个非常特别的数据结构,一方面它等价于Java的数据结构Map<String,Double>,可以给每个元素value赋予一个权重score,另一方面它又类似于TreeSet,内部的元素回按照权重score进行排序,可以得到每个元素的名次,还可以通过score的范围来获取元素的列表。

zset底层使用了两个数据结构

(1)hash,hash的作用就是关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score值。

(2)跳跃表,跳跃表的目的在于给元素value排序,根据score的范围获取元素列表。

跳跃表(跳表)

简介

​ 有序集合在生活中比较常见,例如根据成绩对学生排名,根据得分对玩家排名等。对于有序集合的底层实现,可以用数组、平衡树、链表等。数组不便于元素的插入、删除;平衡树或者红黑树虽然效率高但是结构负责;链表查询需要遍历所有效率低。redis采用的是跳跃表。跳跃表效率堪比红黑树,实现又远比红黑树简单。

实例

对比有序链表和跳跃表,从链表中查询出51.

(1)有序链表

redis-1.png 要查找值为51的元素,需要从第一个元素开始依次查找、比较才能找到。共需要6次比较。

(2)跳跃表 redis-2.png

从第2层开始,1节点比51小,向后比较。21节点比51小,继续向后比较,后面就是null了。

所以从21节点下降到第1层。在第1层,41节点比51节点小,继续向后,61节点比51大,所以从41下降。

在第0层,51节点就是要查找的节点,节点被找到,共查找4次。

查找过程:1->21->null(向下)->41->61(向下,仅判断,没有走)->51

共查找4次。

可以看出跳跃表比有序链表效率要高。

七、redis的发布和订阅

1.什么是发布和订阅

redis发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。

redis客户端可以订阅任意数量的频道。

2.redis的发布和订阅

​ 可以有多个客户端订阅同一个频道。当给这个频道发布消息后,消息就会发送给订阅的客户端。

订阅

语法:

SUBSCRIBE channel1

发布

publish channel1 【消息】

执行后返回的数字是订阅者的数量。

发布的消息是没有经过持久化的,只能受到订阅后发布的消息

八、Jedis

Redis厂商提供的数据库连接的接口是Jedis。

1.所需jar包

<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>

2.注意事项

使用的是Linux操作系统中的Redis,所以需要先禁用掉Linux的防火墙

执行命令【CentOS7版本】

systemctl stop/disable firewalld.service

接着在redis.conf中

注释掉bind 127.0.0.1

然后protected-mode的值设置为no

3.API

测试连接

import redis.clients.jedis.Jedis;
public class Demo01 {
public static void main(String[] args) {
Jedis jedis = new Jedis("192.168.137.3",6379);
String pong = jedis.ping();
System.out.println("连接成功:"+pong);
jedis.close();
}
}

数据类型

①key
jedis.set("k1", "v1");
jedis.set("k2", "v2");
jedis.set("k3", "v3");
Set<String> keys = jedis.keys("*");
System.out.println(keys.size());
for (String key : keys) {
System.out.println(key);
}
System.out.println(jedis.exists("k1"));
System.out.println(jedis.ttl("k1"));                
System.out.println(jedis.get("k1"));

②string
jedis.mset("str1","v1","str2","v2","str3","v3");
System.out.println(jedis.mget("str1","str2","str3"));

③list
List<String> list = jedis.lrange("mylist",0,-1);
for (String element : list) {
System.out.println(element);
}

④set
jedis.sadd("orders", "order01");
jedis.sadd("orders", "order02");
jedis.sadd("orders", "order03");
jedis.sadd("orders", "order04");
Set<String> smembers = jedis.smembers("orders");
for (String order : smembers) {
System.out.println(order);
}
jedis.srem("orders", "order02");

⑤hash
jedis.hset("hash1","userName","lisi");
System.out.println(jedis.hget("hash1","userName"));
Map<String,String> map = new HashMap<String,String>();
map.put("telphone","13810169999");
map.put("address","atguigu");
map.put("email","abc@163.com");
jedis.hmset("hash2",map);
List<String> result = jedis.hmget("hash2", "telphone","email");
for (String element : result) {
System.out.println(element);
}

⑥zset
jedis.zadd("zset01", 100d, "z3");
jedis.zadd("zset01", 90d, "l4");
jedis.zadd("zset01", 80d, "w5");
jedis.zadd("zset01", 70d, "z6");
 
Set<String> zrange = jedis.zrange("zset01", 0, -1);
for (String e : zrange) {
System.out.println(e);
}

4.应用场景

存储手机验证码,设置限制时间,存取效率高。

5.Multi、Exec、discard

​ 从输入Multi命令开始,输入的命令都会依次进入命令队列中,但是不会执行,直到输入Exec后,Redis会将之前的命令队列中的命令依次执行。

​ 组队的过程中可以通过discard来放弃组队。

​ 组队阶段报错,提交失败。

​ 组队成功,提交有成功有失败情况。

watch key [key ...]

​ 在执行multi之前,先执行watch key1[key2] ,可以监视一个(或这些)key被其他命令所改动,那么事务将被打断。

unwatch

​ 取消watch命令对所有key的监视。

​ 如果在执行watch命令之后,exec命令或者discard命令先被执行了的话,那么就不需要再执行unwatch了。

6.事务的错误处理

​ 组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消。

​ 如果执行阶段某个命令报出了错误,那么只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。

7.事务冲突

悲观锁

​ 悲观锁,每次拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样被人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁写锁等,都是在做操作之前先上锁。

乐观锁

​ 乐观锁,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新额时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。

8.redis事务三特性

  1. 单独的隔离操作
    • 事务中的所有命令都会序列化、按顺序的执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
  2. 没有隔离级别的概念
    • 队列中的命令没有提交之前都不会被实际执行,因为事务提交之前任何指令都不会被实际执行。
  3. 不保证原子性
    • 事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚。

9.秒杀

秒杀并发模拟

使用工具ab模拟测试,CentOS7需要手动安装

安装:

联网下:yum install httpd-tools

ab测试

vim postfile 模拟表单提交参数,以&符号结尾;存放当前目录。

内容:prodid=0101&

ab -n 2000 -c 200 -k -p ~/postfile -T application/x-www-form-urlencoded http://主机Ip地址:8080/redis_seckill/doseckill

超卖问题

​ 最终卖出的数量大于预售的数量。

解决方案:利用乐观锁淘汰用户,解决超卖问题

//增加乐观锁
jedis.watch(qtkey);
 
//3.判断库存
String qtkeystr = jedis.get(qtkey);
if(qtkeystr==null || "".equals(qtkeystr.trim())) {
System.out.println("未初始化库存");
jedis.close();
return false ;
}
 
int qt = Integer.parseInt(qtkeystr);
if(qt<=0) {
System.err.println("已经秒光");
jedis.close();
return false;
}
 
//增加事务
Transaction multi = jedis.multi();
 
//4.减少库存
//jedis.decr(qtkey);
multi.decr(qtkey);
 
//5.加人
//jedis.sadd(usrkey, uid);
multi.sadd(usrkey, uid);
 
//执行事务
List<Object> list = multi.exec();
 
//判断事务提交是否失败
if(list==null || list.size()==0) {
System.out.println("秒杀失败");
jedis.close();
return false;
}
System.err.println("秒杀成功");
jedis.close();

继续增加并发量进行测试

①连接有限制
ab -n 2000 -c 200 -k -p postfile -T 'application/x-www-form-urlencoded' http://主机Ip地址:8080/redis_seckill/doseckill 

增加-r参数,-r   Don't exit on socket receive errors.
ab -n 2000 -c 100 -r -p postfile -T 'application/x-www-form-urlencoded' http://主机Ip地址:8080/redis_seckill/doseckill
②发现,已经秒光但是还有库存
ab -n 2000 -c 100 -p postfile -T 'application/x-www-form-urlencoded' http://主机Ip地址:8080/redis_seckill/doseckill 

已经秒光【并发结束】,但是还有库存。

原因:乐观锁导致很多请求都失败了。先点的没有秒杀到,后点的可能秒到了。

③连接超时问题

​ 通过连接池解决。

连接池:

​ 节省每次连接redis服务器带来的消耗,把连接好的实例反复利用。

​ 通过参数管理连接的行为。

连接池参数

  • MaxTotal:控制一个pool可分配多少个jedis实例,通过pool.getResource()来获取;如果赋值为-1,则表示不限制;如果pool已经分配了MaxTotal个jedis实例,则此时pool的状态为exhausted。
  • maxIdle:控制一个pool最多有多少个状态为idle(空闲)的jedis实例;
  • MaxWaitMillis:表示borrow一个jedis实例时,最大的等待毫秒数,如果超过等待时间就直接抛出JedisConnectionException;
  • testOnBorrow:获得一个jedis实例的时候是否检查连接可用性(ping());如果为true,则得到的jedis实例均可用。
④解决库存遗留问题——LUA脚本

​ Lua 是一个小巧的脚本语言,Lua脚本可以很容易的被C/C++ 代码调用,也可以反过来调用C/C++的函数,Lua并没有提供强大的库,一个完整的Lua解释器不过200k,所以Lua不适合作为开发独立应用程序的语言,而是作为嵌入式脚本语言。

​ 很多应用程序、游戏使用LUA作为自己的嵌入式脚本语言,以此来实现可配置性、可扩展性。

LUA脚本在Redis中的优势

​ 将复杂的或者多步的redis操作,写为一个脚本,一次提交给redis执行,减少反复连接redis的次数。提升性能。

​ LUA脚本类似reids事务,有一定的原子性,不会被其他命令插队,可以完成一些redis事务性的操作。

​ 但是注意reids 的lua脚本功能,只能在redis 2.6以上的版本才可以使用。

​ 利用lua脚本淘汰用户,解决超卖问题。

​ redis2.6版本以后,通过lua脚本解决争抢问题,实际上是redis利用它单线程的特性,用任务队列的方式解决多任务并发问题

九、redis持久化——RDB

​ Redis提供了两个不同形式的持久化方式。

  • RDB
  • AOF

1.RDB介绍

​ RDB在指定时间间隔内将内存中的数据集快照写入磁盘,也就是Snapshot快照,它恢复时是将快照文件直接读到内存里。

2.备份如何执行

​ Redis 会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能,如果需要进行大规模的数据的恢复,且对于数据恢复的完整性不是非常敏感,那么RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失

3.fork

​ fork的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等)数值都和原进程一致。但是是一个全新的进程,并作为原进程的子进程

​ 在linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会被exec系统调用,出于效率考虑,linux中引入了“写时复制技术”。

一般父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。

4.RDB持久化流程

redis-3.png

5.dump.rdb文件

​ 在redis.conf中配置文件名称,默认为dump.rdb

配置位置

​ rdb文件的保存路径,也可以修改。默认为Redis启动时的命令行所在的目录下。

​ dir "/myredis/" 将位置配置在myredis文件夹下

6.触发RDB快照:保持策略

配置文件中默认的快照配置

#(3600秒)一小时后有至少一个key改变就触发
save 3600 1
#(300秒)五分钟后至少有10个key改变
save 30 10
#(60秒)一分钟后至少与10000个key改变
save 60 10000

save 和 bgsave

​ save:save时只管保存,其他不管,全部阻塞。手动保存,不建议。

​ bgsave:Redis会在后台异步进行快照操作,快照同时还可以响应客户端请求。

​ 可以通过lastsave命令获取最后一次成功执行快照的时间。

flushall命令

​ 执行flushall命令,也会产生dump.rdb文件,但里面是空的,无意义。

save

格式:save 【秒钟】 【写操作次数】

​ RDB是整个内存的压缩过的Snapshot,RDB的数据结构,可以配合符合的快照触发条件。

​ 默认是一分钟至少一万个key发生变化,或者5分钟至少100个key发生变化,或者一个小时至少1个key发生变化。

​ 如何禁用:不设置save指令,或者给save传入空字符串

stop-writes-on-bgsave-error

​ 当Redis无法写入磁盘的话,直接关掉Redis的写操作。推荐将这个属性设置为yes

rdbcompression压缩文件

​ 对于存储到磁盘中的快照,可以设置是否进行压缩存储。如果该属性设置为yes的话,redis会采用LZF算法进行压缩。

​ 如果不想消耗CPU来进行压缩的话,可以设置为关闭此功能。推荐设置为yes。

rdbchecksum检查完整性

​ 在存储快照后,还可以让redis使用CRC64算法来进行数据校验。

​ 但是这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能。

​ 推荐yes。

rdb的备份

​ 先通过config get dir 查询rdb文件的

​ 将*.rdb的文件拷贝到别的地方

rdb的恢复

  • 关闭redis
  • 先把备份的文件拷贝到工作目录下。cp dump2.rdb dump.rdb
  • 启动redis,备份数据会直接加载

7.优势

  • 适合大规模的数据恢复
  • 对数据完整性和一致性要求不高更适合使用
  • 节省磁盘空间
  • 恢复速度快

8.劣势

  • fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑。
  • 虽然redis在fork的时候使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能。
  • 在备份周期在一定间隔时间做一次备份,所以如果redis以外down掉的话,就会丢失最后一次快照后的所有修改

9.如何停止

​ 动态停止RDB:

redis-cli config set save ""

​ save 后给空值,表示禁用保存策略。

10.总结

  1. RDB是一个非常紧凑的文件。
  2. RDB在保存RDB文件时父进程唯一需要做的就是fork出一个子进程,接下来的工作全部由子进程来做,父进程不需要再做其他IO操作,所以RDB持久化方式可以最大化redis性能
  3. 和AOF相比,在恢复大的数据集的时候,RDB方式会更快一点
  4. 数据丢失风险大
  5. RDB需要经常fork子进程来保存数据集到硬盘上,当数据集比较大的时候,fork的过程是非常耗时的,可能导致redis在一些毫秒级不能响应客户端请求。

十、redis持久化——AOF

1.AOF介绍

以日志形式来记录每个写操作(增量保存),将redis执行过程的所有写指令记录下来(读操作不记录),秩序追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。

2.AOF持久化流程

  1. 客户端的请求写命令会被append追加到AOF缓冲区内;
  2. AOF缓冲区根据AOF持久化策略[always,everysec,no]将操作sync同步到磁盘的AOF文件中;
  3. AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量;
  4. redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的;

3.AOF是默认不开启的

​ 可以在redis.conf中配置文件名称,默认为appendonly.aof

​ AOF文件的保存路径,同RDB路径一致。

4.当AOF和RDB同时开启

​ 系统默认读取AOF的数据(数据不会存在丢失)

5.AOF启动/修复/恢复

​ AOF的备份机制和性能虽然和RDB不同,但是备份和恢复的操作同RDB一样,都是拷贝备份文件,需要恢复时再拷贝到redis工作目录下,启动系统即加载。

正常恢复

  1. 修改默认的appendonly no改为yes
  2. 将现有数据的aof文件复制一份保存到对应目录(查看目录:config get dir)
  3. 恢复:重启redis然后重新加载

异常恢复

  1. 修改默认的appendonly no 改为yes
  2. 如果遇到aof文件损坏,通过/usr/local/bin/redis-check-aof--fix appendonly.aof进行恢复
  3. 备份被写坏的aof文件
  4. 恢复:重启redis,然后重新加载

6.aof同步频率设置

appendfsync always

始终同步,每次reids的写入都会立刻计入日志;性能比较差但数据完整性比较好

appendfsync everysec

每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失

appendfsync no

redis不主动进行同步,把同步时机交给操作系统

7.rewrite压缩

(1)是什么

​ AOF采用文件追加方式,文件会越来越大为避免出现此种情况,新增了重写机制, 当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩, 只保留可以恢复数据的最小指令集.可以使用命令bgrewriteaof

(2)原理

​ aof文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写临时文件,最后再rename),redis4.0版本后的重写,是指上就是把rdb的快照,以二进制形式附在新的aof头部,作为已有的历史数据,替换掉原来的流水账操作。

no-appendfsync-on-rewrite:

如果 no-appendfsync-on-rewrite=yes ,不写入aof文件只写入缓存,用户请求不会阻塞,但是在这段时间如果宕机会丢失这段时间的缓存数据。(降低数据安全性,提高性能)

如果 no-appendfsync-on-rewrite=no, 还是会把数据往磁盘里刷,但是遇到重写操作,可能会发生阻塞。(数据安全,但是性能降低)

触发机制,何时重写

​ Redis会记录上次重写时的AOF大小,默认配置是当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发

​ 重写虽然可以节约大量磁盘空间,减少恢复时间。但是每次重写还是有一定的负担的,因此设定Redis要满足一定条件才会进行重写。

auto-aof-rewrite-percentage:设置重写的基准值,文件达到100%时开始重写(文件是原来重写后文件的2倍时触发)

auto-aof-rewrite-min-size:设置重写的基准值,最小文件64MB。达到这个值开始重写。

例如:文件达到70MB开始重写,降到50MB,下次什么时候开始重写?100MB

系统载入时或者上次重写完毕时,Redis会记录此时AOF大小,设为base_size,

如果Redis的AOF当前大小>= base_size +base_size*100% (默认)且当前大小>=64mb(默认)的情况下,Redis会对AOF进行重写。

重写流程

  1. bgrewriteaof触发重写,判断是否当前有bgsave或者bgrewriteaof在运行,如果有,则等待该命令结束后再继续执行。
  2. 主进程fork出子进程执行重写操作,保证主进程不会阻塞。
  3. 子进程遍历redis内存中数据到临时文件。客户端的写请求同时写入aof_buf缓冲区和aof_rewrite_buf重写缓冲区保证原AOF文件完整性以及新AOF文件生成期间的新的数据修改动作不会丢失。
  4. 子进程写完新的aof文件后,向主进程发信号,父进程更新统计信息;主进程把aof_rewrite_buf中的数据写入到新的aof文件。
  5. 使用新的aof文件覆盖旧的aof文件,完成aof重写。

8.优势

  1. 备份机制更文件,丢失数据概率更低。
  2. 可读的日志文本,通过操作aof文件,可以处理误操作。

9.劣势

  1. 比起去RDB占用更多磁盘文件
  2. 恢复备份速度要慢
  3. 每次读写都同步的话,有一定的性能压力
  4. 存在个别bug,造成无法恢复

10.总结

  1. AOF文件是一个只进行追加的日志文件;
  2. redis可以在AOF文件体积变的过大时,自动的在后台对AOF文件进行重写;
  3. AOF文件有序地保存了对数据库执行的所有写入操作,这些写入操作以redis协议的格式保存,因此AOF文件的内容非常容易被人读懂,对文件进行分析也很轻松;
  4. 对于相同的数据集来说,AOF文件的体积通常大于RDB文件的体积;
  5. 根据所使用的fsync策略,AOF的速度可能慢于RDB。

持久化——总结

官方推荐两个都启用。

如果对数据不敏感,可以选单独用RDB

不建议单独用 AOF,因为可能会出现Bug

如果只是做纯内存缓存,可以都不用

性能建议:

因为RDB文件只用作后备用途,建议只在Slave上持久化RDB文件,而且只要15分钟备份一次就够了,只保留save 900 1这条规则。

如果使用AOF,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只load自己的AOF文件就可以了。

代价,一是带来了持续的IO,二是AOF rewrite的最后将rewrite过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。

只要硬盘许可,应该尽量减少AOF rewrite的频率,AOF重写的基础大小默认值64M太小了,可以设到5G以上。

默认超过原大小100%大小时重写可以改到适当的数值。

十一、Redis主从复制

1.主从复制

​ 主机数据更新后根据配置和策略,自动同步到备机的master/slaver机制,Master以写为主,Slave以读为主。

2.作用

  • 读写分离,性能扩展
  • 容灾快速恢复

3.怎么用

  1. 拷贝多个redis.conf文件include(写绝对路径)
  2. 开启daemonize yes
  3. pid 文件名字 pidfile
  4. 指定端口 port
  5. log文件名字
  6. dump.rdb名字 dbfilename
  7. Appendonly关掉或者换名字

4.示例

(1)新建redis6379.conf,填写以下内容

include /myredis/redis.conf
pidfile /var/run/redis_6379.pid
port 6379
dbfilename dump6379.rdb

(2)redis6380.conf,填写以下内容

include /myredis/redis.conf
pidfile /var/run/redis_6380.pid
port 6380
dbfilename dump6380.rdb

(3)redis6381.conf

include /myredis/redis.conf
pidfile /var/run/redis_6381.pid
port 6381
dbfilename dump6381.rdb

可以添加replica-priority 10

设置从机的优先级,值越小优先级越高,用于选举主机时使用。默认100.

(4)启动三台redis服务器

redis-server redis6379.conf
redis-server redis6380.conf
redis-server redis6381.conf

(5)查看系统进程,三台服务器是否启动

ps -ef|grep redis

(6)查看三台主机运行情况

info replication

(7)配从(库)不配主(库)

slaveof [ip] [port]

成为某个实例的从服务器

  1. 在6380和6381上执行: slaveof 127.0.0.1 6379
  2. 在主机上写,在从机上可以读取数据,此时在从机上写数据是会报错的
  3. 如果主机挂掉,重启就行,是不影响主从关系的
  4. 如果从机重启需要重新设置:slaveof 127.0.0.1 6379

可以将从机的slaveof 关系写入配置文件中。永久生效。

5.一主二仆

切入点问题。

​ slave1,slave2并不是从切入点开始复制,而是从头开始复制

从机可以写吗

​ 不可以。

主机shutdown后情况如何?

​ 从机原地待命,不会上位。

主机回来后,主机新增记录,从机能否顺利复制

​ 可以

其中一台从机down后情况如何?

​ 不能按照原有跟上大部队,得重新slaveof

6.薪火相传

释义:

​ 上一个slave可以是下一个slave的Master,slave同样可以接收其他slaves的连接和同步请求,那么slave作为了链条中下一个的master,可以有效减轻master的写压力,去中心化降低风险。

用slaveof [ip] [port]

中途变更转向:会清除之前的数据,重新建立拷贝最新的。

风险是一旦某个slave宕机,后面的slave都没法备份

主机挂了,从机还是从机,也没有办法写数据了。

7.反客为主

释义:

​ 当一个master宕机后,后面的slave可以立刻升为master,其后面的slave不用做任何修改。

用 slaveof no one 将从机变为主机。

8.复制原理

  • slave启动成功连接到master后会发送一个sync命令。
  • master街道命令启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,再后台进程执行完毕之后,master将传送整个数据文件到slave,以完成一次完全同步。
  • 全量复制:slave服务再接收到数据库文件数据后,将其存盘并加载到内存中。
  • 增量复制:master继续将新的所有收集到的修改命令依次传给slave,完成同步
  • 但是只要是重新连接master,一次完全同步(全量复制)将被自动执行。

9.哨兵模式(sentinel)

是什么

反客为主的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库。

怎么用

(1)调整为一主二仆模式,6379带着6380、6381

(2)自定义的/myredis目录下新建sentinel.conf文件,名字不能错。

(3)配置哨兵,填写内容

sentinel monitor mymaster 127.0.0.1 6379 1

其中 mymaster 为监控对象起的服务器名称,1是至少有多少个哨兵同意迁移的数量。

(4)启动哨兵

/usr/local/bin

redis做压测可以用自带的redis-benchmark工具

执行

redis-sentinel /myredis/sentinel.conf

(5)当主机挂掉,从机选举中产生新的主机

哪个从机会被选举为主机?

根据优先级别slave-priority

原来的主机重启后会变为从机。

10.复制延时

​ 由于所有的写操作都是先在master上操作,然后同步更新到slave上,所以从master同步到slave及其有一定延迟,当系统繁忙时,延迟问题更加严重。slave及其数量的增加也会使这个问题更加严重。

11.故障恢复

(1)新主登基

​ 从下线的主服务的所有从服务中挑选一个从服务,将其转成主服务。选择条件依次为:

  1. 选择优先级靠前的
  2. 选择偏移量最大的
  3. 选择runid最小的从服务

优先级 slave-priority 默认100 值越小优先级越高

偏移量是指获得原主机数据最全的

每个redis实例启动后都会随机生成一个40位的runid

(2)群仆俯首

​ 挑选出新的主服务之后,sentinel向原主服务的从服务发送slaveof新主服务的命令,复制新的master

(3)旧主俯首

​ 当已经下线的服务重新上线时,sentinel会向其发送slaveof命令,让其成为新主的从。

12.java设置主从复制

private static JedisSentinelPool jedisSentinelPool=null;

public static  Jedis getJedisFromSentinel(){
if(jedisSentinelPool==null){
            Set<String> sentinelSet=new HashSet<>();
            sentinelSet.add("192.168.11.103:26379");

            JedisPoolConfig jedisPoolConfig =new JedisPoolConfig();
            jedisPoolConfig.setMaxTotal(10); //最大可用连接数
jedisPoolConfig.setMaxIdle(5); //最大闲置连接数
jedisPoolConfig.setMinIdle(5); //最小闲置连接数
jedisPoolConfig.setBlockWhenExhausted(true); //连接耗尽是否等待
jedisPoolConfig.setMaxWaitMillis(2000); //等待时间
jedisPoolConfig.setTestOnBorrow(true); //取连接的时候进行一下测试 ping pong

jedisSentinelPool=new JedisSentinelPool("mymaster",sentinelSet,jedisPoolConfig);
return jedisSentinelPool.getResource();
        }else{
return jedisSentinelPool.getResource();
        }
}

十二、Redis集群

1.集群能够解决的redis问题

容量不够,redis如何进行扩容?

并发写操作,redis如何分摊?

另外,主从模式,薪火相传模式,主机宕机,导致ip地址发生变化,应用程序中配置需要修改对应的主机地址,端口等信息。

之前是通过代理主机来解决的,但是redis3.0提供了解决方案。就是五中心化集群配置。

2.什么是集群

​ Redis集群实现了对Redis的水平扩容,即启动N个redis节点,将整个数据库分步存储在者N个节点中,每个节点存储总数据的1/n。

​ Redis集群通过分区(partition)来提供一定程度的可用性(availability):即使集群中有一部分节点失效或者无法进行通讯,集群也可以继续处理命令请求。

3.怎么用

(1)删除持久化数据

将rdb,aof文件都删除掉。

(2)制作六个实例6379,6380,6381,6389,6390,6391

①配置基本信息

拷贝多个redis.conf文件include(写绝对路径)
开启daemonize yes
Pid文件名字
指定端口
Log文件名字
Dump.rdb名字
Appendonly 关掉或者换名字

②redis cluster配置修改

cluster-enabled yes    打开集群模式
cluster-config-file nodes-6379.conf  设定节点配置文件名
cluster-node-timeout 15000   设定节点失联时间,超过该时间(毫秒),集群自动进行主从切换。
include /root/myredis/redis.conf
port 6379
pidfile "/var/run/redis_6379.pid"
dbfilename "dump6379.rdb"
cluster-enabled yes
cluster-config-file nodes-6379.conf
cluster-node-timeout 15000

③修改好redis6379.conf文件,拷贝多个redis.conf文件

redis6379.conf

redis6380.conf

redis6381.conf

redis6389.conf

redis6390.conf

redis6391.conf

④使用查找替换修改另外5个文件

例如

:%s/6379/6380 

(3)启动六个redis服务

redis-server redis6379.conf

redis-server redis6380.conf

redis-server redis6381.conf

redis-server redis6389.conf

redis-server redis6390.conf

redis-server redis6391.conf

(4)将六个节点合成一个集群

组合之前,需要确保所有redis实例启动后,nodes-xxx.conf 文件都生成正常

合体

cd /opt/redis-6.2.1/src
redis-cli --cluster create --cluster-replicas 1 192.168.6.100:6379 192.168.6.100:6380 192.168. 6.100:6381 192.168.6.100:6389 192.168. 6.100:6390 192.168.6.100:6391

此处不要用127.0.0.1,要使用真实IP地址。

-replicas 1 采用最简单的方式配置集群,一台主机,一台从机,正好三组。

普通方式登录

​ 可能直接进入读主机,存储数据时,会出现moved重定向操作。所以,应该以集群方式登录。

(5)-c采用集群策略连接

设置数据会自动切换到对应的写主机

redis-cli -c -p 6379

(6)通过cluster nodes 命令查看集群信息

cluster nodes

(7)redis cluster如何分配这六个节点?

一个集群至少要有三个主节点。

​ 选项 --cluster-replicas 1表示我们希望位集群中的每一个主节点创建一个从节点。

分配原则尽量保证每个主数据库运行在不同的IP地址,每个从库和主库不在同一个IP地址上。

(8)什么是slots

插槽。

​ 一个 Redis 集群包含 16384 个插槽(hash slot), 数据库中的每个键都属于这 16384 个插槽的其中一个,

​ 集群使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽, 其中 CRC16(key) 语句用于计算键 key 的 CRC16 校验和 。

​ 集群中的每个节点负责处理一部分插槽。 举个例子, 如果一个集群可以有主节点, 其中:

节点 A 负责处理 0 号至 5460 号插槽。

节点 B 负责处理 5461 号至 10922 号插槽。

节点 C 负责处理 10923 号至 16383 号插槽。

(9)在集群中录入值

​ 在redis-cli每次录入、查询键值,redis都会计算出该key应该送往的插槽,如果不是该客户端对应服务器的插槽,redis会报错,并告知应前往的redis实例地址和端口。

​ redis-cli客户端提供了 –c 参数实现自动重定向。

​ 如 redis-cli -c –p 6379 登入后,再录入、查询键值对可以自动重定向。

​ 不在一个slot下的键值,是不能使用mget,mset等多键操作。

(10)查询集群中的值

CLUSTER GETKEYSINSLOT <slot><count> 

返回 count 个 slot 槽中的键。

(11)故障恢复

如果主节点下线?从节点能否自动升为主节点?

注意:15****秒超时

主节点恢复后,主从关系会如何?

主节点回来变成从机。

如果所有某一段插槽的主从节点都宕掉,redis服务是否还能继续?

  • 如果某一段插槽的主从都挂掉,而cluster-require-full-coverage 为yes ,那么 ,整个集群都挂掉
  • 如果某一段插槽的主从都挂掉,而cluster-require-full-coverage 为no ,那么,该插槽数据全都不能使用,也无法存储。

redis.conf中的参数 cluster-require-full-coverage

4.集群的Jedis开发

即使连接的不是主机,集群会自动切换主机存储。主机写,从机读。

无中心化主从集群。无论从哪台主机写的数据,其他主机上都能读到数据。

public class JedisClusterTest {
  public static void main(String[] args) { 
     Set<HostAndPort>set =new HashSet<HostAndPort>();
     set.add(new HostAndPort("192.168.31.211",6379));
     JedisCluster jedisCluster=new JedisCluster(set);
     jedisCluster.set("k1", "v1");
     System.out.println(jedisCluster.get("k1"));
  }
}

5.Redis集群的好处

  • 实现扩容
  • 分摊压力
  • 无中心配置相对简单

6.Redis集群的不足

  • 不支持多键操作
  • 多键的Redis事务不被支持,lua脚本不被支持
  • 由于集群方案出现较晚,很多公司已经采用了其他的集群方案,而代理或者客户端分片的方案想要迁移至redis cluster,需要整体迁移,而不是逐步过渡,复杂度较大。