(一)NoSQL数据库概述:
NoSQL(NoSQL = Not Only SQL ),意即“不仅仅是SQL”,泛指非关系型的数据库。
NoSQL 不依赖业务逻辑方式存储,而以简单的key-value模式存储。因此大大的增加了数据库的扩展能力。
(二)适用场景:对数据高并发的读写;海量数据的读写;对数据高可扩展性的。
(三)NoSQL不适用场景:
原子性: 所谓原子操作是指不会被线程调度机制打断的操作; 这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch(切换到另一个线程)。
(1)在单线程中, 能够在单条指令中完成的操作都可以认为是"原子操作",因为中断只能发生于指令之间。
(2)在多线程中,不能被其它进程(线程)打断的操作就叫原子操作。 Redis单命令的原子性主要得益于Redis的单线程。
Redis安装
(Linux中)安装目录:/usr/local/bin
查看默认安装目录:
redis-benchmark:性能测试工具,可以在自己本子运行,看看自己本子性能如何
redis-check-aof:修复有问题的AOF文件,rdb和aof后面讲
redis-check-dump:修复有问题的dump.rdb文件
redis-sentinel:Redis集群使用
redis-server:Redis服务器启动命令
redis-cli:客户端,操作入口
(一)前台启动(不推荐)
redis-server
redis配置文件详解
redis.conf
cp /opt/redis-6.2.6/redis.conf /myredis
vi redis.conf
编辑redis-cli
redis-cli shutdown
select
来切换数据库。如: select 8Redis是单线程+多路IO复用技术
多路复用是指使用一个线程来检查多个文件描述符(Socket)的就绪状态,比如调用select和poll函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞直到超时。得到就绪状态后进行真正的操作可以在同一个线程里执行,也可以启动线程执行(比如使用线程池)
Memcache | Redis |
---|---|
多线程+锁 | 单线程+多路IO复用 |
Redis与Memcache三点不同: 支持多数据类型,支持持久化,单线程+多路IO复用
redis是基于key:value存储的
命令 | 作用 |
---|---|
keys * | 查看当前库所有key |
exists key | 判断某个key是否存在 |
type key | 查看你的key是什么类型 |
del key | 删除指定的key数据 |
unlink key | 根据value选择非阻塞删除 |
expire key 10 | 为给定的key设置过期时间,这里是10秒(单位:秒) |
ttl key | 查看key还有多少秒过期,-1表示永不过期,-2表示已过期 |
select 库号 | 命令切换数据库(例:select 8 切换到8号数据库) |
dbsize | 查看当前数据库的key的数量 |
flushdb | 清空当前库 |
flushall | 清空全部库 |
del key
和 unlink key
的区别:
del key
直接删除
unlink key
仅将keys从keyspace元数据中删除,真正的删除会在后续异步操作
简介:
- String是Redis最基本的类型,你可以理解成与Memcached一模一样的类型,一个key对应一个value。
- String类型是二进制安全的。意味着Redis的string可以包含任何数据。比如jpg图片或者序列化的对象。
- String类型是Redis最基本的数据类型,一个Redis中字符串value最多可以是512M
字符串(String)常用命令
命令 | 作用 |
---|---|
set |
添加键值对(当数据库中key不存在时,可以将key-value添加数据库;当数据库中key存在时,覆盖已有的value) |
get |
查询对应键值 |
append |
将给定的 |
strlen |
获得值的长度 |
setnx |
只有在 key 不存在时设置 key 的值,key存在则无法设置key的值(无法覆盖) |
incr |
将 key 中储存的数字值增1,只能对数字值操作,如果为空,新增值为1 |
decr |
将 key 中储存的数字值减1,只能对数字值操作,如果为空,新增值为-1 |
incrby/decrby |
将 key 中储存的数字值增/减。自定义步长。 |
mset |
同时设置一个或多个 key-value对 |
mget |
同时获取一个或多个 value |
msetnx |
同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在(若有一个key-value设置失败,则全部设置失败) |
getrange |
获得值的范围,类似java中的substring |
setrange |
用 |
setex |
设置键值的同时,设置过期时间,单位秒 |
getset |
设置了新值同时获得旧值。 |
数据结构:
String的数据结构为简单动态字符串(Simple Dynamic String,缩写SDS)。是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配.
如图中所示,内部为当前字符串实际分配的空间capacity一般要高于实际字符串长度len。当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次只会多扩1M的空间。需要注意的是字符串最大长度为512M。
简介:
单键多值,Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。
它的底层实际是个双向链表,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差。
命令 | 作用 |
---|---|
lpush/rpush |
从左边/右边插入一个或多个值。 |
lpop/rpop |
从左边/右边获取并移除一个值,类似于出栈/出队 |
rpoplpush |
从 |
lrange |
从左边遍历数据并指定开始和结束索引,0是第一个索引,-1终索引 |
lindex |
获取指定索引数据,没有则为null,没有索引越界 |
llen |
获得列表长度 |
linsert |
在 |
lrem |
从左边删除n个value(从左到右) |
lset |
将列表key下标为index的值替换成value |
数据结构:
List的数据结构为快速链表quickList。
首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是ziplist,也即是压缩列表。
当数据量比较多的时候才会改成quicklist。
因为普通的链表需要的附加指针空间太大,会比较浪费空间。比如这个列表里存的只是int类型的数据,结构上还需要两个额外的指针prev和next。
Redis将链表和ziplist结合起来组成了quicklist。也就是将多个ziplist使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。
简介:
Redis
set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。Redis的Set是string类型的无序集合。它底层其实是一个value为null的hash表,所以添加,删除,查找的复杂度都是O(1)。
集合(Set)常用命令
命令 | 作用 |
---|---|
sadd |
将一个或多个 member 元素加入到集合 key 中,已经存在的 member 元素将被忽略 |
smembers |
取出该集合的所有值 |
sismember |
判断集合 |
scard |
返回该集合的元素个数 |
srem |
删除集合中的某个或多个元素 |
spop |
随机获取集合中的某个数据,并将该数据从集合中移除 |
srandmember |
随机从该集合中取出n个值,不会从集合中删除 |
smove | 将指定数据从原始集合移动到目标集合中 |
sinter |
返回两个集合的交集元素 |
sunion |
返回两个集合的并集元素 |
sdiff |
返回两个集合的差集元素(key1中的,不包含key2中的) |
数据结构:
Set数据结构是dict字典,字典是用哈希表实现的。Java中HashSet的内部实现使用的是HashMap,只不过所有的value都指向同一个对象。Redis的set结构也是一样,它的内部也使用hash结构,所有的value都指向同一个内部值。
简介:
Redis hash 是一个键值对集合。 Redis
hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。
类似Java里面的Map
用户ID为查找的key,存储的value用户对象包含姓名,年龄,生日等信息,如果用普通的key/value结构来存储,主要有以下2种存储方式:
哈希(Hash)常用命令
命令 | 作用 |
---|---|
hset |
给 |
hget |
从 |
hmset |
批量设置hash的值 |
hexists |
查看hash表中是否存在指定字段fieId |
hkeys |
列出该hash集合的所有field |
hvals |
列出该hash集合的所有value |
hincrby |
指定字段的数值数据增加指定的值,increment为负数则减少 |
hsetnx |
将哈希表 key 中的域 field 的值设置为 value ,当且仅当域 field 不存在 |
数据结构:
哈希类型的内部编码有两种:ziplist(压缩列表)、hashtable(哈希表、字典)。
当存储的数据量比较小的情况下,Redis才使用压缩列表来实现字典类型,具体需要满足两个条件:
ziplist使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比hashtable更加优秀,当ziplist无法满足哈希类型时,Redis会使用hashtable作为哈希内部实现,因为此时ziplist的读写效率会下降,而hashtable的读写时间复杂度为O(1)。
压缩列表
压缩列表(ziplist)是列表和哈希的底层实现之一,压缩列表用来紧凑数据存储,节省内存,有序:
压缩列表是由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构,一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值。
哈希表
Redis字典使用散列表为底层实现,一个散列表里面有多个散列表节点,每个散列表节点就保存了字典中的一个键值对,发生哈希冲突采用链表发解决,存储无序
简介:
Redis有序集合zset与普通集合set非常相似,是一个没有重复元素的字符串集合。
不同之处是有序集合的每个成员都关联了一个评分(score),这个评分(score)被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复的。
因为元素是有序的, 所以你也可以很快的根据评分(score)或者次序(position)来获取一个范围的元素。
访问有序集合的中间元素也是非常快的,因此你能够使用有序集合作为一个没有重复成员的智能列表。
有序集合Zset(sorted set)常用命令
命令 | 作用 |
---|---|
zadd |
将一个或多个 member 元素及其 score 值加入到有序集 key 当中 |
zrange |
获取指定范围内的数据,升序,WITHSCORES 代表显示分数 |
zrangebyscore key min max [withscores] [limit offset count] | 返回有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。有序集成员按 score 值递增(从小到大)次序排列 |
zrevrangebyscore key max min [withscores] [limit offset count] | 同上,改为从大到小排列 |
zincrby |
指定值的分数增加increment |
zrem |
删除该集合下,指定值的元素 |
zcount |
统计该集合分数区间内的元素个数 |
zrank |
返回该值在集合中的排名,从0开始 |
数据结构:
有序集合是由ziplist(压缩列表)或skiplist(跳跃表)组成的
当数据比较少时,有序集合使用的是ziplist存储的,使用ziplist格式存储需要满足以下两个条件:
当元素比较多时,此时ziplist的读写效率会下降,时间复杂度是O(n),跳跃表的时间复杂度是O(logn)
跳跃表
Redis使用跳跃表作为有序集合的底层实现之一,如果一个有序集合包含的元素数量比较多,又或者有序集合中元素的成员是比较长的字符串时,Redis就会使用跳跃表来作为有序集合键的底层实现
跳跃表在链表的基础上增加了多级索引以提升查找的效率,索引是占内存的,所以是一个空间换时间的方案。原始链表中存储的有可能是很大的对象,而索引节点只需要存储关键值和几个指针,并不需要存储对象,因此当节点本身比较大或者元素数量比较多的时候,其优势可以被放大,而缺点则可以忽略
简介:
现代计算机用二进制(位) 作为信息的基础单位, 1个字节等于8位,合理地使用操作位能够有效地提高内存使用率和开发效率。
Redis提供了
Bitmaps
这个“数据类型”可以实现对位的操作:
Bitmaps
本身不是一种数据类型, 实际上它就是字符串(key-value) , 但是它可以对字符串的位进行操作。- Bitmaps单独提供了一套命令, 所以在Redis中使用Bitmaps和使用字符串的方法不太相同。 可以把Bitmaps想象成一个以位为单位的数组, 数组的每个单元只能存储0和1, 数组的下标在Bitmaps中叫做偏移量。
常用命令
命令 | 作用 |
---|---|
setbit |
设置指定key中偏移量上对应的bit值,value只能为0或1 |
getbit |
获取指定key中偏移量上对应的bit值 |
bitcount |
统计字符串从start字节到end字节比特值为1的数量 |
bitop and(or/not/xor) |
做多个Bitmaps的and(交集) 、 or(并集) 、 not(非) 、 xor(异或) 操作并将结果保存在destkey中 |
简介:
在工作当中,我们经常会遇到与统计相关的功能需求,比如统计网站PV(PageView页面访问量),可以使用Redis的incr、incrby轻松实现。
但像UV(UniqueVisitor,独立访客)、独立IP数、搜索记录数等需要去重和计数的问题如何解决?这种求集合中不重复元素个数的问题称为基数问题。
解决基数问题有很多种方案:
(1)数据存储在MySQL表中,使用distinct count计算不重复个数
(2)使用Redis提供的hash、set、bitmaps等数据结构来处理
以上的方案结果精确,但随着数据不断增加,导致占用空间越来越大,对于非常大的数据集是不切实际的。
能否能够降低一定的精度来平衡存储空间?Redis推出了HyperLogLog
Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog
的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。 在 Redis 里面,每个
HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64
个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。 但是,因为 HyperLogLog
只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。
什么是基数?
比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素)为5。 基数估计就是在误差可接受的范围内,快速计算基数
常用命令
命令 | 作用 |
---|---|
pfadd |
添加指定元素到 HyperLogLog 中 |
pfcount |
计算HyperLogLog的近似基数,可以计算多个HyperLogLog |
pfmerge |
将一个或多个HyperLogLog合并后的结果存储在另一个HyperLogLog中 |
简介:
Redis 3.2
中增加了对GEO类型的支持。GEO,Geographic,地理信息的缩写。该类型,就是元素的2维坐标,在地图上就是经纬度。redis基于该类型,提供了经纬度设置,查询,范围查询,距离查询,经纬度Hash等常见操作。
常用命令
命令 | 作用 |
---|---|
geoadd |
加地理位置(经度,纬度,名称) |
geopos |
获得指定地区的坐标值 |
geodist |
获取两个位置之间的直线距离 |
georadius |
以给定的经纬度为中心,找出某一半径内的元素 |
Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息。Redis 客户端可以订阅任意数量的频道。
客户端可以订阅频道如下图:
当给这个频道发布消息后,消息就会发送给订阅的客户端:
常用命令
命令 | 作用 |
---|---|
subscribe channel [channel …] | 订阅给定的一个或多个频道 |
unsubscribe [channel [channel …]] | 退订一个或多个频道 |
publish channel message | 向指定频道发布消息 |
psubscribe pattern [pattern …] | 订阅一个或多个符合给定模式的频道 |
punsubscribe [pattern [pattern …]] | 退订一个或多个符合给定模式的频道 |
pubsub channels [pattern] | 列出当前的活跃频道 |
pubsub numsub [channel-1 … channel-N] | 返回给定频道的订阅者数量, 订阅模式的客户端不计算在内 |
pubsub numpat | 返回订阅模式的数量(注意, 这个命令返回的不是订阅模式的客户端的数量, 而是客户端订阅的所有模式的数量总和) |
命令测试
---------------订阅端---------------
127.0.0.1:6379> SUBSCRIBE bg #订阅频道bg
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "bg"
3) (integer) 1
---------------消息发布端---------------
127.0.0.1:6379> PUBLISH bg hello #向bg频道发布消息hello
(integer) 1
127.0.0.1:6379>
这个时候再去看订阅端:
---------------订阅端---------------
127.0.0.1:6379> SUBSCRIBE bg
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "bg"
3) (integer) 1
1) "message" #消息
2) "bg" #哪个频道的消息
3) "hello" #消息内容
(注:当订阅端收到消息之后,输入命令无反应的情况,如下图)
这个时候按ctrl+c就可以解决了。
退订频道:
---------------订阅端---------------
127.0.0.1:6379> UNSUBSCRIBE bg #退订bg频道
1) "unsubscribe"
2) "bg"
3) (integer) 0
127.0.0.1:6379>
订阅一个或多个符合给定模式的频道:
127.0.0.1:6379> psubscribe news.*
Reading messages... (press Ctrl-C to quit)
1) "psubscribe" #显示订阅成功
2) "news.*" #订阅的模式
3) (integer) 1 #目前已订阅模式的数量
退订一个或多个符合给定模式的频道:
127.0.0.1:6379> PUNSUBSCRIBE news.*
1) "punsubscribe"
2) "news.*"
3) (integer) 0
原理:
Redis是使用C实现的,通过分析Redis源码里的pubsub.c文件,了解发布和订阅机制的底层实现,籍此加深对Redis的理解。
Redis通过PUBLISH、SUBSCRIBE 和PSUBSCRIBE等命令实现发布和订阅功能。
通过SUBSCRIBE命令订阅某频道后, redis-server 里维护了一个字典,字典的键就是一个个 频道! , 而字典的值则是一个链表,链表中保存了所有订阅这个channel的客户端。SUBSCRIBE 命令的关键,就是将客户端添加到给定channel的订阅链表中。
通过PUBLISH命令向订阅者发送消息, redis-server 会使用给定的频道作为键,在它所维护的channel字典中查找记录了订阅这个频道的所有客户端的链表,遍历这个链表,将消息发布给所有订阅者。
Pub/Sub从字面上理解就是发布( Publish )与订阅( Subscribe ) , 在Redis中,你可以设定对某一个key值进行消息发布及消息订阅,当一个key值上进行了消息发布后,所有订阅它的客户端都会收到相应的消息。这一功能最明显的用法就是用作实时消息系统,比如普通的即时聊天,群聊等功能。
客户端订阅,就被链接到对应频道的链表的尾部,退订则就是将客户端节点从链表中移除。
使用场景
缺点
如果一个客户端订阅了频道,但自己读取消息的速度却不够快的话,那么不断积压的消息会使redis输出缓冲区的体积变得越来越大,这可能使得redis本身的速度变慢,甚至直接崩溃。
这和数据传输可靠性有关,如果在订阅方断线,那么他将会丢失所有在短线期间发布者发布的消息。
介绍
jedis 是 redis推荐的java客户端。通过Jedis我们可以很方便地使用java代码的方式,对redis进行操作。jedis使用起来比较简单,它的操作方法与redis命令相类似。
使用(这里的例子使用maven)
在使用之前我们得修改以下redis配置文件:
(一)这里的bind得注释掉,如果不注释掉,那就只能本机访问,后面的代码无法通过ip访问到。
(二)把这里的保护模式改为no(默认是yes),如果是yes也无法通过远程访问,也只能本机访问
(三)关闭防火墙(开启防火墙:systemctl start firewalld)
然后引入依赖:
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
<version>3.2.0version>
dependency>
然后创建一个类JedisDemo
public class JedisDemo {
public static void main(String[] args) {
//创建Jedis对象
Jedis jedis = new Jedis("192.168.157.128",6379);
//测试
String value = jedis.ping();
System.out.println(value);
}
我们还能通过jedis用java代码对redis中的数据类型进行操作
jedis操作和使用
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-pool2artifactId>
<version>2.6.0version>
dependency>
#Redis服务器地址
spring.redis.host=192.168.157.128
#Redis服务器连接端口
spring.redis.port=6379
#Redis数据库索引(默认为0)
spring.redis.database= 0
#连接超时时间(毫秒)
spring.redis.timeout=1800000
#连接池最大连接数(使用负值表示没有限制)
spring.redis.lettuce.pool.max-active=20
#最大阻塞等待时间(负数表示没限制)
spring.redis.lettuce.pool.max-wait=-1
#连接池中的最大空闲连接
spring.redis.lettuce.pool.max-idle=5
#连接池中的最小空闲连接
spring.redis.lettuce.pool.min-idle=0
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setConnectionFactory(factory);
//key序列化方式
template.setKeySerializer(redisSerializer);
//value序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
//value hashmap序列化
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题),过期时间600秒
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
}
@RestController
@RequestMapping("/redisTest")
public class RedisTestController {
@Autowired
private RedisTemplate redisTemplate;
@GetMapping
public String testRedis() {
//设置值到redis
redisTemplate.opsForValue().set("name","lucy");
//从redis获取值
String name = (String)redisTemplate.opsForValue().get("name");
return name;
}
}
我们早在学习MySQL的时候就知道事务(ACID)就是所谓的原子性(Atomicity)
一致性(Consistency),隔离性(Isolation),持久性(Durability)
Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
Redis事务的主要作用就是串联多个命令防止别的命令插队。
开启事务(Multi
)
从输入Multi命令开始,输入的命令都会依次进入命令队列中(为了方便理解,这里将其称为组队),但不会执行。
执行事务(Exec
)
输入Exec后,Redis会将之前的命令队列中的命令依次执行。
(注意:当我们执行完事务要重新开启事务!!!)
127.0.0.1:6379> MULTI #开启事务
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> exec #执行事务
1) OK
2) OK
3) OK
127.0.0.1:6379>
127.0.0.1:6379> MULTI #开启事务
OK
127.0.0.1:6379(TX)> set k4 v4
QUEUED
127.0.0.1:6379(TX)> set k5 v5
QUEUED
127.0.0.1:6379(TX)> set k6 v6
QUEUED
127.0.0.1:6379(TX)> DISCARD #取消事务
OK
127.0.0.1:6379>
代码语法错误(编译时异常)所有的命令都不执行。
就是组队中某个命令出现了报告错误,执行的时侯整个的所有队列都会被取消。
127.0.0.1:6379> MULTI #开启事务
OK
127.0.0.1:6379(TX)> set pym serein
QUEUED
127.0.0.1:6379(TX)> set bg ggz
QUEUED
127.0.0.1:6379(TX)> seete k1 v1 #没有这个语法,所以出现了编译时异常
(error) ERR unknown command `seete`, with args beginning with: `k1`, `v1`,
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> EXEC #执行事务
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get pym #因为出现了编译时异常,所以整个队列都不会被执行
(nil)
代码逻辑错误 (运行时异常) 其他命令可以正常执行 所以不保证事务原子性。
就是执行事务阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行
127.0.0.1:6379> MULTI #开启事务
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> INCR k1 #这里因为k1的值是v1,无法加1,所以会报错
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> EXEC #执行事务
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
4) OK
127.0.0.1:6379> get k3 #能取到k3的值,说明后面两行命令也执行了
"v3"
127.0.0.1:6379>
虽然中间有一条命令报错了,但是后面的指令依旧正常执行成功了。 所以说Redis单条指令保证原子性,但是Redis事务不能保证原子性。
悲观锁(Pessimistic Lock)
顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞(block)直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
乐观锁(Optimistic Lock)
顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。
redis实现乐观锁
使用
watch key
监控指定数据,相当于乐观锁加锁。
在执行multi之前,先执行watch key1 [key2],可以监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
redis监视测试:
127.0.0.1:6379> set money 100 #设置余额:100
OK
127.0.0.1:6379> set usedMoney 0 #设置使用金额:0
OK
127.0.0.1:6379> WATCH money #监视余额(上锁)
OK
127.0.0.1:6379> MULTI #开启事务
OK
127.0.0.1:6379(TX)> decrby money 30 #余额-30
QUEUED
127.0.0.1:6379(TX)> incrby usedMoney 30 #使用金额+30
QUEUED
127.0.0.1:6379(TX)> EXEC #执行事务,监视值没有被中途修改,事务正常执行
1) (integer) 70
2) (integer) 30
模拟插队线程:
---------------主线程---------------
127.0.0.1:6379> WATCH money #监视money(上锁)
OK
127.0.0.1:6379> MULTI #开启事务
OK
127.0.0.1:6379(TX)> decrby money 20 #money-20
QUEUED
127.0.0.1:6379(TX)> incrby usedMoney 20 #usedMoney+20
QUEUED
127.0.0.1:6379(TX)> EXEC #执行事务
注意:这里还没有执行
当我们正要执行上述事务时,出现了下面的线程:
---------------插队线程---------------
127.0.0.1:6379> set money 500
OK
这时候我们再执行主线程的exec:
127.0.0.1:6379(TX)> EXEC #执行事务
(nil)
这是因为,执行这个事务之前,另一个线程,修改了我们的值
简单点说,当我们对watch money上锁以后,我们就会在执行的时候把监视时候的money和执行前的money进行对比,如果两个money的值不一样(被修改了)就会出现(nil)
当事务执行失败了,我们需要先解锁,再重新上锁:
127.0.0.1:6379> UNWATCH #解锁
OK
127.0.0.1:6379> WATCH money #获取最新的money值,再上锁
OK
127.0.0.1:6379> MULTI #开启事务
OK
127.0.0.1:6379(TX)> decrby money 20
QUEUED
127.0.0.1:6379(TX)> incrby usedMoney 20
QUEUED
127.0.0.1:6379(TX)> EXEC #执行事务时,会将现在的money和上锁时的money值进行对比,如果一样,那么执行成功,不一样则执行失败
1) (integer) 480
2) (integer) 20
watch指令在redis事物中提供了CAS的行为。为了检测被watch的keys在是否有多个客户
同时改变引起冲突,这些keys将会被监控。如果至少有一个被监控的key在执行exec命令
前被修改,整个事物将会回滚,不执行任何动作,从而保证原子性操作,并且执行exec会
得到null的回复。
这种形式的锁被称作乐观锁, 它是一种非常强大的锁机制。 并且因为大多数情况下, 不同的客户端会访问不同的键, 碰撞的情况一般都很少, 所以通常并不需要进行重试。
为什么需要持久化?
Redis对数据的操作都是基于内存的,当遇到了进程退出、服务器宕机等意外情况,如果没有持久化机制,那么Redis中的数据将会丢失无法恢复。有了持久化机制,Redis在下次重启时可以利用之前持久化的文件进行数据恢复。理解和掌握Redis的持久机制,对于Redis的日常开发和运维都有很大帮助,也是在大厂面试经常被问到的知识点。Redis支持的两种持久化机制:
RDB:把当前数据生成快照保存在硬盘上。
AOF:记录每次对数据的操作到硬盘上。
在指定的时间间隔内将内存中的数据集快照写入磁盘,这个过程就是RDB。 也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里。
Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。
整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能
如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。
Fork
:
1、 默认情况下, Redis 将数据库快照保存在名字为 dump.rdb的二进制文件中。文件名可以在配置文件(
redis.conf
)中进行自定义。
2、rdb文件的保存路径,也可以修改。默认为Redis启动时命令行所在的目录下
3、当Redis无法写入磁盘的话,直接关掉Redis的写操作。推荐yes
4、压缩文件,对于存储到磁盘中的快照,可以设置是否进行压缩存储。如果是的话,redis会采用LZF算法进行压缩。如果你不想消耗CPU来进行压缩的话,可以设置为关闭此功能。推荐yes.
5、检查完整性,在存储快照后,还可以让redis使用CRC64算法来进行数据校验,但是这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能,推荐yes
6、
save
格式:save 秒钟 写操作次数
RDB是整个内存的压缩过的Snapshot,RDB的数据结构,可以配置复合的快照触发条件,
默认是15分钟内改了1次,或5分钟内改了10次,或1分钟内改了1万次.
就是如果15分钟内改了1次,就做持久化操作,以此类推。
flushall命令
flushall 命令也会触发持久化(可以先将dump.rdb文件删除,然后再执行flushall命令,就会发现会重新生成新的dump.rdb)
命令
save
VSbgsave
save :save时只管保存,其它不管,全部阻塞。手动保存。不建议。
bgsave:Redis会在后台异步进行快照操作, 快照同时还可以响应客户端请求。 可以通过lastsave
命令获取最后一次成功执行快照的时间
如何恢复rdb文件:
只需要将rdb文件放在我们redis启动目录就可以,
redis启动的时候会自动检查dump.rdb恢复其中的数据 查看需要存在的位置
127.0.0.1:6379> config get dir
1) "dir"
2) "/usr/local/bin"
优点:
缺点:
以日志的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(读操作不记录), 只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作
因为AOF默认不开启,所以得在redis.conf中开启,还可以设置生成的文件名称,AOF文件的保存路径,同RDB的路径一致。
AOF和RDB同时开启,系统默认取AOF的数据(数据不会存在丢失)
AOF同步频率设置 :
appendfsync always
始终同步,每次Redis的写入都会立刻记入日志;性能较差但数据完整性比较好
appendfsync everysec
每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失。
appendfsync no
redis不主动进行同步,把同步时机交给操作系统。
AOF的备份机制和性能虽然和RDB不同, 但是备份和恢复的操作同RDB一样,都是拷贝备份文件,需要恢复时再拷贝到Redis工作目录下,启动系统即加载。
终端1:
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3 #写入三个key
OK
127.0.0.1:6379> shutdown #关闭redis
[root@localhost /]# ls #发现有appendonly.aof文件
appendonly.aof dev home media opt run sys var
bin dump.rdb lib mnt proc sbin tmp
boot etc lib64 myredis root srv usr
[root@localhost /]# cp appendonly.aof appendonly.aof.bak #复制一份appendonly.aof(这里复制的文件名随便取)
[root@localhost /]# rm -rf appendonly.aof #删除appendonly.aof
终端2
[root@localhost /]# mv appendonly.aof.bak appendonly.aof #将刚刚复制的文件改回来(appendonly.aof)
[root@localhost /]# redis-server /myredis/redis.conf #重启redis
[root@localhost /]# redis-cli
127.0.0.1:6379> keys * #查看之前存进去的key
1) "k3"
2) "k2"
3) "k1"
先将之前的aof文件修改一下,让其损坏(这里加了hello):
[root@localhost /]# redis-server /myredis/redis.conf #启动redis
[root@localhost /]# redis-cli
Could not connect to Redis at 127.0.0.1:6379: Connection refused #提示连接拒绝
not connected>
当启动redis的时候,会读取appendonly.aof文件,把文件中的内容进行加载。因为刚刚我对文件的内容进行的修改,文件就不对了,被我们玩坏了,就没法启动,告诉我们连接不了
在/usr/local/bin
中有一个redis-check-aof
[root@localhost /]# redis-check-aof --fix appendonly.aof #对appendonly.aof进行修复
0x bb: Expected prefix '*', got: 'h' #告诉我们哪里有问题
AOF analyzed: size=194, ok_up_to=187, ok_up_to_line=44, diff=7
This will shrink the AOF from 194 bytes, with 7 bytes, to 187 bytes
Continue? [y/N]: y #询问是否继续,y修复,n取消
Successfully truncated AOF #修复成功
再次查看appendonly.aof,会发现我们加的hello不见了
再去启动redis,就可以了
[root@localhost /]# redis-server /myredis/redis.conf
[root@localhost /]# redis-cli
127.0.0.1:6379> keys *
1) "k3"
2) "k2"
3) "k1"
127.0.0.1:6379>
什么是文件重写机制?
AOF采用文件追加方式,文件会越来越大为避免出现此种情况,新增了重写机制, 当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩, 只保留可以恢复数据的最小指令集.
相关设置
如果
no-appendfsync-on-rewrite yes
,不写入aof文件只写入缓存,用户请求不会阻塞,但是在这段时间如果宕机会丢失这段时间的缓存数据。(降低数据安全性,提高性能)
如果no-appendfsync-on-rewrite no
, 还是会把数据往磁盘里刷,但是遇到重写操作,可能会发生阻塞。(数据安全,但是性能降低)
auto-aof-rewrite-percentage
:设置重写的基准值,文件达到100%时开始重写(文件是原来重写后文件的2倍时触发)
auto-aof-rewrite-min-size
:设置重写的基准值,最小文件64MB。达到这个值开始重写。
实现原理
首先创建新的AOF文件是通过aof_rewrite函数来实现,但是这个函数会进行大量的写入操作,所以调用这个函数的线程将被长时间的阻塞,因为Redis服务器使用单线程来处理命令请求;所以如果直接是服务器进程调用AOF_REWRITE函数的话,那么重写AOF期间,服务器将无法处理客户端发送来的命令请求。(说白了,就是占用主线程,导致其他请求无法被处理)。
为了解决以上问题,那么Redis将AOF重写程序放到子进程(后台)里执行,这样子进程进行AOF重写期间,主进程可以继续处理命令请求;子进程带有主进程的数据副本,使用子进程而不是线程,可以避免在锁的情况下,保证数据的安全性。
那么问题又来了,使用子进程进行AOF重写的问题,子进程在进行AOF重写期间,服务器进程还要继续处理命令请求,而新的命令可能对现有的数据进行修改,这会让当前数据库的数据和重写后的AOF文件中的数据不一致。
为了解决这种数据不一致的问题,Redis增加了一个AOF重写缓存,这个缓存在fork出子进程之后开始启用,Redis服务器主进程在执行完写命令之后,会同时将这个写命令追加到AOF缓冲区和AOF重写缓冲区 。
重写流程
AOF文件重写的触发条件及触发方式
触发条件
没有BGSAVE命令(RDB持久化)/AOF持久化在执行;
没有BGREWRITEAOF在进行;
当前AOF文件大小要大于server.aof_rewrite_min_size(默认为1MB),或者在redis.conf配置了auto-aof-rewrite-min-size大小;
当前AOF文件大小和最后一次重写后的大小之间的比率等于或者等于指定的增长百分比(在配置文件设置了auto-aof-rewrite-percentage参数,不设置默认为100%)
如果前面三个条件都满足,并且当前AOF文件大小比最后一次AOF重写时的大小要大于指定的百分比,那么触发自动AOF重写。
触发方式
优劣
概念
主从复制:指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(Master/Leader),后者称为从节点(Slave/Follower),
数据的复制是单向的!只能由主节点复制到从节点(主节点以写为主、从节点以读为主)。
Redis的主从复制是异步复制,异步分为两个方面,一个是master服务器在将数据同步到slave时是异步的,因此master服务器在这里仍然可以接收其他请求,一个是slave在接收同步数据也是异步的
主从复制作用:
模拟主从复制:
这里我们开启三个redis,端口分别是6379、6380、6381
相关命令:
info replication
打印主从复制的相关信息
127.0.0.1:6379> info replication
# Replication
role:master #当前角色:主机
connected_slaves:0 #从服务器个数
master_failover_state:no-failover
master_replid:fe0de1410ce3e019021dcdd5810d339154fa5817
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
127.0.0.1:6380> info replication
# Replication
role:master
connected_slaves:0
master_failover_state:no-failover
master_replid:1da470d0eda7fbce2e2e1bc224bcbfbf67c1dde1
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
127.0.0.1:6381> info replication
# Replication
role:master
connected_slaves:0
master_failover_state:no-failover
master_replid:f6a329bac7c9d611484eb9dffc6cec7887e139a3
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
这时候会发现,三个端口都是主机,那么怎么设置从机呢?(6379为主机,6380和6381为从机)
这个时候我们只需要在终端上输入以下命令就能将其变成从机。配从(库)不配主(库)
slaveof
成为某个实例的从服务器(ip和port是主机的ip和端口号)
这个时候我们在主机(6379)中写入数据,在两个从机中读取数据
127.0.0.1:6379> set a1 v1 #主机写入数据
OK
127.0.0.1:6379> keys *
1) "a1"
127.0.0.1:6379> get a1
"v1"
127.0.0.1:6380> keys * #从机6380读取数据
1) "a1"
127.0.0.1:6381> keys * #从机6381读取数据
1) "a1"
我们这里是使用命令搭建,是暂时的,真实开发中应该在从机的配置文件中进行配置这样的话是永久的
可以把#去掉然后设置maserip和masterprot把配置写死。
复制原理
常用的三种“招”
“薪火相传”
上一个Slave可以是下一个slave的Master,Slave同样可以接收其他 slaves的连接和同步请求,那么该slave作为了链条中下一个的master, 可以有效减轻master的写压力,去中心化降低风险。
用 slaveof
中途变更转向:会清除之前的数据,重新建立拷贝最新的。风险是一旦某个slave宕机,后面的slave都没法备份。主机挂了,从机还是从机,无法写数据了。
“夺权篡位”
当一个主机宕机后,后面的从机可以立刻升为主机,其后面的从机不用做任何修改。
用 slaveof no one
将从机变为主机。
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();
}
}
什么是哨兵模式?
使用步骤
sentinel.conf
文件(名字不能错)#其中mymaster为监控对象起的服务器名称;主机ip地址;端口号; 1 为至少有多少个哨兵同意迁移的数量。
sentinel monitor mymaster 127.0.0.1 6379 1
启动哨兵
执行
redis-sentinel /myredis/sentinel.conf
这个时候将主机(6379)挂掉:
如图,这个时候6380变成了主机,而6379和6381变成了从机。并且就算6379重新启动,也无法变成之前的主机,依旧是从机。
首先判断优先级,在redis.conf中默认:
replica-priority 100
,值越小优先级越高。
例如:一个从机的是10 ,一个从机的是100,会优先选取10的做主机。
当优先级相同的时候,会比较偏移量。偏移量:是指获得原主机数据最全的。
例如:一个从机有主机的10个数据,而另一个只有9个,会优先选取有10个数据的从机做主机。
当前面两个条件都无法做出判定的时候,会选择runid最小的从服务器(从机)。
每个redis实例启动后都会随机生成一个40位的runid。
什么是集群?
Redis3.0加入了Redis的集群模式,实现了数据的分布式存储,对数据进行分片,将不同的数据存储在不同的master节点上面,从而解决了海量数据的存储问题。
Redis集群采用去中心化的思想,没有中心节点的说法,对于客户端来说,整个集群可以看成一个整体,可以连接任意一个节点进行操作,就像操作单一Redis实例一样,不需要任何代理中间件,当客户端操作的key没有分配到该node上时,Redis会返回转向指令,指向正确的node。
Redis也内置了高可用机制,支持N个master节点,每个master节点都可以挂载多个slave节点,当master节点挂掉时,集群会提升它的某个slave节点作为新的master节点。
集群的使用,运行机制及其优缺点