Redis数据库
MySQL的缓存系统就可以胜任大部分的数据存储工作。但是MySQL的数据始终是存储在硬盘上的,如果是快速更新或是频繁使用的数据,MySQL的磁盘IO读写性能完全不能满足上面的需求,能够满足上述需求的只有内存,因为速度远高于磁盘IO。
NoSQL概论
NoSQL全称是Not Only SQL(不仅仅是SQL)它是一种非关系型数据库
特点:
- 不保证关系数据的ACID特性
- 并不遵循SQL标准
- 消除数据之间关联性
它的优势:
- 远超传统关系型数据库的性能
- 非常易于扩展
- 数据模型更加灵活
- 高可用
NoSQL数据库分为以下几种:
- **键值存储数据库:**所有的数据都是以键值方式存储的,类似于我们之前学过的HashMap,使用起来非常简单方便,性能也非常高。
- **列存储数据库:**这部分数据库通常是用来应对分布式存储的海量数据。键仍然存在,但是它们的特点是指向了多个列。
- **文档型数据库:**它是以一种特定的文档格式存储数据,比如JSON格式,在处理网页等复杂数据时,文档型数据库比传统键值数据库的查询效率更高。
- **图形数据库:**利用类似于图的数据结构存储数据,结合图相关算法实现高速访问。
Redis数据库,就是一个开源的键值存储数据库,所有的数据全部存放在内存中,它的性能大大高于磁盘IO,并且它也可以支持数据持久化,他还支持横向扩展、主从复制等。
基本操作
在Redis下,数据库是由一个整数索引标识,而不是由一个数据库名称。
默认情况下,我们连接Redis数据库之后,会使用0号数据库,可以通过Redis配置文件中的参数来修改数据库总数,默认为16个。
select
语句进行切换数据库:
select 序号;
数据操作
代码语言:javascript复制#添加数据
set <key> <value>
-- 一次性多个
mset [<key> <value>]...
# 通过键值获取存入的值
get <key>
#存入时设置过期时间
set <key> <value> EX 秒
set <key> <value> PX 毫秒
#添加后进行设置
expire <key> 秒
#查看过期时间
ttl <key>
-- 毫秒显示
pttl <key>
-- 转换为永久
persist <key>
#删除数据(一个或者多个)
del <key>...
#查看数据库所有的键值
keys *
#查看键值是否存在
exists <key>...
#随机拿取键值
randomkey
#移动数据到另一个数据库
move <key> 数据库序号
#修改键值
rename <key> <新的名字>
-- 下面这个会检查新的名称是否已经存在
renamex <key> <新的名称>
#对数字数据进行自增自减
-- 等价于a = a 1
incr <key>
-- 等价于a = a b
incrby <key> b
-- 等价于a = a - 1
decr <key>
-- 等价于a = a - b
decrby <key> b
#查看数据类型
type <key>
数据类型
键值对除了存储一个String类型的值以外,还支持多种常用的数据类型
Hash
类型本质上就是一个HashMap像这样:
代码语言:javascript复制#Redis默认存String类似于这样:
Map<String, String> hash = new HashMap<>();
#Redis存Hash类型的数据类似于这样:
Map<String, Map<String, String>> hash = new HashMap<>();
键是hashmap的名称,值是一个map,可以在此Map中放入类的各种属性和值
操作:
代码语言:javascript复制#添加数据
hset <key> [<字段> <值>]...
#获取数据
hget <key> <字段>
#获取所有字段
hgetall <key>
#判断字段是否存在
hexists <key> <字段>
#删除字段
hdel <key>
#获取所有字段的值
hvals <key>
#获取键值对数据个数
hlen <key>
List
List类型,是一个列表,而列表中存放一系列的字符串,它支持随机访问,支持双端操作,就像我们使用Java中的LinkedList一样。
操作:
代码语言:javascript复制-- 向列表头部添加元素
lpush <key> <element>...
-- 向列表尾部添加元素
rpush <key> <element>...
-- 在指定元素前面/后面插入元素
linsert <key> before/after <指定元素> <element>
#获取数据
-- 根据下标获取元素
lindex <key> <下标>
-- 获取并移除头部元素
lpop <key>
-- 获取并移除尾部元素
rpop <key>
-- 获取指定范围内的
lrange <key> start stop
-- 获取列表a中的全部元素
lrange a 0 -1
-- 从前一个数组的最后取一个数出来放到另一个数组的头部,并返回元素
rpoplpush 当前数组 目标数组
-- 如果列表中没有元素,那么就等待,如果指定时间(秒)内被添加了数据,那么就执行pop操作,如果超时就作废,支持同时等待多个列表,只要其中一个列表有元素了,那么就能执行
blpop <key>... timeout
Set和SortedSet
Set集合其实就像Java中的HashSet一样,不允许出现重复元素,不支持随机访问,但是能够利用Hash表提供极高的查找效率。
set操作:
代码语言:javascript复制#添加一个或多个值
sadd <key> <value>...
#查看Set集合中有多少个值
scard <key>
-- 是否包含指定值
sismember <key> <value>
-- 列出所有值
smembers <key>
-- 集合之间的差集
sdiff <key1> <key2>
-- 集合之间的交集
sinter <key1> <key2>
-- 求并集
sunion <key1> <key2>
-- 将集合之间的差集存到目标集合中
sdiffstore 目标 <key1> <key2>
-- 同上
sinterstore 目标 <key1> <key2>
-- 同上
sunionstore 目标 <key1> <key2>
#移动指定值到另一个集合中
smove <key> 目标 value
-- 随机移除一个幸运儿
spop <key>
-- 移除指定
srem <key> <value>...
SortedSet,它支持我们为每个值设定一个分数,分数的大小决定了值的位置,所以它是有序的。
代码语言:javascript复制#添加一个带分数的值
zadd <key> [<value> <score>]...
-- 查询有多少个值
zcard <key>
-- 移除
zrem <key> <value>...
-- 获取区间内的所有
zrange <key> start stop
-- 通过分数段查看
zrangebyscore <key> start stop [withscores] [limit]
-- 统计分数段内的数量
zcount <key> start stop
-- 根据分数获取指定值的排名
zrank <key> <value>
持久化
Redis数据库中的数据都是存放在内存中,虽然很高效,但是突然停电,数据就全部丢失了。这个时候就需要持久化,将数据备份到硬盘上。
持久化的实现方式:
- 直接保存当前已经存储的数据,相当于复制内存中的数据到硬盘上,需要恢复数据时直接读取即可
- 保存存放数据的所有过程,需要恢复数据时,只需要将整个过程完整地重演一遍就能保证与之前数据库中的内容一致。
RDB
RDB就是第一种解决方案
命令执行保存:
代码语言:javascript复制-- 直接保存
save
-- 单独开一个子进程后台执行保存
bgsave
执行后,会在服务端目录下生成一个dump.rdb文件,而这个文件中就保存了内存中存放的数据,当服务器重启后,会自动加载里面的内容到对应数据库中。
由于会完整复制所有的数据,如果数据库中的数据量比较大,那么复制一次可能就需要花费大量的时间,所以可以每隔一段时间自动进行保存
如果基本上都是在进行读操作,而没有进行写操作,实际上只需要偶尔保存一次即可,因为数据几乎没有怎么变化。
配置文件中设置自动保存:
代码语言:javascript复制save 300 10 # 300秒(5分钟)内有10个写入
save 60 10000 # 60秒(1分钟)内有10000个写入
RDB的缺点:
- 完整地保存整个数据库中的数据
- 后台保存过程中也会产生额外的内存开销
- 并不是实时保存的,依然会导致少量数据的丢失
AOF
AOF是第二种方式,它会以日志的形式将每次执行的命令都进行保存,服务器重启时会将所有命令依次执行,通过这种重演的方式将数据恢复,这样就能很好解决实时性存储问题。
写日志策略:
- always:每次执行写操作都会保存一次
- everysec:每秒保存一次(默认配置),这样就算丢失数据也只会丢一秒以内的数据
- no:看系统心情保存
配置文件中配置:
代码语言:javascript复制appendonly yes
# appendfsync always
appendfsync everysec
# appendfsync no
服务器目录下appendonly.aof`文件,存储的就是我们执行的命令
AOF的缺点:
每次服务器启动都需要进行过程重演,相比RDB更加耗费时间
随着操作变多,不断累计,可能aof文件会变得无比巨大
AOF重写机制:
保证最终的重演结果和原有语句的结果一致,对多条语句进行压缩
命令执行重写操作:
代码语言:javascript复制bgrewriteaof
配置文件中配置自动重写:
代码语言:javascript复制# 百分比计算,这里不多介绍
auto-aof-rewrite-percentage 100
# 当达到这个大小时,触发自动重写
auto-aof-rewrite-min-size 64mb
- AOF:
- 优点:存储速度快、消耗资源少、支持实时存储
- 缺点:加载速度慢、数据体积大
- RDB:
- 优点:加载速度快、数据体积小
- 缺点:存储速度慢大量消耗资源、会发生数据丢失
事务和锁机制
事物
在Redis中也有事务机制,保证多条命令一次性完整执行而中途不受到其他命令干扰。
命令操作:
代码语言:javascript复制#开启事务
multi
#执行事物
exec
#取消事物
discard
#监测数据,执行exec前数据修改会抛出错误取消执行
watch
exec执行时,redis保证不会插入其他人语句来实现隔离,执行时可能导致长时间阻塞其他人
redis事务是创建了一个命令队列,查询指令是无法像MySQL事务能单独得到结果,而是将所有的命令装在队列中,等提交事务的时候再统一执行。
锁
redis使用的是乐观锁方式,这种方式允许exec前修改,这时会触发异常通知。mysql的悲观锁,会锁住资源,其他客户端不能修改数据,直到事务结束释放锁。
乐观锁和悲观锁:
- 悲观锁:时刻认为别人会来抢占资源,禁止一切外来访问,直到释放锁,具有强烈的排他性质。
- 乐观锁:并不认为会有人来抢占资源,所以会直接对数据进行操作,在操作时再去验证是否有其他人抢占资源。
Redis使用watch来监视一个目标,如果执行事务之前被监视目标发生了修改,则取消本次事务:
代码语言:javascript复制watch
取消监视:
代码语言:javascript复制unwatch
Java与Redis交互
依赖:
代码语言:javascript复制<dependencies>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.0.0</version>
</dependency>
</dependencies>
连接Redis数据库:
代码语言:javascript复制public static void main(String[] args) {
//创建Jedis对象
Jedis jedis = new Jedis("localhost", 6379);
//使用之后关闭连接
jedis.close();
}
调用命令的同名方法来执行Redis命令:
代码语言:javascript复制public static void main(String[] args) {
//直接使用try-with-resouse,省去close
try(Jedis jedis = new Jedis("192.168.10.3", 6379)){
jedis.set("test", "lbwnb"); //等同于 set test lbwnb 命令
System.out.println(jedis.get("test")); //等同于 get test 命令
jedis.hset("hhh", "name", "sxc"); //等同于 hset hhh name sxc
jedis.hset("hhh", "sex", "19"); //等同于 hset hhh age 19
jedis.hgetAll("hhh").forEach((k, v) -> System.out.println(k ": " v));
jedis.lpush("mylist", "111", "222", "333"); //等同于 lpush mylist 111 222 333 命令
jedis.lrange("mylist", 0, -1)
.forEach(System.out::println); //等同于 lrange mylist 0 -1
}
}
三大缓存问题
缓存穿透
当查询一个一定不存在的数据,比如Mybatis在缓存是未命中的情况下需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。
这显然是很浪费资源的,我们希望的是,如果这个数据不存在,缓存这一层直接返回空,不必再去查数据库了,这时就可以使用布隆过滤器来进行判断。
使用布隆过滤器,能够告诉你某样东西一定不存在或是某样东西可能存在。
缓存击穿
某个 Key 属于热点数据,访问非常频繁,同一时间很多人都在访问,在这个Key失效的瞬间,大量的请求到来,这时发现缓存中没有数据,就全都直接请求数据库,相当于击穿了缓存屏障,直接攻击整个系统核心。
这种情况下,最好的解决办法就是不让Key那么快过期,如果一个Key处于高频访问,那么可以适当地延长过期时间。
缓存雪崩
当你的Redis服务器炸了或是大量的Key在同一时间过期,这时相当于缓存直接GG了,那么如果这时又有很多的请求来访问不同的数据,同一时间内缓存服务器就得向数据库大量发起请求来重新建立缓存,很容易把数据库也搞GG。
解决这种问题最好的办法就是设置高可用,也就是搭建Redis集群,当然也可以采取一些服务熔断降级机制。 必再去查数据库了,这时就可以使用布隆过滤器来进行判断。
使用布隆过滤器,能够告诉你某样东西一定不存在或是某样东西可能存在。
缓存击穿
某个 Key 属于热点数据,访问非常频繁,同一时间很多人都在访问,在这个Key失效的瞬间,大量的请求到来,这时发现缓存中没有数据,就全都直接请求数据库,相当于击穿了缓存屏障,直接攻击整个系统核心。
这种情况下,最好的解决办法就是不让Key那么快过期,如果一个Key处于高频访问,那么可以适当地延长过期时间。
缓存雪崩
当你的Redis服务器炸了或是大量的Key在同一时间过期,这时相当于缓存直接GG了,那么如果这时又有很多的请求来访问不同的数据,同一时间内缓存服务器就得向数据库大量发起请求来重新建立缓存,很容易把数据库也搞GG。
解决这种问题最好的办法就是设置高可用,也就是搭建Redis集群,当然也可以采取一些服务熔断降级机制。