目录
什么是NoSql
NoSQL(NoSQL = Not Only SQL),意即“不仅仅是SQL”,泛指非关系型的数据库。随着互联网web2.0网站的兴起,传统的关系数据库在应付特别是超大规模和高并发类型纯动态网站已经显得力不从心,暴露了很多难以克服的问题。
结构化数据和非结构化数据
- 结构化数据指的是由二维表结构来逻辑表达和实现的数据,严格遵循数据格式与长度规范,也称作为行数据。
- 非结构化数据,指的是数据结构不规则或不完整,没有任何预定义的数据模型,不方便用二维逻辑表来表现的数据,例如办公文档(Word)、文本、图片、HTML、各类报表、视频音频等。
mysql、oracle等等这种数据库存储数据是结构化的,通过表结构来展现数据,这样对数据的展示更加清晰明了,但是这种以二维表的方式展示数据时,对于图片或者视频这类资源是不好展示的。而redis这类数据库比较灵活,而且速度更快,因为是将数据存储到内存中的,无需操作磁盘
1、KV型NoSql(代表----Redis)
KV型NoSql顾名思义就是以键值对形式存储的非关系型数据库,是最简单、最容易理解也是大家最熟悉的一种NoSql,因此比较快地带过。
特点:
- 数据基于内存,读写效率高
- KV型数据,时间复杂度为O(1),查询速度快
注意:
KV型NoSql最大的优点就是高性能,利用Redis自带的BenchMark做基准测试,TPS可达到10万的级别,性能非常强劲。
关系型数据库和非关系型数据的区别
关系型数据库
关系型数据库最典型的数据结构是表,由二维表及其之间的联系所组成的一个数据组织 优点:
- 易于维护:都是使用表结构,格式一致;
- 使用方便:SQL语言通用,可用于复杂查询;
- 复杂操作:支持SQL,可用于一个表以及多个表之间非常复杂的查询。 缺点:
- 读写性能比较差,尤其是海量数据的高效率读写;
- 固定的表结构,灵活度稍欠;
非关系型数据库
优点:
- 格式灵活:存储数据的格式可以是key,value形式、文档形式、图片形式等等,文档形式、图片形式等等,使用灵活,应用场景广泛,而关系型数据库则只支持基础类型。
- 速度快:nosql可以使用硬盘或者随机存储器作为载体,而关系型数据库只能使用硬盘;
- 高扩展性;
- 成本低:nosql数据库部署简单,基本都是开源软件。
缺点:
- 不提供sql支持,学习和使用成本较高;
- 无事务处理;
- 数据结构相对复杂,复杂查询方面稍欠。
Redis概念
Redis默认端口号为6379
Redis提供了16个数据库,默认使用0号数据库,更改数据库使用select 几号
Redis的数据存储在内存中
启动与停止
redis可视化工具
下载Redis Desktop Manager
注意:
选择安装路径
连接Redis服务
关闭防火墙
systemctl stop firewalld.service
关闭保护模式
protected-mode no
开启远程访问
redis默认只允许本地访问,要使redis可以远程访问可以修改redis.conf。
注释掉bind 127.0.0.1 可以使所有的ip访问redis
配置连接服务
配置信息
数据类型
五种常用的数据类型
Redis存储数据类似于map存储数据,可以看做不同的数据类型为不同的map,不同的map的存放逻辑不同
key命令
1、keys
作用:查看当前库中所有的key
语法结构:keys 通配符
有三种通配符:* ,?,[]
- *:代表所有字符
- ?:匹配一个字符
- []:匹配括号内的某个字符
2、type
作用:查看指定key的类型
语法规则:type key
3、exists
作用:查看指定key是否存在
语法规则:exists key
4、del
作用:删除指定key
语法规则:del key
5、expire
作用:指定某个key的过期时间
语法规则:expire key 时间
6、ttl
作用:查找指定key的过期时间
语法规则:ttl key
string
常用命令:
SET key value | 设置指定key的值 |
GET key | 获取指定key的值 |
SETEX key seconds value | 设置指定key的值,并设置key的存活时间(单位为秒s) |
SETNX key value | 只有在key不存在时设置key的值(如果key已存在则无法添加) |
APPEND key value | 将给定的value追加到原来的value的末尾 |
STRLEN key | 获取指定key的value长度 |
GETRANGE key 开始索引 结束索引 | 获取指定索引范围的值 |
SETRANGE key 索引 value | 设置指定索引范围的值 |
INCR key | 将key中的数字+1 |
DECR key | 将key中的数字- 1 |
MSET key1 value1 key2 value2 | 同时设置多个key-value |
MGET key1 key2 | 同时获取多个key的值 |
GETSET key value | 将给定key值设为value,并返回key的旧值,简单来说就是:先get后set |
常用示例:
1、set
2、get
3、setex
4、setnx
如果key已经存在则无法设置,返回值为0就是设置失败
5、append
6、strlen
7、incr key
8、decr key
List
Redis列表是类似双向链表的结构,按照插入顺序排序,
常用命令:
LPUSH key value [..values] | 将一个或多个值插入到列表头部 |
LRANGE key start stop | 获取列表指定范围内的元素(lrange key 0 -1获取所有元素) |
RPOP key | 移除并获取列表最后一个元素 |
LPOP key | 移除并获取列表第一个元素 |
LLEN key | 获取列表的长度 |
BRPOP key1 [..keys] timeout | 移除并获取列表最后一个元素,如果列表没有元素会堵塞列表直到等待超时或发现可弹出元素位置 |
LINDEX key index | 获取指定index位置的值 |
LREM key count value | 移除列表中count个的value |
Linsert key before/after value newvalue | 在列表中value值的前边/后边插入一个newvalue |
Lset value | 将指定索引的值设置为value |
常用示例:
1、lpush
2、rpop
3、lrem
4、lrange
hash
Redis中hash的常用命令:
HSET key field value | 将哈希表key中的字段field的值设置为value |
HGET key field | 获取存储在哈希表中的指定字段的值 |
HDEL key field | 删除存储在哈希表中指定字段 |
HKEYS key | 获取哈希表中指定key的所有字段 |
HVALS key | 获取哈希表中指定key的所有值 |
HGETALL key | 获取在哈希表中指定字段的key的所有字段和值 |
HEXISTS key field
| 判断指定key中是否存在field |
HSETNX key field value | 给哈希表中不存在的字段赋值 |
示例:
1、hset
2、hget
3、hkeys
4、hvals
5、hdel
6、hgetall
set
Redis set是string类型的无序集合。集合成员是唯一的,集合中不能出现重复的数据:
SADD key member1 [..members] | 向集合中添加一个或多个成员 |
SMEMBERS key | 返回集合中的所有成员 |
SCARD key | 返回集合中的成员数 |
SINTER key1 [..keys] | 返回给定所有集合的交集 |
SUNION key1 [..keys] | 返回给定所有集合的并集 |
SDIFF key1 [..keys] | 返回给定所有集合的差集(key1 - key2……) |
SREM key member1 [..members] | 删除指定集合的一个或多个成员 |
SISMEMBER key value | 寻找指定key的集合中是否有指定value |
示例:
1、sadd
2、smembers
3、scard
4、sinter
5、sunion
6、sdiff
7、srem
zset
Redis sorted set 有序集合是string类型元素的集合,且不重复的成员。每个元素都会关联一个double类型的分数(score)。redis正是通过分数来为集合中的成员进行从小到大排序。有序集合成员是惟一的,但分数却可以重复
ZADD key score1 member1 [score2 member2…] | 向有序集合中添加一个或多个成员,或者更新已存在成员的分数 |
ZRANGE key start stop [WITHSCORES] | 返回有序集合指定区间的成员 |
ZINCRBY key increment member | 对指定成员的分数增加increment |
ZREM key member [..members] | 移除有序集合中的一个或多个成员 |
ZCOUN key minscore maxscore | 统计该集合在minscore和maxscore分数区间中元素的个数 |
ZRANK key value | 返回value在集合中的排名,从0开始 |
示例:
1、zadd
2、zrange
3、zincrby
4、zrem
SpringBoot整合Redis
1、创建springboot项目,引入SpringDataRedis依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.12</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.itbaizhan</groupId>
<artifactId>redisblog</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>redisblog</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<!--springdataredis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2、配置redis,编写yml文件
spring:
data:
redis:
host: 192.168.138.102 #redis服务器的ip
port: 6379 #redis服务器的端口
database: 0 #所使用的数据库
3、编写测试类
package com.itbaizhan.redisblog2;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import javax.annotation.Resource;
@SpringBootTest
public class RedisTest {
@Autowired
@Resource
//redis通过RedisTemplate操作对象操作redis
private RedisTemplate redisTemplate;
@Test
public void t1(){
String key = "k1";
ValueOperations ops = redisTemplate.opsForValue();
ops.set(key,"v1");
String value = (String) ops.get(key);
System.out.println(key+":"+value);
}
}
4、运行测试类
数据持久化
RDB
RDB是什么
在指定的时间间隔内将内存的数据集快照写入磁盘,也就是行话讲的快照,它恢复时是将快照文件直接读到内存里。
注意:
这种格式是经过压缩的二进制文件。
配置dump.rdb文件
RDB保存的文件,在redis.conf中配置文件名称,默认为dump.rdb。
439
440 # The filename where to dump the DB
441 dbfilename dump.rdb
442
rdb文件的保存位置,也可以修改。默认在Redis启动时命令行所在的目录下。
rdb文件的保存路径,也可以修改。默认为Redis启动时命令行所在的目录下
dir ./
触发机制-主要三种方式
RDB配置
快照默认配置:
- save 3600 1:表示3600秒内(一小时)如果至少有1个key的值变化,则保存。
- save 300 100:表示300秒内(五分钟)如果至少有100个 key 的值变化,则保存。
- save 60 10000:表示60秒内如果至少有 10000个key的值变化,则保存。
配置新的保存规则
给redis.conf添加新的快照策略,30秒内如果有5次key的变化,则触发快照。配置修改后,需要重启Redis服务。
save 3600 1
save 300 100
save 60 10000
save 30 5
flushall
执行flushall命令,也会触发rdb规则。
save与bgsave
手动触发Redis进行RDB持久化的命令有两种:
- save
该命令会阻塞当前Redis服务器,执行save命令期间,Redis不能处理其他命令,直到RDB过程完成为止,不建议使用。 - bgsave
执行该命令时,Redis会在后台异步进行快照操作,快照同时还可以响应客户端请求。
高级配置
stop-writes-on-bgsave-error
默认值是yes。当Redis无法写入磁盘的话,直接关闭Redis的写操作。
rdbcompression
默认值是yes。对于存储到磁盘中的快照,可以设置是否进行压缩存储。如果是的话,redis会采用LZF算法进行压缩。如果你不想消耗CPU来进行压缩的话,可以设置为关闭此功能,但是存储在磁盘上的快照会比较大。
rdbchecksum
默认值是yes。在存储快照后,我们还可以让redis使用CRC64算法来进行数据校验,但是这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能。
恢复数据
只需要将rdb文件放在Redis的启动目录,Redis启动时会自动加载dump.rdb并恢复数据。
优势
- 适合大规模的数据恢复
- 对数据完整性和一致性要求不高更适合使用
- 节省磁盘空间
- 恢复速度快
劣势
- 在备份周期在一定间隔时间做一次备份,所以如果Redis意外down掉的话,就会丢失最后一次快照后的所有修改。
AOP
AOF是什么
以日志的形式来记录每个写操作,将Redis执行过的所有写指令记录下来。
AOF默认不开启
可以在redis.conf中配置文件名称,默认为appendonly.aof。
注意:
AOF文件的保存路径,同RDB的路径一致,如果AOF和RDB同时启动,Redis默认读取AOF的数据。
AOF启动/修复/恢复
开启AOF
设置Yes:修改默认的appendonly no,改为yes
appendonly yes
注意:
修改完需要重启redis服务。
设置数据。
set k11 v11
set k12 v12
set k13 v13
set k14 v14
set k15 v15
AOF同步频率设置
参数:
- appendfsync always
始终同步,每次Redis的写入都会立刻记入日志,性能较差但数据完整性比较好。
- appendfsync everysec
每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失。
- appendfsync no
redis不主动进行同步,把同步时机交给操作系统。
优势
- 备份机制更稳健,丢失数据概率更低。
- 可读的日志文本,通过操作AOF稳健,可以处理误操作。
劣势
- 比起RDB占用更多的磁盘空间。
- 恢复备份速度要慢。
- 每次读写都同步的话,有一定的性能压力。
如何选用持久化机制:
不要仅仅使用RDB
RDB数据快照文件,都是每隔5分钟,或者更长时间生成一次,这个时候就得接受一旦redis进程宕机,那么会丢失最近5分钟的数据。
也不要仅仅使用AOF
综合使用AOF和RDB两种持久化机制
用AOF来保证数据不丢失,作为数据恢复的第一选择,用RDB来做不同程度的冷备,在AOF文件都丢失或损坏不可用的时候,还可以使用RDB来进行快速的数据恢复。
- 你通过AOF做冷备,没有RDB做冷备,来的恢复速度更快。
- RDB每次简单粗暴生成数据快照,更加健壮,可以避免AOF这种复杂的备份和恢复机制的bug。
集群配置
主从复制
哨兵模式
Cluster模式
企业级解决方案
redis脑裂
Redis的集群脑裂是指因为网络问题,导致Redis Master节点跟Redis slave节点和哨兵集群处于不同的网络分区,此时因为哨兵集群无法感知到master的存在,所以将slave节点提升为master节点。
注意:
此时存在两个不同的master节点,就像一个大脑分裂成了两个。集群脑裂问题中,如果客户端还在基于原来的master节点继续写入数据,那么新的Master节点将无法同步这些数据,当网络问题解决之后,sentinel集群将原先的Master节点降为slave节点,此时再从新的master中同步数据,将会造成大量的数据丢失。
解决方案
redis.conf配置参数:
min-replicas-to-write 1
min-replicas-max-lag 5
参数:
- 第一个参数表示最少的slave节点为1个
- 第二个参数表示数据复制和同步的延迟不能超过5秒
配置了这两个参数:如果发生脑裂原Master会在客户端写入操作的时候拒绝请求。这样可以避免大量数据丢失。
缓存预热
当我们项目启动的时候,redis服务器刚刚启动,还没有缓存,此时如果并发量上来了,大量的请求发送到了数据库,数据库很有可能会发生崩溃
解决办法:所以我们可以再redis服务器启动之后将一些数据提前缓存进去,这样子可以防止并发量突然提升导致数据库崩溃
缓存穿透
缓存穿透是指缓存和数据库中不存在的数据,用户不断发送请求,如不断发送id为-1的数据,这种不存在的数据,首先会查询缓存,缓存中没有,然后再访问数据库,数据库也没有,然后才返回查询不到。此时如果查询一次还好,但是如果用户不断发送请求,那么这时的用户就是攻击者,攻击会导致数据库压力过大。
解决办法:
1、将不存在的数据缓存
2、布隆过滤器:如果想判断一个元素是不是在一个集合里,一般想到的是将集合中所有元素保存起来,然后通过比较确定。
什么是布隆过滤器
布隆过滤器是一种数据结构,比较巧妙的概率型数据结构(probabilistic data structure),特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”。
注意:
布隆说不存在一定不存在,布隆说存在你要小心了,它有可能不存在。
代码实现
引入hutool包
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.17</version>
</dependency>
java代码实现
// 初始化 注意 构造方法的参数大小10 决定了布隆过滤器BitMap的大小
BitMapBloomFilterfilter=newBitMapBloomFilter(10);
filter.add("123");
filter.add("abc");
filter.add("ddd");
booleanabc=filter.contains("abc");
System.out.println(abc);
缓存击穿
缓存击穿指的是某个key突然过期了,在缓存过期的一瞬间,同时有大量的请求打进来,由于此时缓存已经过期了,大量请求发送到数据库,造成数据库压力瞬增,甚至可能导致数据库崩溃
解决办法:
1、互斥锁:当一个请求拿到锁之后,其他请求在外等待,该请求查询缓存发现缓存已经过期,所以访问数据库,查询到对应数据之后将数据缓存从,此时其他请求拿到了锁,查询缓存就可以获取到对应数据了
2、缓存没有过期时间
缓存雪崩
缓存雪崩是缓存击穿的升级版,缓存击穿是一个key过期,缓存雪崩是大量key过期
解决方案
- 过期时间打散:既然是大量缓存集中失效,那最容易想到就是让他们不集中生效。可以给缓存的过期时间时加上一个随机值时间,使得每个 key 的过期时间分布开来,不会集中在同一时刻失效。
- 热点数据不过期:该方式和缓存击穿一样,也是要着重考虑刷新的时间间隔和数据异常如何处理的情况。
- 加互斥锁: 该方式和缓存击穿一样,按 key 维度加锁,对于同一个 key,只允许一个线程去计算,其他线程原地阻塞等待第一个线程的计算结果,然后直接走缓存即可。
加锁排队代码如下:
public Object GetProductListNew(String cacheKey) {
int cacheTime = 30;
//1,加锁的时候,为什么不可以直接给key加锁,还设置一个加锁的key(lockkey)?
//回答:为了代码的阅读性,所以加了一个变量,直接使用也没有问题
String lockKey = cacheKey;
// 获取key的缓存
String cacheValue = jedis.get(cacheKey);
// 缓存未失效返回缓存
if (cacheValue != null) {
return cacheValue;
} else {
// 枷锁
synchronized(lockKey) {
//2,为什么在一开始,就有在缓存里去数据,如果没有在缓存中取到数据,里面为什么还要在获取一次?
//回答:在多线程的情况下,所有的线程对这个方法进行访问,当value为空的时候,某一条线程会抢占到锁资源,此刻需要重新去拿去值,因为在锁之前,会导致线程的并发问题,所以在锁之内通过自己的key获取value,是为了增加数据的安全性
// 获取key的value值
cacheValue = jedis.get(cacheKey);
//3,如果说代码中锁的部分把数据重新加入到缓存中,在我的理解中,所有的线程应该是都进入到了那个if。。。else。。。中的else部分,一个线程进去,其他线程阻塞。锁中缓存加入成功,那么其他线程是怎么从缓存数据里取的呢?在重新走一遍代码吗?
//回答:在同一时刻进入改方法的请求,也会进锁之内,如果缓存中有key对应的value,则会在缓存中拿去
if (cacheValue != null) {
return cacheValue;
} else {
//这里一般是sql查询数据
// db.set(key)
// 添加缓存
jedis.set(cacheKey,"");
}
}
return cacheValue;
}
}
注意:
加锁排队只是为了减轻数据库的压力,并没有提高系统吞吐量。
redis开发规范
key设计技巧(每一段之间通过冒号隔开)
• 1、把表名转换为key前缀,如tag:
• 2、把第二段放置用于区分key的字段,对应msyql中主键的列名,如user_id
• 3、第三段放置主键值,如2,3,4
• 4、第四段写存储的列名
user_id name age
1 baizhan 18
2 itbaizhan 20
示例
# 表名 主键 主键值 存储列名字
set user:user_id:1:name baizhan
set user:user_id:1:age 20
#查询这个用户
keys user:user_id:9*
这种设计技巧可以使redis可视化工具更好的展示数据,可视化工具会自动根据冒号分隔:
value设计
拒绝bigkey
防止网卡流量、慢查询,string类型控制在10KB以内,hash、list、set、zset元素个数不要超过5000。
命令使用
1、禁用命令
禁止线上使用keys、flushall、flushdb等,通过redis的rename机制禁掉命令,或者使用scan的方式渐进式处理。
2、合理使用select
redis的多数据库较弱,使用数字进行区分,很多客户端支持较差,同时多业务用多数据库实际还是单线程处理,会有干扰。
3、使用批量操作提高效率
• 原生命令:例如mget、mset。
• 非原生命令:可以使用pipeline提高效率。
注意:
但要注意控制一次批量操作的元素个数(例如500以内,实际也和元素字节数有关)。
4、不建议过多使用Redis事务功能
Redis的事务功能较弱(不支持回滚),而且集群版本(自研和官方)要求一次事务操作的key必须在一个slot上。
客户端使用
1. Jedis : https://github.com/xetorthio/jedis 重点推荐
2. Spring Data redis : https://github.com/spring-projects/spring-data-redis 使用Spring框架时推荐
3. Redisson : https://github.com/mrniko/redisson 分布式锁、阻塞队列的时重点推荐
1、避免多个应用使用一个Redis实例
不相干的业务拆分,公共数据做服务化。
2、使用连接池
可以有效控制连接,同时提高效率,标准使用方式:
执行命令如下:
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
//具体的命令
jedis.executeCommand()
} catch (Exception e) {
logger.error("op key {} error: " + e.getMessage(), key, e);
} finally {
//注意这里不是关闭连接,在JedisPool模式下,Jedis会被归还给资源池。
if (jedis != null)
jedis.close();
}
数据一致性
缓存已经在项目中被广泛使用,在读取缓存方面,大家没啥疑问,都是按照下图的流程来进行业务操作。
缓存说明:
从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。
三种更新策略
- 先更新数据库,再更新缓存
- 先删除缓存,再更新数据库
- 先更新数据库,再删除缓存
1、先更新数据库,再更新缓存
这套方案,大家是普遍反对的。为什么呢?
线程安全角度
同时有请求A和请求B进行更新操作,那么会出现
(1)线程A更新了数据库 (2)线程B更新了数据库 (3)线程B更新了缓存 (4)线程A更新了缓存
这就出现请求A更新缓存应该比请求B更新缓存早才对,但是因为网络等原因,B却比A更早更新了缓存。这就导致了脏数据,因此不考虑。
2、先删缓存,再更新数据库
该方案会导致不一致的原因是。同时有一个请求A进行更新操作,另一个请求B进行查询操作。那么会出现如下情形:
(1)请求A进行写操作,删除缓存 (2)请求B查询发现缓存不存在 (3)请求B去数据库查询得到旧值 (4)请求B将旧值写入缓存 (5)请求A将新值写入数据库
注意:
该数据永远都是脏数据。
3、先更新数据库,再延时删缓存
这种情况存在并发问题吗?
(1)缓存刚好失效
(2)请求A查询数据库,得一个旧值
(3)请求B将新值写入数据库
(4)请求B删除缓存
(5)请求A将查到的旧值写入缓存
发生这种情况的概率又有多少?
发生上述情况有一个先天性条件,就是步骤(3)的写数据库操作比步骤(2)的读数据库操作耗时更短,才有可能使得步骤(4)先于步骤(5)。可是,大家想想,数据库的读操作的速度远快于写操作的,因此步骤(3)耗时比步骤(2)更短,这一情形很难出现。