Redis
Redis
1. NoSQL介绍
NoSQL泛指非关系型数据库。它不依赖一无逻辑方式存储,而是以简单的key-value模式存储。大大增加了数据库的扩展能力。
特点:
- 不遵循SQL标准。
- 不支持ACID(原子性、一致性、隔离性、持久性)。
- 远超于SQL的性能。
适用于:
- 对数据高并发读写。
- 海量数据的读写。
- 对数据高可扩展性。
不适用于:
- 需要事务支持。
- 基于SQL的结构化存储,处理复杂的关系。
- 用不着SQL的和用了SQL也不行的情况,请考虑用NoSQL。
MongoDB:
- 高性能、开源、模式自由(schema free)的文档型数据库
- 数据都在内存中, 如果内存不足,把不常用的数据保存到硬盘
- 虽然是key-value模式,但是对value(尤其是json)提供了丰富的查询功能
- 支持二进制数据及大型对象
- 可以根据数据的特点替代RDBMS ,成为独立的数据库。或者配合RDBMS,存储特定的数据。
2. Redis安装
用docker进行安装配置
docker pull redis:6.2.6
启动redis:
docker start redis
docker exec -it redis redis-cli
关闭redis:shutdown
切换数据库:select 12
所有的数据库密码都相同
3. 常用五大数据类型
3.1 Redis键(key)
key值的操作:
keys *
查看当前库中所有keyset key value
设置key值与valueexists key
判断key是否存在,存在返回1,不存在返回0type key
查看key是什么类型del key
删除指定的key数据unlink key
根据value选择非阻塞删除,仅将keys从keyspace元数据中删除,真正的删除会在后续异步操作expire key 10
10秒钟:为给定的key设置过期时间ttl key
查看还有多少秒过期,-1表示永不过期,-2表示已过期
库的选择:
select
命令切换数据库dbsize
查看当前数据库的key数量flushdb
清空当前库flushall
通杀全部库
3.2 String字符串
- 一个key对应一个value
- 二进制安全的,即可包含任何数据
- value最多可以是512m
参数设置:
set key value
设置key值,当设置的值存在时,新设置的值会把原来的值覆盖掉get key
查询key值append key value
将给定的value追加到原值末尾strlen key
获取值的长度setnx key value
只有在key不存在的时候,设置key值,若设置的值存在,则不做任何事incr key
将key值存储的数字增1,只对数字值操作,如果为空,新增值为1decr key
将key值存储的数字减1,只对数字值操作,如果为空,新增值为1incrby/decrby key <步长>
将key值存储的数字增减如步长
原子操作:是指不会被线程调度机制打断的操作
这种操作一旦开始,就会一直运行到结束,中间不会有任何context switch(切换到另一个线程)。
Redis单命令的原子性主要得益于Redis的单线程
问题:Java中的i++是否是原子操作? 答:不是
问题:i=0,两个线程分别对i进行++100次,i值最后等于多少? 答:2~200
补充额外的字符串参数:
mset key value key value..
同时设置一个或者多个key-valuemget key key...
同时获取一个或多个valuemsetnx key value key value..
同时设置一个或者多个key-value,当且仅当所有给定key都不存在getrange key <起始位置> <结束位置>
获取key的起始位置和结束位置的值,类似于java中的substringsetrange key <起始位置> value
将value的值覆盖起始位置开始setex key <> value
设置键值的同时,设置过期时间getset key value
用新值换旧值
3.3 List列表
Redis列表是简单的字符串列表,按照插入顺序排序。
其底层其实是一个双向列表,对两端的操作性能很高,通过索引下标的操作中间的节点西能会较差。
lpush/rpush key value value...
从左或者右插入一个或者多个值(头插与尾插)lpop/rpop key
从左或者右吐出一个或者多个值(值在键在,值没键没)rpoplpush key1 key2
从key1列表右边吐出一个值,插入到key2的左边lrange key start stop
按照索引下标获取元素(从左到右)lrange key 0 -1
获取所有值lindex key index
按照索引下标获得元素llen key
获取列表长度linsert key before/after value newvalue
在value的后面/前面插入一个新值lrem key n value
从左边删除n个value值lset key index value
在列表key中的下标index中修改值value
List的数据结构为快速链表quickList
在列表元素较少的情况下会使用一块连续的内存存储,这个结构式ziplist,也即压缩列表。
但数据量比较多时才会改成quicklist。
3.4 Set集合
Set是可以自动排重的,并且是无序的
Redis的Set是String类型的无需集合。它的底层其实是一个value为null的hash表,所以添加、删除、查找的时间复杂度都是O(1)。
sadd key value value...
将一个或者多个member元素加入集合key中,已经存在的member元素被忽略smembers key
取出该集合的所有值sismember key value
判断该集合key是否存在该值scard key
返回该集合的元素个数srem key value value
删除集合中的某个元素spop key
随机从集合中取出一个元素,会从集合中删除该值srandmember key n
随即从该集合中取出n个值,不会从集合中删除smove <一个集合a><一个集合b>value
将一个集合a的某个value移动到另一个集合bsinter key1 key2
返回两个集合的交集元素sunion key1 key2
返回两个集合的并集元素sdiff key1 key2
返回两个集合的差集元素(key1有的,key2没有)
Set数据结构是dict字典,字典是用哈希表实现的。
Java中的HashSet的内部实现使用的就是HashMap,只不过所有的value都指向同一个对象。Redis的set结构也是一样,它内部也是用hash结构,所有的value都指向同一个内部值。
3.5 Hash哈希
Redis中的Hash是一个键值对集合。
它是一个String类型的field和value的映射表,hash特别适合用于存储对象。
类似Java中的Map<String,Object>
hset key field value
给key集合中的filed键赋值valuehget key1 field
集合field取出valuehmset key1 field1 value1 field2 value2
批量设置hash的值hexists key1 field
查看哈希表key中,给定域field是否存在hkeys key
列出该hash集合的所有fieldhvals key
列出该hash集合的所有valuehincrby key field increment
为哈希表key中的域field的值加上增量1 -1hsetnx key field value
将哈希表key中的域field的值设置为value,当且仅当域field不存在
Hash类型对应的数据结构有两种:ziplist、hashtable。当field-value的长度较短且个数较少时,使用ziplist,否则使用hashtable。
3.6 Zset有序集合
Redis有序集合Zset与普通集合set非常相似,是一个没有重复元素的字符串集合。
不同之处是有序集合的每个成员都关联了一个评分(score),这个评分被用来按照从最低到最高的方式来排序。集合中的成员是唯一的,但是评分是可以重复的。
zadd key score1 value1 score2 value2
将一个或多个member元素及其score值加入到有序key中zrange key start stop (withscores)
返回有序集key,下标在start与stop之间的元素,带withscores,可以让分数一起和值返回到结果集。zrangebyscore key min max(withscores)
返回有序集key,所有score值介于min和max之间(包括等于min或max)的成员。有序集成员按score的值递增次序排列zrevrangebyscore key max min (withscores)
同上,改为从大到小排列zincrby key increment value
为元素的score加上增量zrem key value
删除该集合下,指定值的元素zcount key min max
统计该集合,分数区间内的元素个数zrank key value
返回该值在集合中的排名,从0开始
zset底层使用了两个数据结构
- hash,hash的作用就是关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score。
- 跳跃表,跳跃表的目的在于给元素value排序,根据score的范围获取元素列表。
4. 配置文件详解
默认情况bind 127.0.0.1
只支持本地连接,可以注释掉。
protected-mode no
可以进行远程访问。
timeout 0
表示永不超时,以秒为单位。
tcp-keepalive 300
每隔300s检测一次是否还在操作,如果300秒都没操作,就释放连接。
daemonize yes
设置后台启动
pidfile
保存进程号的路径
设置密码
- 在redis.conf文件中加入
requirepass foobared
- 使用命令行
config get requirepass
,config set requirepass "123456"
设置之后每次一输入redis-cli进入的时候都要输入 密码才能访问。
5. Redis发布和订阅
Redis发布订阅是一种消息通信模式,redis客户端可以订阅任意数量的频道。
6. 新数据类型
6.1 Bitmaps
- 合理使用操作位可以有效提高内存使用率和开发使用率。
- 本身是一个字符串,不是数据类型,数组的每个单元只能放01,数组下标在Bitmaps叫做偏移量。
- 节省空间,一般存储活跃用户比较多。
setbit key offset value
设置值getbit key offset
取出值bitcount key
统计字符串中比特值为1的数量bitop and(or/not/xor) destkey key
符合操作,交并非异或,结果保存在destkey
6.2 HyperLogLog
- 统计网页中页面访问量
- 只会根据输入元素来计算基数,而不会存储输入元素本身,不能像集合那样,返回输入的各个元素
- 基数估计是在误差可接受的范围内,快速计算
pfadd key element
添加指定元素到hyperloglog中,成功则返回1,不成功返回0pfcount key
计算key的近似基数pfmerge destkey sourcekey sourcekey
一个或多个key合并后的结果存在destkey中
6.3 Geographic
提供经纬度设置,查询范围,距离查询等
geoadd key longitude latitude member
添加地理位置geopos key member
获取指定地区的坐标值geodist key member1 member2 (m km ft英尺 mi英里)
获取两个位置之间的直线距离georadius key longitude latitude radius (m km ft mi)
以给定的经纬度为中心,找出另一半径的内元素
7. Jedis操作
通过java操作redis
创建maven工程,引入依赖
1
2
3
4
5<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.7.1</version>
</dependency>创建jedis类
1
2
3
4
5
6
7
8
9
10
11public class JedisDemo1 {
public static void main(String[] args) {
//创建jedis对象
Jedis jedis = new Jedis("192.168.134.134",6379);
//测试
String ping = jedis.ping();
System.out.println(ping);
}
}可能出现的问题解决方法:
- 关闭linux的防火墙
- 在redis.conf配置文件中注释掉bind
- 设置保护模式为no
7.1 类型测试
测试key
1 |
|
测试String
1 |
|
测试List
1 |
|
测试Set
1 |
|
测试Hash
1 |
|
测试Zset
1 |
|
7.2 jedis实例-手机验证码
需求:
- 输入手机号,点击发送后随机生成6位数字码,2分钟内有效
- 输入验证码,点击验证,返回成功或失败
- 每个手机号每天只能输入三次
思路:
- 生成随机六位数字验证码:用Random
- 验证码在2分钟内有效:把验证码放到redis中,设置过期时间120秒
- 判断验证码是否一致:把redis中的验证码和输入的验证码比较,是否一致
- 每个号码每天只能输入三次:incr每次发送后+1,当大于2的时候,提交不能发送
代码:
1 | public class PhoneCode { |
8. SpringBoot整合Redis
引入依赖
1
2
3
4
5
6
7
8
9
10<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.0</version>
</dependency>application.properties中配置redis
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16#redis服务器地址
spring.redis.host=192.168.134.134
#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.lettcue.pool.max-idle=5
#连接池中最小空闲连接
spring.redis.lettuce.pool.min-idle=0创建redis配置类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class RedisConfig extends CachingConfigurerSupport {
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;
}
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;
}
}编写controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class RedisTestController {
private RedisTemplate redisTemplate;
public String testRedis(){
//设置值到redis中
redisTemplate.opsForValue().set("name","lucy");
//获取值
String name = (String)redisTemplate.opsForValue().get("name");
return name;
}
}
9. Redis事务
- 单独的隔离操作
- 事务中的所有命令都会序列化、按顺序执行
- 事务在执行过程中,不会被其他客户端送来的命令请求打断
从输入Multi命令开始,输入的命令都会一次进入命令队列中,但不会执行,直到输入Exec后,Redis会将之前的命令队列中的命令依次执行。
组队过程中可以通过discard来放弃组队。
事务的错误处理:
- 组队中某个命令出现了错误报告,执行时整个的所有队列都会被取消。
- 执行阶段某个命令出现了错误,则只有报错的命令不会被执行,其他命令都会执行,不会回滚。
9.1 悲观锁和乐观锁
- 悲观锁:不能同时进行多人,执行的时候先上锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁
- 乐观锁:通过版本号一致与否,即给数据加上版本,同步更新数据以及加上版本号。不会上锁,判断版本号,可以多人操作,类似生活中的抢票。每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的
在执行multi之前,执行命令watch key1 [key2]
,如果实物执行之前这个(或这些)key被其他命令所改动,那么事务被打断。
在事务1中:
1 | 127.0.0.1:6379> set balance 100 |
在事务2中:
1 | 127.0.0.1:6379> watch balance |
会发现在事务2中exec
命令执行后,显示失败,并没有执行加20的操作,这就是乐观锁
9.2 Redis事务三大特性:
- 单独的隔离操作,事务中的所有命令都会序列化,按照顺序来执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
- 没有隔离级别的概念,队列中的命令没有提交之前都不会实际被执行。
- 不保证原子性,事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚。
10. Redis持久化
具体Redis 提供了2个不同形式的持久化方式
- RDB(Redis DataBase)
- AOF(Append Of File)
10.1 RDB
在指定的时间间隔内
将内存中的数据集快照
写入磁盘
具体的备份流程如下:
Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到 一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。 整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能
- 如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。
- RDB的缺点是最后一次持久化后的数据可能丢失。
数据如果有变化的,会在/usr/local/bin目录下生成一个dum.rdb的文件
关于fork进程
Fork的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等) 数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程
- 在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,Linux中引入了“写时复制技术”
- 一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。
关于redis.conf配置文件的部分解释:
- save 3600
save 300 10
save 60 10000
大概意思如下:save 秒钟 写操作次数,60秒传10000次的写操作。
不设置save指令,或者给save传入空字符串
关于save和bgsave的比较:
save :save时只管保存,其它不管,全部阻塞。手动保存。不建议。
bgsave:Redis会在后台异步进行快照操作, 快照同时还可以响应客户端请求。
可以通过lastsave 命令获取最后一次成功执行快照的时间
- stop-writes-on-bgsave-error yes 关闭写入磁盘操作。比如当Redis无法写入磁盘的话,直接关掉Redis的写操作
- rdbcompression yes 对于存储到磁盘中的快照,可以设置是否进行压缩存储,如果你不想消耗CPU来进行压缩的话,可以设置为关闭此功能。推荐yes.
- rdbchecksum yes 增加数据校验,增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能
- dbfilename dump.rdb 在redis.conf中配置文件名称,默认为dump.rdb
- dir ./ 默认为Redis启动时命令行所在的目录下
具体rdb的备份
因为是临时文件,如果redis关闭之后,rdb的东西就会不见
所以通过mv 更改其名字之后mv dump.rdb d.rdb
在启动之前 又更改回来名字即可,mv d.rdb dump.rdb
(启动Redis, 备份数据会直接加载)
总结
优点:
- 适合大规模的数据恢复
- 对数据完整性和一致性要求不高更适合使用
- 节省磁盘空间
- 恢复速度快
缺点:
- Fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑
- 虽然Redis在fork时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能。
- 在备份周期在一定间隔时间做一次备份,所以如果Redis意外down掉的话,就会丢失最后一次快照后的所有修改。
10.2 AOF
以日志的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(读操作不记录), 只许追加文件但不可以改写文件
- redis启动之初会读取该文件重新构建数据,换言之,redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作
关于redis.conf配置文件的部分解释
AOF的备份机制和性能虽然和RDB不同, 但是备份和恢复的操作同RDB一样,都是拷贝备份文件,需要恢复时再拷贝到Redis工作目录下,启动系统即加载
默认是不开启AOF,开启RDB
可以在redis.conf中配置文件名称,默认为 appendonly.aof
AOF文件的保存路径,同RDB的路径一致
appendonly no
改为yes
插入其数据的时候,在日志里面会看到数据的添加
如果直接在日志添加一些无法识别的数据,启动redis会启动不了
可以通通过/usr/local/bin/redis-check-aof--fix appendonly.aof
进行恢复
在当前目录下有redis-check-aof
这个文件
关于Rewrite压缩
AOF采用文件追加方式,文件会越来越大为避免出现此种情况,新增了重写机制, 当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩, 只保留可以恢复数据的最小指令集.可以使用命令bgrewriteaof
重写机制:
AOF文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写临时文件最后再rename
redis4.0版本后的重写,是指上就是把rdb 的快照,以二级制的形式附在新的aof头部,作为已有的历史数据,替换掉原来的流水账操作
no-appendfsync-on-rewrite:
缓存,yes ,不写入aof文件只写入缓存,用户请求不会阻塞,但是在这段时间如果宕机会丢失这段时间的缓存数据。(降低数据安全性,提高性能)
磁盘,no,还是会把数据往磁盘里刷,但是遇到重写操作,可能会发生阻塞。(数据安全,但是性能降低)
什么时候重写
Redis会记录上次重写时的AOF大小,默认配置是当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发
重写虽然可以节约大量磁盘空间,减少恢复时间。但是每次重写还是有一定的负担的,因此设定Redis要满足一定条件才会进行重写。
auto-aof-rewrite-percentage
:设置重写的基准值,文件达到100%时开始重写(文件是原来重写后文件的2倍时触发)auto-aof-rewrite-min-size
:设置重写的基准值,最小文件64MB。达到这个值开始重写。
重写流程
(1)bgrewriteaof触发重写,判断是否当前有bgsave或bgrewriteaof在运行,如果有,则等待该命令结束后再继续执行。
(2)主进程fork出子进程执行重写操作,保证主进程不会阻塞。
(3)子进程遍历redis内存中数据到临时文件,客户端的写请求同时写入aof_buf缓冲区和aof_rewrite_buf重写缓冲区保证原AOF文件完整以及新AOF文件生成期间的新的数据修改动作不会丢失。
(4)1).子进程写完新的AOF文件后,向主进程发信号,父进程更新统计信息。2).主进程把aof_rewrite_buf中的数据写入到新的AOF文件。
(5)使用新的AOF文件覆盖旧的AOF文件,完成AOF重写
总结
优点:
- 备份机制更稳健,丢失数据概率更低
- 可读的日志文本,通过操作AOF稳健,可以处理误操作
缺点:
- 比起RDB占用更多的磁盘空间。
- 恢复备份速度要慢。
- 每次读写都同步的话,有一定的性能压力。
- 存在个别Bug,造成恢复不能
10.3 总结
RDB持久化方式能够在指定的时间间隔能对你的数据进行快照存储
AOF持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以redis协议追加保存每次写的操作到文件末尾.
Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大
只做缓存:如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化方式.
同时开启两种持久化方式
在这种情况下,当redis重启的时候会优先载入AOF文件来恢复原始的数据, 因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整.
RDB的数据不实时,同时使用两者时服务器重启也只会找AOF文件。那要不要只使用AOF呢?
答:建议不要,因为RDB更适合用于备份数据库(AOF在不断变化不好备份), 快速重启,而且不会有AOF可能潜在的bug,留着作为一个万一的手段。