目录
什么是NoSql
关系型数据库和非关系型数据的区别
Redis概念
启动与停止
redis可视化工具
数据类型
五种常用的数据类型
key命令
string
List
hash
set
zset
SpringBoot整合Redis
数据持久化
RDB
AOP
如何选用持久化机制:
集群配置
主从复制
哨兵模式
Cluster模式
企业级解决方案
redis脑裂
缓存预热
缓存穿透
缓存击穿
缓存雪崩
redis开发规范
数据一致性
NoSQL(NoSQL = Not Only SQL),意即“不仅仅是SQL”,泛指非关系型的数据库。随着互联网web2.0网站的兴起,传统的关系数据库在应付特别是超大规模和高并发类型纯动态网站已经显得力不从心,暴露了很多难以克服的问题。
结构化数据和非结构化数据
mysql、oracle等等这种数据库存储数据是结构化的,通过表结构来展现数据,这样对数据的展示更加清晰明了,但是这种以二维表的方式展示数据时,对于图片或者视频这类资源是不好展示的。而redis这类数据库比较灵活,而且速度更快,因为是将数据存储到内存中的,无需操作磁盘
1、KV型NoSql(代表----Redis)
KV型NoSql顾名思义就是以键值对形式存储的非关系型数据库,是最简单、最容易理解也是大家最熟悉的一种NoSql,因此比较快地带过。
特点:
注意:
KV型NoSql最大的优点就是高性能,利用Redis自带的BenchMark做基准测试,TPS可达到10万的级别,性能非常强劲。
关系型数据库
关系型数据库最典型的数据结构是表,由二维表及其之间的联系所组成的一个数据组织 优点:
非关系型数据库
优点:
缺点:
Redis默认端口号为6379
Redis提供了16个数据库,默认使用0号数据库,更改数据库使用select 几号
Redis的数据存储在内存中
下载Redis Desktop Manager
注意:
官网: RESP.app (formerly Redis Desktop Manager) - GUI for Redis ® available on Windows, macOS, iPad and Linux.
选择安装路径
连接Redis服务
关闭防火墙
systemctl stop firewalld.service
关闭保护模式
protected-mode no
开启远程访问
redis默认只允许本地访问,要使redis可以远程访问可以修改redis.conf。
注释掉bind 127.0.0.1 可以使所有的ip访问redis
配置连接服务
配置信息
Redis存储数据类似于map存储数据,可以看做不同的数据类型为不同的map,不同的map的存放逻辑不同
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
常用命令:
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
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
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
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
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
1、创建springboot项目,引入SpringDataRedis依赖
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.7.12
com.itbaizhan
redisblog
0.0.1-SNAPSHOT
redisblog
Demo project for Spring Boot
11
org.springframework.boot
spring-boot-starter-data-redis
org.springframework.boot
spring-boot-starter-test
test
io.projectreactor
reactor-test
test
org.springframework.boot
spring-boot-maven-plugin
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是什么
在指定的时间间隔内将内存的数据集快照写入磁盘,也就是行话讲的快照,它恢复时是将快照文件直接读到内存里。
注意:
这种格式是经过压缩的二进制文件。
配置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配置
快照默认配置:
配置新的保存规则
给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持久化的命令有两种:
高级配置
stop-writes-on-bgsave-error
默认值是yes。当Redis无法写入磁盘的话,直接关闭Redis的写操作。
rdbcompression
默认值是yes。对于存储到磁盘中的快照,可以设置是否进行压缩存储。如果是的话,redis会采用LZF算法进行压缩。如果你不想消耗CPU来进行压缩的话,可以设置为关闭此功能,但是存储在磁盘上的快照会比较大。
rdbchecksum
默认值是yes。在存储快照后,我们还可以让redis使用CRC64算法来进行数据校验,但是这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能。
恢复数据
只需要将rdb文件放在Redis的启动目录,Redis启动时会自动加载dump.rdb并恢复数据。
优势
劣势
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同步频率设置
参数:
始终同步,每次Redis的写入都会立刻记入日志,性能较差但数据完整性比较好。
每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失。
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
参数:
配置了这两个参数:如果发生脑裂原Master会在客户端写入操作的时候拒绝请求。这样可以避免大量数据丢失。
当我们项目启动的时候,redis服务器刚刚启动,还没有缓存,此时如果并发量上来了,大量的请求发送到了数据库,数据库很有可能会发生崩溃
解决办法:所以我们可以再redis服务器启动之后将一些数据提前缓存进去,这样子可以防止并发量突然提升导致数据库崩溃
缓存穿透是指缓存和数据库中不存在的数据,用户不断发送请求,如不断发送id为-1的数据,这种不存在的数据,首先会查询缓存,缓存中没有,然后再访问数据库,数据库也没有,然后才返回查询不到。此时如果查询一次还好,但是如果用户不断发送请求,那么这时的用户就是攻击者,攻击会导致数据库压力过大。
解决办法:
1、将不存在的数据缓存
2、布隆过滤器:如果想判断一个元素是不是在一个集合里,一般想到的是将集合中所有元素保存起来,然后通过比较确定。
什么是布隆过滤器
布隆过滤器是一种数据结构,比较巧妙的概率型数据结构(probabilistic data structure),特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”。
注意:
布隆说不存在一定不存在,布隆说存在你要小心了,它有可能不存在。
代码实现
引入hutool包
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过期
解决方案
加锁排队代码如下:
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;
}
}
注意:
加锁排队只是为了减轻数据库的压力,并没有提高系统吞吐量。
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)更短,这一情形很难出现。