一、NoSQL 数据库概述
1.1、定义、特点
NoSQL,(Not Only SQL),泛指,非关系型数据库。不依赖业务逻辑的存储方式,是以 key-value 的形式存储数据的,大大增加了数据库的扩展能力!他的排名也算是比较靠前的(数据库排名);
- 它不遵循 SQL 标准;
- 不支持 ACID (即四个特性:原子性(atomicity,或称不可分割性)、一致性(consistency)、隔离性(isolation,又称独立性)、持久性(durability));
- 性能远超 SQL;
1.2、NoSQL适用场景
- 适用于海量数据的读写
- 对数据高并发的读写
- 对数据的高可扩展性
二、Redis 的概述、安装教程
2.1、概述
- Redis 是一个开源的 key-value 存储系统;
- 和 Memcached 类似,它是支持存储的 value 类型相对更多,包括 string(字符串)、list(链表)、Set(集合)、zset(sorted set –有序集合)和 hash(哈希类型);
- 这些数据类型都支持 push/pop、add/remove 及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的;
- 在此基础上,Redis 支持各种不同方式的排序;
- 与 memcached 一样,为了保证效率,数据都是缓存在内存中;
- 区别的是 Redis 会周期性地把更新的数据写入磁盘或者把修改操作写入追加的记录文件;
- 并且在此基础上实现了 master-slave(主从)同步;
2.2、安装教程
2.2.1、下载 Linux 版本的 Redis
Redis 官网
2.2.2、安装 gcc 运行环境
先查看 linux 中是否存在 gcc,是否需要更新;
# 查看命令gcc --version
安装命令(通过yum来进行安装);
yum install gcc
2.2.3、解压下载下来的安装包(这里的版本是 redis-6.2.6.tar.gz )
运行解压命令;
tar -zxvf redis-6.2.6.tar.gz
解压好之后进入其目录进行编译操作;
cd redis-6.2.6make
2.2.4、编译之后,执行 install 命令
make install
2.2.5、测试启动
1)启动(终端前台启动,不推荐。启动了之后不能关闭终端,终端关闭,服务停止)
安装完成后来到/use/local/bin,进入到主目录查看;
cd /usr/local/bin
redis-servere
2)后台启动
后台启动需要一点点配置,首先来到刚解压安装编译的位置(我自己的路径,根据需求修改即可);
cd /home/zyd/redis/redis-6.2.6
这条路径下会有一个redis.conf的文件,将其赋值一份到其他路径下(我这里就暂且拷贝当这个位置/etc路径下);
cp /home/zyd/redis/redis-6.2.6/redis.conf /etc/redis.conf
拷贝完成后,修改redis.conf中的选项,将daemonize改为 yes 即可;
回到解压的位置(带文件)启动
redis-server /etc/redis.conf
Redis 出现了一些问题,启动后无法正常地实现存到磁盘任务的问题,解决方案如下:
将配置项stop-writes-on-bgsave-error设置为no,可以在 Redis 命令行里的配置,也可以在redis.conf配置文件里改;
启动就到这里就可以跟安装启动部分告一段落 了!
三、Redis 的五大常用数据类型
3.1常用、通用的命令
查看当前库所有 key;
keys *
判断某个 key 是否存在;
exists key
查看你的 key 是什么类型;
type key
删除指定的 key 数据;
del key
根据 value 选择非阻塞删除(仅将 keys从 keyspace 元数据中删除,真正的删除会在后续异步操作);
unlink key
10 秒钟:未给定的 key 设置过期时间;
expire key 10
查看还有多少秒过期,-1 表示永不过期,-2 表示已过期;
ttl key
命令切换数据库 例如select 0代表切换到0库;
select
查看当前数据库的key的数量;
dbsize
清空当前库;
flushdb
通杀全部库;
flushall
3.2、Redis 字符串 String
String 是 Redis 的最基本数据类型,就是一个典型的 key-value 对儿,String 是二进制安全的,这就意味着你可以存放任何数据到 Redis 的 string 中,包括序列化的对象,以及图片等等。一个字符串的最大占用内存是 512M
3.2.1、常用命令
添加一个键值对;
set key value
查询对应的键的值;
get key
追加对应 key 的值;
append key value...
获得值得长度;
strlen key
只有在 key 不在的时候设置值;
setnx key values
将key中存放的数字进行自增1操作,如果为空,新增值为 -1;
incr keydecr key
自增自减可以自定义步长;
incrby key 2decrby key 1
设置/获取一对或者多个键值对的值;
mset k2 value2 k3 value3 k4 value4 k5 3mget k1 k2 k3 k4 k5
只有在 key 不在的时候设置值(基于他的原子性,有一个失败,就都失败);
msetnx k2 value2 k3 value3 k4 value4 k5 3
根据给定的范围获取值(值得范围是左闭右闭),类似于 Java 中的 substring( ) 方法;
getrange k2 0 -1 获取全部getrange k2 0 30-3一共四个字符
根据给定的位置开始覆盖写值;
get k2-> value2setrange k2 0 testget k2-> teste2
设置键值的同时,设置过期时间(单位为秒);
setex key 5 value
设置新值的同时获得旧值;
getset key value
3.3、Redis 列表 List
Redis 的 list 是单键多值,即一个键可以对应一个或者多个值。Redis 是一个简单的字符串列表,按照插入顺序排序。他的底层是一个双向链表,所以你可以在链表的头部和尾部插入元素,下面有简单示例;
3.3.1、常用命令
从左边或者右边插入一个或多个值(并非键值对);
lpush/rpush k1 v1 v2 v3
从左边或者右边弹出一个值;特点:值在,键在,值无,键亡;
lpop/rpop key
从k1右边弹出一个值插入到左边的k2中(右弹左压);
rpoplpush k1(右弹) k2(左压)
按照索引下标获得元素;
lrange key start endlrange k5 0 3共四位lrange k5 0 -1 代表全部
按照索引下标从左向右开始查找对应下标的值;
lindex k5 0
在指定值得前面插入一个新值;
linsert key before lilei newvalue
从左边开始删除 n 个 value;
lrem key n vakue
将列表 key 下标为 index 的值换为 value;
lset key index value
3.3.2、数据结构
List 的数据结构quickList;首先,在元素比较少的时候,会使用一块连续的内存存储,这个结构是ziplist,称之为压缩列表。他讲所有的元素紧紧挨着一起存储,分配的是一块连续对的存储。当数据量比较多的时候,才会改成quicklist;
因为普通的链表需要附加指针的空间很大,会比较浪费空间。Redis 将链表个ziplist结合起来组成了quicklist,也就是使用双向指针将ziplist穿起来使用,解决了快速插入删除的性能问题,又不会出现太大的数据冗余!
3.4、Redis 集合 Set
Redis Set对外提供的功能是与 list 是类似的,特殊之处在于 Set 是可以自动排重的,也就是说,当你需要一个列表数据,又不希望出现重复数据,就可以选择使用 Set,Set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是 list 所不能够提供的
Redis 的 Set 是 string 类型的无序集合,它底层其实是一个 value 为 null 的 hash 表,所以添加、查找、删除的复杂度都是 o(1);
3.4.1、常用命令
添加一个或多个值到 Set 集合中;
sadd key value1 value2...
返回该键的所有值(并不是删除);
smembers key
返回该集合的元素个数;
scard key
删除结合中的一个或多个元素;
srem key value1 value2...
从该集合中随机 弹出一个值;
spop key
从该集合中随意取出 n 个值,但是不会从集合中删除;
srandmember key n
把集合中的一个值从 source 集合移动到另一个集合destination;
smove source destination value
返回两个或多个集合的交集;
sinter key1 key2
返回两个集合或多个集合的并集;
sunion key1 key2
返回两个集合的差集元素(key1 有但是 key2 没有,也就是 key1 – key2);
sdiff key1 key2
3.4.2、数据结构
Set 数据结构对应的是 dict 字典,字典是用哈希表实现的;而 Java 中的 HashSet 的内部实现使用的是 HashMap,只不过所有的 value 都只想同一个对象,Redis 的 Set 结构也是一样的,它的内部也使用 hash 结构,所有的 value 都指向同一个内部值;
3.5、Redis 哈希 Hash
Redis Hash 是一个键值对集合,他是一个 string 类型的 field 和 value 的映射表。类似于 Java 里的Map,所以 Hash 特别适合用于存储对象;如下图所示
如上图,key (用户 ID) + field (属性标签) 就可以操作对应的属性的数据了,既不需要重复存储数据,也不会带来序列化和并发修改控制的问题;
3.5.1、常用命令
给 hash 集合中添加值(经过测试,是可以同时添加很多个属性的);
hset user1: id 1
hset user1: id 1 name zhangsan age 18
从集合中取出来值 value (记得加上冒号);
hget user1: name
批量设置值;
hmset key: field value field value...
hmset user2: id 2 name lisa age 26
查看 Hash 表 key 中,给定的域是否是存在的;
hexists key: field
查看该 key 的所有的 field (记得加上冒号) ;
hkeys key:
查看该结合的所有的 filed 的值;
hvals key:
为哈希表 key 中的域 field 的值增加量(1,0,-1);
hincrby user1: age 2
为 key 中的域 field 的值设置为 value (当且仅当域 field 不存在时才可以设置成功);
hsetnx user1: firstName lisi
3.5.2、数据结构
Hash 类型对应的数据结构是两种:ziplist (压缩列表),hashtable (哈希表) 。当 field-value 长度较短且个数较少,使用 ziplist,否则使用 hashtable,我认为和之前的那个 List 的是类似的;
3.6、Redis 集合 Zset
他是有序集合 Zset(sorted set),与普通的 Set 集合非常的相似,他是一个没有重复元素的字符串集合,区别在于他多了一个“评分”,这个用来实现了排序的功能,集合里的元素是唯一的,但是评分是可以重复的;
注意:为了方便理解和做笔记,这里将引用 set 集合中的 field 和 value 的概念,即 score = value
3.6.1、常用命令
将一个或多个 member 元素及其 score 值加入到有序集 key 中;
zadd key score1 field score2 field...
返回有序集合中,下标在 start 和 stop 之间的元素;
zrange key start stop# WITHSCORES 携带评分一起返回zrange key start stop withscores
返回有序集合中,评分在 min 和 max 之间的结果(从小到大就是 min,max);
zrangebyscore key min maxzrangebyscore key min max withscores
从大到小就是 max,min;
zrangebyscore key max minzrangebyscore key max min withscores
给指定的集合中的元素加上一个指定的数(这里只得是加上评分);
zincrby key value field
删除指定的元素,不是根据评分删除,是根据值;
zrem key field
统计该集合中,评分区间内的元素的个数
zcount key 0 500
根据已存在的 field(不是评分),来返回对应的排位(从 0 开始)
zrank key field
3.6.2、数据结构
SortedSet (zset) 是 Redis 提供的一个非常特别的数据结构,在结构方面和 Set 非常的相似,即Map,可以给每一个元素 value (field) 赋予一个权重(score);另外,他在内部又类似于 TreeSet ,内部元素会根据权重(score)进行排序,可以得到每个元素的名次,也可以通过 score 的范围来获取元素的列表;
zset 的底层还用了两个数据结构,hash和跳跃表;
hash,hash 的作用就是关联元素的 value (field) 和权重 score,保障内部元素 value (field) 的唯一性,可以通过元素 value (score) 来找到对应的 score 的值;
跳跃表,跳跃表的目的在于给元素 value (field) 排序,根据 score 的范围来获取元素列表;什么是跳跃表?
四、配置文件
首先到我们自己定义的配置文件的位置去,我这里是/etc/redis.conf
4.1、Units 单位
第一个部分是Units单位,我们在 Redis 中是只支持 byte(字节) ,而不支持 bit(位),大小写不敏感;
4.2、INCLUDES 包含
4.3、网络相关配置
4.3.1、bind
默认情况下,他为 127.0.0.1 ,即只允许本地进行访问,在不写的情况下,允许任何地址进行访问,我这里就注释掉了;
4.3.2、protected-mode
在下面一点的位置,有一个 protected-mode ,这里我们将他设置为 no 将他关掉;这边是有点说道的,假如你开启了保护模式,且没有设置 bind ip 和 密码,那么 Redis 是只允许本机进行访问的;
4.3.3、port
以及我们可以看到他的端口设置 6379 ,那么这个就不在这里赘述啦;
4.3.4、tcp-backlog
通过设置 tcp 的 backlog,backlog 其实是一个连接队列,连接总和包括未完成三次握手队列和已经完成了三次握手的队列;在高并发环境下,可以设置一个高的 backlog 值来避免客户端连接问题;
linux 内核会将这个值减小到 /proc/sys/net/core/somaxconn 的值(128),所以需要确认增大 /proc/sys/net/core/somaxconn 和 /proc/net.ipv4/tcp_max_syn_backlog(128)两个值,来达到想要的结果;
4.3.5、timeout
超时,一个客户端维持多少秒会关闭,0 表示永远不关闭;
4.3.6、daemonize
是否启用后台进程,通常设置为 yes,守护进程,后台启动;
4.3.7、pidfile
存放 pid 文件的位置,每个实例都会产生一个不同的 pid 文件;
4.3.8、loglevel
指定日志级别,Redis 总共分为四个级别,分别为:debug、verbose、notice、warning,默认为 notice;
4.3.9、logfile
日志文件名称
4.3.10、database 16
设置默认的库的数量为 16,默认的数据库为 0,可以使用 select 命令来指定连接某个库;
4.3.11、设置密码
在命令行里面设置的密码是临时的,在 Redis 服务器重启之后,密码就还原了。需要永久的设置密码还得是在配置文件里面设置;
4.3.12、LIMITS 限制
1)maxclients
设置 Redis 同时可以连接多少个客户端进行连接,默认情况下为 10000 个客户端;如果达到了该限制, Redis 则会拒接新的连接请求,并反馈为 “max number of clients reached”;
2)maxmemory
建议设置,否则内存满的时候,服务器会直接宕机;
设置 Redis 的可使用内存量之后,当到达内存的使用上限的时候,Redis 将视图移除内部的数据,移除的规则可以通过maxmemory-policy来指定;
- volatile-lru:使用LRU算法移除key,只对设置了过期时间的键;(最近最少使用);
- allkeys-lru:在所有集合key中,使用LRU算法移除key;
- volatile-random:在过期集合中移除随机的key,只对设置了过期时间的键;
- allkeys-random:在所有集合key中,移除随机的key;
- volatile-ttl:移除那些TTL值最小的key,即那些最近要过期的key;
- noeviction:不进行移除。针对写操作,只是返回错误信息;
也可以设置不允许移除,那么 Redis 就会返回一些错误信息,比如 SET、LPUSH 等;
但是对于无内存申请的指令,仍然会正常响应,比如 GET 等。当然,如果你有从 Redis 的情况下,那么在设置内存使用上限时,需要在系统中留出一些内存空间给同步队列缓存,只有在你设置的是不移除的情况下,就可以不考虑这个因素;
3)maxmemory-samples
设置样本数量
五、 订阅和发布
订阅和发布是 Rerdis 的一种消息通信模式;Redis 客户端可以订阅任意数量的频道;
客户端进行订阅;
当在频道中发布消息的时候,消息就会发给订阅的客户端;
5.1、订阅
进入客户端之后,先进行订阅,等待消息发布即可;
subscribe channel
5.2、发布
进入另一台客户端之后,开始发布,那么订阅了该频道的客户端就会收到该发布的消息;
publish channel helloworld
发布端,返回的数字即为订阅的客户端的数量;
六、Redis 新数据类型
6.1、Bitmaps
6.1.1、概念
Redis 提供了 Bitmaps 这个 “数据类型” 可以实现对位的操作;Bitmaps 并不是一个数据类型,实际上他只是一个字符串(key-value),但是他可以对字符串的位进行操作;Bitmaps 单独提供了一套命令,所以在 Redis 中使用 Bitmaps 和使用字符串的方法不太相同,可以把 Bitmaps 想象成一个以位为单位的数组,数组的每个数组只能存储 0 和 1,数组的下标在 Bitmaps 中叫做偏移量;
6.1.2、常用命令
给 key 中指定偏移量的位置上赋值 0 或 1,在第一次初始化 Bitmaps 时,加入偏移量比较大时,那么整个初始化过程会比较慢,而且可能会造成 Redis 阻塞;
setbit key offset value
获取 key 中的某个偏移量的值
getbit key offset
操作如下图所示
统计集合中 1 的个数
bitcount key
将多个集合进行交集、并集等操作
bitop and/or/not result k1 k2...
6.1.3、Bitmap 与 Set 集合的对比
我们要辩证的看待这个问题!
Bitmap 占用的空间小,Set 的占用空间较大;所以当数据量很大的时候,那么我们此时此刻来使用 Set 是不利的,会消耗很大的内存空间;但是也并不意味这 Bitmap 就是比 Set 要好,在数据量很大的情况下,有用的数据很少的时候,那么用 Bitmap 就是十分不划算的,因为其大部分的数据都是 0 ,造成了很大的数据冗余,空间浪费;
6.2、HyperLogLog
6.2.1、概念
有一个场景,我们在某些情况下需要这种数据类型,比如在统计网站 PV(PageView 页面访问量),可以利用 Redis 的 incr、incrby 来进行实现;但是存在的问题,像 UV(UniqueVisitor 独立访客)、独立的 IP 数、搜索记录数等需要去重的问题,这类问题统称为基数问题;
6.2.2、常用命令
添加指定的元素到 HyperLogLog 返回 1,则说明添加成功,返回 0,则说明添加失败;
pfadd key value
计算 HLL 的近似数;
pfcount key
合并两个 HLL 数据类型的数据集;
pfmerge
演示如下图所示
6.3、Geospatial
6.3.1、概念
Redis 3.2 中增加了对 GEO 类型的支持。GEO,Geographic,地理信息的缩写。该类型,就是元素的二维坐标,在地图上就是经纬度。Redis 基于该类型,提供了经纬度设置,查询,范围查询,距离查询,经纬度 Hash 等常见操作;
6.3.2、常用命令
加地理位置(经度、纬度、名称);
geoadd key longitude latitude member longitude latitude member ...
获得指定地区的坐标值;
geopos key membergeopos china:city chongqing
获得两个地区的直线距离;
geodist key member1 member2 [m/km/ft/mi]
获得指定经纬度的某一半径内的元素;
georadius key longitude latitude radius [m/km/ft/mi]georadius china:city 110 30 1000 km
七、Jedis
7.1、概念
Jedis 是用于使用 Java 代码来操作 Redis 的一个软件(可以理解为 Redis 专用的 JDBC),我们创建一个简单的 Maven 工程测试一下相关功能。
7.2、环境搭建
- 创建好 Maven 工程之后,直接在 pom.xml 里面写入依赖,这里是用的是 jedis 3.2.0
redis.clients jedis 3.2.0
- 等待下载完成后,我这里建了一个测试类准备进行测试,如下
public class JedisDemo1{ public static void main(String[] args){ // 创建Jedis对象(使用有参构造) Jedis jedis = new Jedis("192.168.182.128",6379); String s = jedis.ping(); System.out.println(s); }}
- 现在去立马运行程序,大概率会发生拒绝连接等问题,返回异常,大概率那是因为以下两点
- 配置文件中的选项没有设置好;
- Linux 中的 firewall 没有开启对应的端口 6379;
解决上述问题 1:配置文件中将 bind 选项直接注释掉,和 protected-mode 设置为 no,具体看上边的部分,这里不做赘述;
解决上述问题 2:在 Linux 中开启 6379 端口;
# 查看当前防火墙状态systemctl status firewalld.service# 查看当前需要打开的端口6379的状态firewall-cmd --zone=docker --query-port=6379/tcpfirewall-cmd --query-port=6379/tcp# 如果没有打开,那么就打开,并再次查看是否开启# firewall-cmd --add-port=6379/tcpfirewall-cmd --add-port=6379/tcp --permanent# 开启后,重启服务systemctl stop firewalld.servicesystemctl start firewalld.service# 2021.11.26 补充(今天看到了新的打开端口的方法,这种方法更加简洁有效,补充如下)# 1.查看防火墙状态firewall-cmd --state# 2.开启端口firewall-cmd --permanent --zone=public --add-port=22/tcp# 3.重启防火墙firewall-cmd --reload# 4.查看防火墙看起的端口
在这之后,就可以进行成功连接了;我在这里搞了一个小、shell脚本,用来开启指定端口,建议放在 root 的权限下!
#!/bin/bashcommand=`firewall-cmd --state`if [ $command == "running" />
执行脚本(打开 8888 端口号)
./addport.sh 8888
具体的代码操作这里不做详细描述;
八、SpringBoot 整合 Redis
8.1、导入依赖
org.springframework.boot spring-boot-starter-parent 2.2.1.RELEASE org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-starter-data-redis org.apache.commons commons-pool2 2.6.0
8.2、配置环境
这里遇到一个坑,也是我长期没有看 SpringBoot 的结果,他的包扫描范围由他的启动类来决定,也就是说他的包扫描路径是“他的父目录下的所有子目录”,否则会因为无法扫描到包而导致无法启动
8.2.1、Redis 通用配置类
@EnableCaching@Configurationpublic class RedisConfig extends CachingConfigurerSupport{ @Bean public RedisTemplate redisTemplate(RedisConnectionFactory factory) { RedisTemplate template = new RedisTemplate(); RedisSerializer 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 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; }}
8.2.2、测试类
@RestController@RequestMapping("/redisTest")public class RedisTestController{ @Autowired RedisTemplate redisTemplate; @GetMapping public String testRedis(){ redisTemplate.opsForValue().set("name","张三"); String name = (String) redisTemplate.opsForValue().get("name"); return name; }}
九、Redis 事务和锁机制
首先他将开始进行组队,按顺序组队,组队的过程是为了防止执行过程中有其他命令进行插队操作,导致我们没有达到想要的结果!
基本的操作流程如下所示:
9.1、命令行操作事务
# 开启事务操作multi# 开始向事务队列中进行添加语句,这里称之为组队阶段set key value...# 如果需要中途打断组队,放弃事务操作,那么就立即使用 discard 命令进行操作。discard# 执行阶段,开始执行队列里的语句exec
9.2、事务出错
9.2.1、情况一
组队失败
当我们在组队时发生错误时,那我们后面执行的时候,也是会直接报错的,整个事务是失败的,相当于是一个回滚的操作;
9.2.2、情况二
组队成功,运行出错;
当我们发生组队成功,但是运行失败的情况时,报错的那一条执行失败,其他的都是会执行成功的,没有类似于回滚操作;
9.3、事务冲突的问题
我们都知道,在学习数据库的时候会有一个非常经典的案例,就是银行取钱结账的这个场景,这里就不在这里赘述这个场景具体是怎么样的了;有两种锁可以很好的结局这种问题;
9.3.1、悲观锁
当我们在操作数据的时候,会对数据直接进行上锁,以防止其他人对数据进行操作。等操作结束后,释放锁。等一下个人过来进行操作数据的时候,首先会判断是否满足条件,那么这就会解决我们上面阐述的事务冲突的问题;悲观锁,可以理解为是悲观的,永远认为有人会操作他的数据,所以直接上锁,这种态度被称为是一种悲观的态度,所以称之为悲观锁;
9.3.2、乐观锁
乐观锁,顾名思义,是乐观的,认为不会有人更改他的数据,所以叫做乐观锁,实际上是给数据加了一个版本号,用来做数据校验;当我们进行操作数据的时候,会拿着数据的版本号和数据库的数据的版本号进行对比,如果是一样的版本号且数据满足操作条件,就进行直接操作数据,否则不操作;
乐观锁的操作
终端一:
# 用 watch 关键字来监视一个 keyset blance 100watch blance# 开启事务multi# 在事务下进行对 blane 的操作incrby blance 10execblance = 110
终端二:
# 用 watch 关键字来监视一个 keyset blance 100watch blance# 开启事务multi# 在事务下进行对 blance 的操作incrby blance 10exec操作失败返回 nil
9.3.3、秒杀案例
秒杀案例,这里不做笔记和演示,只做一些原理说明;
版本一
秒杀案例的实现,靠 Redis 的乐观锁和事务进行实现,先用 Watch 来对库存进行监视,然后建立一个事务 multi 进行事务操作,再进行操作,但是这样的方法可以解决超卖的问题,但是会出现连接超时的问题;
版本二
通过数据连接池来解决了版本一中的连接超时的问题,但是同时又发现了,有库存遗留的问题;
版本三
Lua 脚本是一个小巧的脚本语言,Lua 脚本可以轻松的被 C 语言、C ++ 调用,也可以轻松的调用 C 语言等函数,Lua 没有提供强大的类库,所以只能充当一个嵌入式脚本语言,不适合作为开发独立应用程序的语言
利用 Lua 脚本完成了 Redis 实现的秒杀任务,并且解决了遗留库存的问题;
十、Redis 持久化
10.1、Redis 持久化之 RDB
在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是 Snapshot 快照中,它恢复时是将快照文件直接读到内存中;
10.1.1、备份是如何进行的
Redis 会单独创建(fork)一个进程用来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,在用这个临时文件替换上次持久化好的文件,在整个过程中,主线程是不会进行 IO 操作的,这样就会极大的提高了 Redis 的性能;如果需要大规模的进行数据恢复,并且对于大数据恢复的完整性不是非常的敏感,那 RDB 方式要比 AOF 方式更加的高效。RDB 的缺点是最后一次持久化后的数据可能丢失(服务器宕机之前的一个备份将会丢失,因为它会存在一个持久化周期,例如save 20 3表示在 20 秒内如果有 3 个数据发生改变,就会触发持久化操作);
Fork 子进程的作用就是复制一个和当前进程一样的进程,新进程的所有数据(变量、环境变量、程序计数器等)数值和原进程一致,但是他是一个全新的进程,并作为原进程的子进程;
10.1.2、RDB 的持久化流程图
10.1.3、dump.rdb 文件
在 redis.conf 中配置文件名称,默认为 dump.rdb;该文件的默认保存路径,默认为 Redis 启动命令行所在的目录下为 dir "/myredis/";
快照中默认的快照配置
表示的是在 3600 秒内有一个数据发生变化就会触发备份操作,其他的类同;
命令 save 和 bgsave
save:save 只管保存,其他不管,全部阻塞手动保存,不建议使用这种方法;
bgsave:Redis 会在后台一步进行快照操作,快照的同时还可以响应客户端的请求;
可以通过 lastsave 命令获取最后一次成功执行快照的时间;
flushall 命令
执行 flushall 命令,也会产生 dump.rdb 文件,但是里面是空的,将是毫无意义的;
配置文件中的选项
stop-writes-on-bgsave-error
当 Redis 无法写入磁盘的时候,直接关掉 Redis 的写操作,推荐 yes;
rdbcompression 压缩文件
对于已经存储到磁盘中的快照,可以设置是否进行压缩存储,如果是的话,Redis 会使用 LZF 算法进行压缩存储;
如果你不想消耗 CPU 来进行压缩的话,可以设置为关闭此功能;
rdbchecksum 检查完整性
在存储快照后,还可以让 Redis 使用 CRC64 算法进行数据校验,但是这样做会增大 10% 的性能消耗,如果希望获取到更大的性能提升,可以关闭此功能;
rdb 的备份
先通过 config get dir 查询 rdb 文件的目录,将 *.rdb 的文件拷贝到别的地方;
rdb 的恢复:
- 关闭 Redis;
- 先把备份的文件拷贝到工作目录下cp dump2.rdb dump.rdb;
- 启动 Redis,备份数据会直接加载;
优势
- 适合大规模的数据恢复;
- 对数据完整性和一致性要求不高更合适使用;
- 节省磁盘空间;
- 恢复速度快;
劣势
- Fork 的时候,内存中的数据被克隆了一份,大致 2 倍的膨胀性需要考虑;
- 虽然 Redis 在 Fork 时使用了写时拷贝技术,但是如果数据量庞大时还是比消耗内存的;
- 在备份周期内(在一定时间间隔内做一次备份),所以如果 Redis 在备份的时候发生宕机,就会丢失最后一次快照后的所有修改;
如何停止
动态停止 RDB:redis-cli config set save"" save 后给空值,表示禁用保存策略;
10.2、Redis 持久化之 AOF
AOF 是什么?Append Only File,他是以日志的形式来记录每个写操作(增量保存),将 Redis 执行过的所有指令记录写下来(读操作不记录),只是追加文件不会改写文件,Redis 启动之初会读取改文件重新构建数据,换言之,Redis 重启的话就根据日志文件的内容将写指令从前到后执行一次,从而达到恢复数据的目的;
10.2.1、AOF 持久化流程
- 客户端的请求写命令会被 append 追加到 AOF 缓冲区内;
- AOF 缓冲区根据 AOF 持久化策略【always、everysec、no】将操作 sync 同步到磁盘 AOF 文件中;
- AOF 文件大小超过重写策略或手动重写时,会对 AOF 文件 rewrite 重写,压缩 AOF 文件容量;
- Redis 服务重启时,会重新加载 AOF 文件中的写操作达到数据恢复的目的;
10.2.2、AOF 默认不开启
AOF 默认不开启,可以在 redis.conf 中配置文件名称,默认为 appendonly.aof ,AOF 文件的保存路径,同 RDB 的路径一致;
10.2.3、RDB 和 AOF 同时开启
当他们同时开启的时候,系统会默认读取 AOF 的数据(数据不会存在丢失);
10.2.4、AOF 启动、修复、恢复
- AOF 的备份机制和性能虽然和 RDB 不通,但是备份和恢复的操作同 RDB 一样,都是拷贝备份文件,需要恢复时,再拷贝到 Redis 工作目录下,启动系统即加载;
- 正常恢复
- 修改默认的 appendonly no 改为 yes;
- 将有数据的 aof 文件复制一份保存到对应的目录(查看目录:config get dir)
- 恢复:重启 redis 然后重新加载
- 异常恢复
- 修改默认的 appendonly no 改为 yes;
- 如果遇到 AOF 文件损坏,通过/usr/local/bin/redis-check-aof --fix appendonly.aof来进行恢复;
- 备份被写坏的 AOF 文件;
- 恢复:重启 Redis,然后重新加载;
10.2.5、AOF 同步频率设置
appendfsync always
始终同步,每次 Redis 的写入都会立刻记入日志;性能较差,但是数据完整性较好;
appendfsync everysec
每秒同步,每秒记入日志一次,如果宕机,本地的数据可能丢失;
appendfsync no
Redis 不主动进行同步,把同步时机交给操作系统;
10.2.6、Rewrite 压缩
AOF采用文件追加方式,文件会越来越大为避免出现此种情况,新增了重写机制, 当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩, 只保留可以恢复数据的最小指令集.可以使用命令 bgrewriteaof
10.2.7、优势
- 备份机制更加稳健,丢失数据概率更低;
- 可读的日志文本,通过操作 AOF 稳健,可以处理误操作;
10.2.8、劣势
- 比起 RDB 占用更多的磁盘空间;
- 恢复备份速度要慢;
- 每次读写都同步的话,有一定的性能压力;
- 存在个别 Bug,造成不能恢复的后果;
10.3、总结
- 官方推荐两个都启用;
- 如果对数据不敏感,可以单独启用 RDB;
- 不建议单独使用 AOF,因为可能会出现 Bug;
- 如果只是做纯内存缓存,可以都不启用;
十一、Redis 主从复制
10.1、搭建 Redis 主从服务器
10.1.1、配置文件
首先复制几个一样的配置文件出来;
cp /home/zyd/redis/redis-6.2.6/redis.conf /home/zyd/myredis/redis.conf
设置配置文件中的一些设置;
vim redis.confdaemonize yesvim redis6379.conf# 以下就是配置文件中的内容include /home/zyd/myredis/redis.confpidfile /var/run/redis_6379.pidport 6379dbfilename dump6379.rdb
拷贝另外的两台 Redis 服务器的配置,并且设置好其中的配置端口;
cp redis6379.conf redis6380.confcp redis6379.conf redis6381.conf
10.1.2、启动服务器
redis-server /home/zyd/myredis/redis6379.conf
redis-server /home/zyd/myredis/redis6380.conf
redis-server /home/zyd/myredis/redis6381.conf
这样一来,三台服务器就此就启动成功了;我们可以通过命令来查看服务器的状态;
# 连接端口号为 6379 的 redis 服务器reids-cli -p 6379# 查看服务器信息、状态> info replication
10.1.3、让从机连接主机
redis-cli -p 6380> slaveof 127.0.0.1 6379redis-cli -p 6381> slaveof 127.0.0.1 6379
下图可能会存在一些问题,他们的 IP 地址可能存在一些问题,应该是用127.0.0.1才行;
10.1.4、情况说明
- 当我们 Redis 的主机服务器挂掉的话,那么两台从机是不会发生“叛变”的,会继续等待主机的恢复;
- 当我们的 Redis 的有一台从机挂掉的时候,那么他再启动起来的时候,就会变为 master 主机状态,需要重新关联主机,但是再一次连上 Redis 主机的时候,就会将数据同步过来;
10.2、常用的三种模式
10.2.1、一主二仆
该模式就是我们常见的,一台主服务器下面挂着很多台从服务器;主机挂了,等主机重启后,地位不变还是主机,从机还是从机;
10.2.2、薪火相传
该模式就是主机下面只直接挂了一台从机服务器,其他的从机服务器就挂在第一台从机上,达到“薪火相传”的目的;
- 主机挂了,从机还是从机,无法进行写数据;
- 中途变更转向,会清除之前的数据,重新建立最新的拷贝;
- 某一个从机挂了后面的从机都无法进行备份;
10.2.3、反客为主
当一个 master 宕机之后,后面的 slave 可以立刻升为 master,其后面的 slave 不用做任何修改;
slaveof no one
10.3、复制原理
- Slave 启动成功连接到 master 后会发送一个 sync 命令;
- master 接到命令启动后台的存盘进程,同时手机所有接收到的用于修改数据集命令,在后台进程执行完毕之后,master 将传送整个数据文件到 slave ,完成一次同步复制;
- 全量复制:slave 服务器在接收到数据库文件数据后,将其存盘并加载到内存中;
- 增量复制:master 继续将新的所有收集到的修改命令一次传给 slave,完成同步;
- 只要重新连接 master,一次完全同步(全量复制)将被自动执行;
10.4、哨兵模式(sentinel)
反客为主的自动版,能够后台监控主机是否故障,如果故障了会根据投票数自动将从库换为主库;
10.4.1、使用步骤
- 将三台服务器,调整为一主二仆模式,在这里就是 6379 带着 6380、6381;
- 在自定义的目录下新建sentinel.conf文件,这个名字绝对是不能错的;
- 在上述一步的配置文件中写好配置内容为 sentinel monitor mymaster 127.0.0.1 6379 1其中 mymaster 为监控对象起得服务器的名称,1 为至少有多个哨兵统一迁移的标识;
- 启动哨兵vim /home/zyd/myredis/sentinel.conf # 编辑内容 sentinel monitor mymaster 127.0.0.1 1启动哨兵redis-sentinel /home/zyd/myredis/sentinel.conf
启动成功,如上图所示,看的到主机是 127.0.0.1 6379 ,两台从机分别又是 6380,6381;
10.4.2、若干情况
主机宕机情况
当我们的主机宕机了,那么我们的哨兵就会根据一些规则在从机中来选出来一个新的主机当服务器;而当我们的主机重启成功的时候,他只能作为从机来使用;
复制延时
由于所有的写操作都是在主机 master 上进行的操作的,然后同步更新到 slave 上,所以从 master 同步到 slave 机器上会有一定的延迟,当系统很繁忙的时候,延迟的问题会更加严重,slave 机器数量的增加也会使这个问题更加严重;
故障恢复
- 优先级在其配置文件 redis.conf 中:replica-priority 100;
- 偏移量是指的是,获得原主机数据最全的;
- 每个 Redis 实例启动后都会随机生成一个 40 位的 runid;
主从复制的 Java 代码实现
private static JedisSentinelPool jedisSentinelPool=null;public static Jedis getJedisFromSentinel(){ if(jedisSentinelPool==null){ Set 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(); }}
十二、Redis 集群
12.1、集群的搭建
Redis 集群实现了对 Redis 的水平扩容,即启动 N 个 Redis 节点,将整个数据库分布存储在这 N 个节点中,每个节点存储总数据的 1/N;
我们这里搭建6台 Redis 服务器组成集群,由于条件限制我们选择在不同的端口来启动这些服务器;
12.1.1、删除相关文件
删除持久化相关的备份文件:rdb、aof;
12.1.2、制作 6 个实例
制作 6 个实例,映射 6 个端口,分别是6379、6380、6381、6389、6390、6391
12.1.3、配置基本信息
在 redis.conf 中 开启 daemonize yes
cluster-enable yes 打开集群模式
cluster-config-file nodes-6379.conf 设定节点配置文件名
cluster-node-timeout 15000 设定节点失联时间
总的配置信息如下:
在 vim 编辑器中的命令行模式使用 /%s/6379/6380 命令来替换端口信息,快速配置;
启动这 6 个 Redis 服务器;
redis-server redis6379.conf
使这 6 个节点合成一个集群,在执行以下组合命令前。要保证 nodes-xxxx.conf 文件都正常的生成了;
cd /home/zyd/redis/redis-6.x.x/src/
此处应该写上 198.x.x.x 的真实的 ip 地址,不应该写 127.0.0.1
redis-cli --cluster create --cluster-replicas 1 192.168.182.128:6379 192.168.182.128:6380 192.168.182.128:6381 192.168.182.128:6389 192.168.182.128:6390 192.168.182.128:6391
12.2、登录客户端
以集群方式登录!
-c 采用集群策略连接,设置数据会自动切换到相应的写主机上
redis-cli -c -p 6379
12.2.1、cluster nodes
在 Redis 客户端内,通过 cluster nodes 命令查看集群信息
> cluster nodes
12.2.2、分配节点
- redis cluster 如何分配 6 个节点,一个集群至少要有三个主力节点;
- 集群启动选项中 --cluster-replicas 1 表示我们希望为集群中的每个主力节点创建一个从节点;要实现主节点挂了从节点顶上;
- 分配原则尽量保证每个主数据库运行在不同的 IP 地址,每个从库和主库不在一个 IP 地址上;
12.3、slots
什么是 slots,一个 Redis 集群包含 16384 个插槽(hash slot),数据库中的每个键都属于这 16384 个插槽的其中一个;
集群会使用 CRC16(key)% 16384 来计算键 key 属于哪个槽,其中 CRC16(key)语句用于计算 key 的 CRC16 校验和;
12.4、在集群中录入值
在 redis-cli 每次录入、查询键值,Redis 都会计算出该 key 应该送往哪个插槽(slots),如果不是客户端对应的服务器的插槽,Redis 会报错,并告知应前往的 Redis 实例地址和端口;
redis-cli 客户端提供了 -c 参数实现自动重定向;
如 redis-cli -c -p 6379 登入后,再录入、查询键值对可以自动重定向;
不在一个 slot 下的键值,是不能使用 mget,mset 等多键操作。可以通过 { } 来定义组的概念,从而使 key 中 { } 内相同的键值对放到一个 slot 中去;
12.5、查询集群中的值
CLUSTER GETKEYSINSLOTS 返回 count 个 slot 槽中的键;
12.6、故障恢复
如果主节点下线,从节点会自动升为主节点,其中会有 15 秒超时;主节点恢复后,主节点会变为从机;
如果某一段插槽的主机从机全部挂掉了,而 cluster-require-full-coverage 为 yes,那么整个集群都会挂掉,如果该配置项为 no,那么,该插槽数据全部不能使用,也无法存储;
redis.conf 中的参数 cluster-require-full-coverage
12.7、集群的优缺点
优点:
- 实现扩容、分摊压力、无中心配置相对简单
缺点:
- 多键操作是不被支持的;
- 多键的 Redis 事务是不背支持的,lua 脚本不被支持;
十三、分布式锁
13.1、问题描述
随着业务发展的需要,远单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分部在不同的机器上,这将使原单机部署情况下并发控制所策略失效,单纯的 Java API 并不能提供分布式锁的能力。为了解决这个问题就需要一种跨 JVM 的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题;
分布式锁的主流实现方案:
- 基于数据库实现分布式锁;
- 基于缓存 Redis 等;
- 基于 Zookeeper
每一种分布式锁解决方案都有各自的优缺点:
- 性能:Redis 最高;
- 可靠性:Zookeeper 最高;
这里,我们就基于 Redis 实现分布式锁;
13.2、解决方案
使用 Redis 实现分布式锁
Redis 命令
set key value nx px 10000
EX second :设置键的过期时间为 second 秒,SET key value EX second 效果等同于 SETEX key second value;
PX millisecond:设置键的过期时间为 millisecond 毫秒。SET key value PX millisecond 效果等同于 PSETEX key millisecond value;
NX:只在键不存在是,才对键进行设置操作。SET key value NX 效果等同于 SETNX key value;
XX:只在键已经存在时,才对键进行设置操作;
- 多个客户端同时获得锁(setnx);
- 获取成功,执行业务逻辑{从 db 获取数据,放入缓存},执行完成释放锁(del);
- 其他客户端等待重试;
13.3、优化之设置锁的过期时间
设置过期时间有两种方式
- 首先想到的是通过 expire 设置过期时间(缺乏原子性;如果在 setnx 和 expire 之间出现异常,锁也无法释放);
- 在 set 时指定过期时间(推荐);