一、NoSQL初识
1.1 NoSQL
NoSQL(Non-Relational Sql),非关系型数据库,还有一种说法是(Not Only Sql)意为:不仅仅是Sql,NoSQL是一项全新的数据库理念,泛指非关系型数据库,随着互联网web2.0网站的兴起,传统的关系型数据库在应付超大规模的高并发类型的问题时,web2.0网站已显得力不从心,NoSQL数据库的产生就是为了解决超大规模的数据规模以及多种数据类型带来的挑战,尤其是大数据应用难题。
1.2 NoSQL的优势
- 成本低
NoSQL数据库简单易部署,基本上都是开源软件,相比于关系型数据库价格便宜。
- 易扩展
NoSQL数据库种类繁多,但有一个共同的特点,都是去掉关系型数据库的关系型特性,数据之间无相互关系,就非常容易扩展,也无形间在架构层面上带来了课扩展能力。
- 查询速度快
NoSQL数据库是将数据存储在缓存之中,而传统的关系型数据库是将数据存储在磁盘中,所以NoSQL的查询速度远远大于传统的关系型数据库
大数据、高性能
NoSQL数据库都具有非常高的读写性能,尤其在大数据量下,同样表现优秀。这得益于它的数据库的结构简单。 一般MySQL使用Query Cache(查询缓存),每次表的更新Cache就失效,是一种大粒度的Cache,性能不高。 而NoSQL的Cache是记录级的,是一种细粒度的Cache,性能高很多。
灵活的数据模型
NoSQL支持存储的数据模型是key-value形式,也可以是图片,文档等……所以可以存储基础类型以及对象、集合等,NoSQL无需事先为要存储的数据建立字段,可以随时存储自定义的数据格式。
高可用
NoSQL在不太影响性能的情况,就可以方便的实现高可用的架构。
1.3 CAP定理
- C:Consistency(一致性)
所有节点在同一时间具有相同的特性。
- A:Availability(可用性)
保证每个请求不管成功或是失败都有响应。
- P:Partition tolerance(分区容忍)
系统中任意信息的丢失或失败不会影响系统的继续运作
- CAP理论的核心是:一个分布式系统不可能同时很好的满足一致性,可用和分区容错性这三个需求,最多只能同时较好的满足两个
- CA:单点集群,满足一致性,可用性的系统,通常在可扩展性上不太强大。
- CP:满足一致性,分区容忍性的系统,通常性能不是特别高。
- AP:满足可用性,分区容忍性的系统,通常可能对一致性要求低一些。
1.4 NoSQL分类
类型 | 相关产品 | 典型应用 | 数据模型 | 优势 | 劣势 |
---|---|---|---|---|---|
键值(Key-Value) | Tokyo Cabinet/Tyrant、Redis、Voldemort、Berkeley DB | 内容缓存,主要用于处理大量数据的高访问负载。 | 键值对的形式 key-value |
查询速度快 | 存储的数据缺少结构化 |
列族 | Cassandra, HBase, Riak |
分布式的文件系统 | 以列簇式存储,将同一列的数据存储在一起 | 查找速度快,可扩展性强,更容易进行分布式扩展 | 功能相对局限 |
文档 | CouchDB、MongoDB | Web应用(与Key-Value类似,Value是结构化的) | 一系列键值对,类似于json格式存储 | 数据结构要求不严格 | 查询性能不高,而且缺乏统一的查询语法 |
图 | Neo4J、InfoGrid、 Infinite Graph |
社交网络 | 图结构 | 利用图形结构存储 | 需要对整个图做计算才能得出结果,不容易做分布式的集群方案。 |
二、Redis入门
2.1 什么是Redis?
- Redis(Remote Dictionary Server)远程字典服务,是一个开源的,内存中的数据结构存储系统,他可以用作数据库,缓存和消息中间件。
- Redis是使用C语言编写的,支持网络交互的,可基于内存也可持久化的Key-Value数据库,同时还提供了list、set、zset(sorted set)、hash……等数据结构的存储。
- Redis支持数据的备份,即master-slave模式的数据备份。
- Redis是单进程的,默认有16个数据库,分别用0~15表示,0代表第一个数据库。
- Redis的默认端口号是:6379
2.2 Redis的安装方式
2.2.1 Windows的安装方式
2.2.2 Linux的安装安装方式
1)在未来的实际开发过程中都是将项目部署在Linux系统环境下的。
2)首先安装VirtualBox虚拟机,选择Windows host下载并安装。
3)启动VirtualBox,选择管理->导入虚拟电脑->选择导入centos7.ova,这是已经配置好了的虚拟机镜像。
4)设置网络,设置->网络->网卡1->连接方式->界面名称
- 连接方式选择:桥接网络
- 界面名称基于电脑的网络连接方式:
- WIFI连接选择:Intel(R) Wireless-AC 9560 160MHz
- 宽带网线连接选择:Realtek PCIe GbE Family Controller #2
5)启动虚拟机,登录输入用户名(root)和密码(123456)。
6)输入ifconfig(Linux命令)查看ip地址。
7)安装 SecureCRT或XShell建立虚拟机连接,如果是SecureCRT需要修改窗口的编码格式。
8)进入Redis安装目录
cd /usr/local/bin
9)启动Redis服务
./redis-server ~/myredis/redis.conf
10)启动Redis客户端
./redis-cli -p 6379 使用6379端口号启动redis客户端 ./redis-cli -p 6379 —raw 如果需要在客户端中显示中文,需要加—raw
11)验证启动状态,输入ping,返回pong
2.3 基础命令
2.3.1 Linux的基础命令
ifconfig 查看ip地址,用于连接Xshell或secureCRT客户端使用 ll 查看当前目录下的所有文件 cd 进入指定目录 cd .. 返回上级目录 cd~ 返回初始目录 mkdir 目录名 创建文件夹 cp 文件路径 新路径 拷贝文件 vim redis.conf 打开当前文件,可以通过键盘上下键移动光标, . 输入:q退出 . 输入:q! 不保存 输入:wq 保存
2.3.2 Redis的基础命令
1、Redis启动相关
redis-server 启动服务端命令,但是这种方式启动会保持当前窗口 redis-server ~/myredis/redis.conf 使用指定的配置文件启动redis服务,这样可以在当前窗口编辑 redis-cli -p 6379 启动客户端命令,-p表示使用的端口号 exit 退出当前连接,但不会结束当前redis服务 shutdown 退出当前服务,再执行一次exit完全退出redis客户端 redis-benchmark 压力测试,测试在不同命令下,10万次请求在多少时间内完成
2、Redis数据库相关
keys * 查看当前数据库所有的key keys ? 使用通配符的方式使用,匹配查询相似的key select 1 选择相应的数据库 dbsize 展示当前数据库中key的个数 flushdb 删除当前数据库中所有的数据 flushall 删除所有数据库中的数据
3、Redis键值操作相关
exists key 判断该key是否存在 type key 显示当前key所存储的数据类型 expire key n 设置key值的有效期为n秒,过了有效期,这个key就会被删除 pexpire key n 设置key值的有效期为n毫秒,过了有效期,这个key就会被删除 ttl key 查看key还剩余多少有效时间,返回剩余的秒数 ——返回 -2 代表key已经不存在了 ——返回 -1 代表当前key没有设置有效期,为长期有效 pttl key 查看key还剩余多少毫秒数 persist key 设置数据长期有效,用于错误设置了key的有效期后及时进行改正 del key 删除一个key rename key newKey 重命名key值 randomkey 随机出一个key move key dbId 移动指定的key到另一个数据库中
三、Redis的数据类型
3.1 String(字符串)
1、概念:String类型是Redis最基本的数据类型,一个键最大能存储512MB。String类型是二进制安全的。意思是 redis的String可以包含任何数据。
2、原理:redis的字符串是动态字符串,内部结构类似ArrayList。采用预分配冗余空间的方式减少内存的频繁分配。内部为字符串分配的实际空间一般高于字符串长度,当字符串长度<1MB时,扩容方式是直接加倍,如果 >1MB,一次扩容只扩1MB,直到扩大到512MB。
3、常用命令:
strlen 查看字符串长度 append 追加字符串,返回字符串长度
当字符串为整数时,进行数学运算
incr 自增,返回最新的值 decr 自减,返回最新的值 incrby 增加相应的值,返回最新的值 decrby 减少相应的值,返回最新的值
范围内的操作
getrange key start end 获取从初始位置到结束位置的值,从0开始计数,类似于java中的substring setrange key start newStr 设置从其实位置开始,替换指定位置为新的字符串
整合命令
setex key 时间 value set+expire的组合命令,设置值的同时设置有效期 setnx key value set if not exists,判断如果key不存在就设置key getset key value 先取出原来的值,再设置新的值,返回原来的值
批量操作
mget more get,可以同时取出多个key的值,mget key1 key2 key3 mset more set,可以同时设置多个key的值,mset k1 v1 k2 v2 k3 v3 msetnx 同时判断并设置多个值,这是一条命令,需要满足所有key都不存在是才会设置成功
3.2 list(列表)
1、概念:Redis的list列表是一个字符链表,内部结构类似LinkedList。左右两端都可以插入添加。如果key不存在,就会创建一个新的链表,如果key已经存在,会继续往这个链表中新增内容。如果链表中的值全部移除了,那么对应的key也就消失了。每个列表最多可以存储2^32 - 1个元素(4294967295,约40多亿)。
2、原理:底层是一个“快速链表”(quicklist)的结构,在列表元素较少时,使用连续的内存存储成压缩列表ziplist。当数据量较多的时候,改成quicklist,也就是将多个ziplist使用双向指针串起来使用,以减少内存的碎片化。
3、常用命令
lpush key value value… 从左侧存入数据,可同时存储多个数据,类似于栈结构,先进后出 rpush key value value… 从右侧存入数据,可同时存储多个数据,类似于队列结构,先进先出 lpop 从左侧弹出数据 lrange key start end 范围内查看数据 llen key 查看链表长度 lindex 获取某一个位置的值,索引从0开始 lrem key n value 从左边开始遍历,删除n个value值 ltrim key start end 截取链表中某个范围的数据重新赋值给key linsert key before/after oldValue newValue 从左边开始遍历,将数据插入到oldValue的前后 lpushx 将数据插入到左侧的开头,如果key不存在不会创建新的链表 rpush 将数据插入到右侧的开头,如果key不存在不会创建新的链表 lset key index value 可以更改指定位置的值,index从0开始计算
3.3 Hash(哈希)
1、概念:redis的无序字典是一个string类型的field和value的映射表,内部结构类似HashMap。 每个hash 可以存储 2^32 - 1个键值对(40多亿)。
2、原理:底层的实现结构,与HashMap也一样,是“数组+链表”的二维结构,第一维hash的数据位置碰撞时,将碰撞元素用链表串接起来,不同的是,redis字典的值只能是字符串,而且两者的rehash方式不同。java的hashmap是一次全部rehash,耗时较高,redis为了优化性能,采用渐进式rehash策略。具体方式为,同时保留新旧两个hash结构,然后逐步搬迁,搬迁完成后再取而代之。
3、常用命令:
hset key k v 存储数据,以键值对的方式存储 hget key k 获取数据,根据k取得对应的值 hsetnx 如果key不存在则设置新的key值 hmset key k v k v… 批量存储数据,以一个个键值对的方式存储 hmget key k k… 批量获取数据 hkeys key 查看所有的key hvals key 查看所有的value hlen key 查看hash的长度 hgetall key 返回全部的key和value hexists k 判断k是否存在 hdel key k 删除key中某个k值 hincrby key k n 对整数的增加操作,增加n值
3.4 Set(集合)
1、概念:redis的集合是string类型的无序不重复的元素集。同时提供求交集、并集、差集等操作。集合中最大的成员数为 2^32 - 1(40多亿)。
2、原理:类似HashSet,也是通过哈希表实现的,相当于所有的value都是空。通过计算hash的方式来快速排重,也是set能提供判断一个成员是否在集合内的原因。
3、常用命令:
sadd key vaule value… 创建或添加set类型的集合 scard key 查看set的大小 smember key 查看set的所有值 sismember key value 判断该key中是否存在指定的value ——返回1代表存在 ——返回2代表不存在 srem key value value 删除key中指定的value srandmember key n 可以在set中随机出n个元素 spop key n 可以随机弹出n个值 smove key1 key2 value 将value从key1移动到key2 sinter key1 key2 求两个集合的交集 sdiff key1 key2 返回在key1中存在但是在key2中不存在的值,即两个集合的差集 sunion key1 key2 求两个集合的并集
3.5 ZSet(有序集合)
1、概念:redis的有序集合和set一样也是string类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个 double类型的分数。redis正是通过分数来为集合中的成员进行从小到大排序。zset的成员是唯一的,但分数 (score)却可以重复。
2、原理:类似于SortedSet和HashMap的结合,内部实现是一种叫“跳跃列表”的数据结构。通过层级制,将元素分层并串联,每隔几个元素选出一个代表,再将代表使用另外一级指针串起来,以此类推,形成金字塔结构。同一个元素在不同层级之间身兼数职,是为“跳跃”。新元素插入时,逐层下潜,直到找到合适的位置。
3、常用命令:
zadd key score1 value1 score2 value2… 创建或增加zset的元素值,每一个元素值都包含<分数,值> zrange key start end 查看指定范围内的元素值,加上withscore可以附带分数 zrangebyscore key minScore maxScore 查看指定分数范围内的元素值,默认代表闭区间 . 在分数前加上 “(“ 代表开区间,支持limit分页 . limit offset(偏移量) num(返回的数量) zrem key value 删除指定的value值 zcard key 统计元素个数 zcount key minScore maxScore 统计指定分数范围内的元素个数 zscore key member 查询指定成员的分数 zrank key member 查询指定成员的索引位置 zrevrank key member 逆序返回成员的索引位置,默认是从低到高,逆序代表排名 zrevrange key 默认从小到大排序,逆序后从大到小排序
3.6 GEO(位置信息)
1、概念:GEO,可以将用户给定的地理位置信息储存起来。名字取自业界通用的地理位置距离排序算法GeoHash,将二维的经纬度数据映射到一维的整数,也就是挂载到一条线上,方便计算两点之间的距离。实际的内部结构是zset。
2、原理:映射算法,将地球看成一个二维平面,划分成一系列正方形的方格,所有地图坐标都被放置于唯一的方格中,然 后进行整数编码(如切蛋糕法),编码越接近的方格距离越近。
3、常用命令:
geoadd key 经度 维度 位置名 通过经纬度存储位置信息,一个key中可以存储多个位置信息 geodist key 位置1 位置2 距离单位 查询两个地点之间的相对距离 . 距离单位:m(米)、km(千米)、mi(英里)、ft(英尺) geopos key member 查询成员的经纬度数据 geohash 进行hash编码,得到编译结果 georadius 以某一个中心经纬度的位置为中心,画一个指定距离的半径,返回集合中满足条件的地址 . 使用方式:georadius key 中心经度 中心维度 半径距离 半径单位 . 可选参数:withdist(返回距离)、withcoord(返回经纬度)、withhash(返回哈希编码)
3.7 Bitmap(位图)
1、概念:BitMap就是一个byte数组,元素中每一个 bit位用来表示元素在当前节点对应的状态,实际上底层也是通过对字符串的操作来实现,对应开发中boolean类型数据的存取。
2、原理:位数组是自动扩展的,可以直接得到字符串的ascii码,是为整存零取,也可以零存零取或零存整取。如果对应 的字节是不可打印字符,会显示该字符的十六进制。
3、常用命令:
setbit key index 0/1 设置key的某个位置的值为0或1 getbit key index 获取某个位置的值 get key 零存整取,直接获取二进制字符数组对应的字符串 bitcount key 查询数组中有多少个1
3.8 HyperLogLog(基数统计)
1、概念:Redis 的基数统计,这个结构可以非常省内存的去统计各种计数。它是一个基于基数估算的算法,但并不绝对准确,标准误差是0.81% 。HyperLogLog的发明人是Philippe Flajolet,pf是名字首字母缩写。
2、原理:HyperLogLog最大占用12KB的存储空间。当计数比较小时,使用稀疏矩阵存储,占用空间很小,在变大到超过 阈值时,会转变成稠密矩阵,占用12KB。
3、算法:给定一系列的随机整数,记录低位连续0位的最大长度k,通过k可以估算出随机数的数量N。
4、常用命令:
pfadd key k 增加数据 pfcount key 统计数据
3.9 Stream(流)
1、概念:进程间的一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接受消息。
2、常用命令:
subscribe channel 订阅频道的命令,会持续接收到消息 publish channel message 在某个频道发布消息,相应的订阅者会接收到消息
生成消息的命令:
xadd 生成消息,返回消息id(时间戳+顺序),顺序指的是该毫秒下产生的第几条消息 xlen 查看消息列表的长度 xrange key - + 遍历所有消息 xdel key 消息id 通过消息id删除消息
读取消息的命令:
xread streams key 0-0 遍历读取所有消息 . 支持参数:count num 可以指定返回消息的数量 . 支持参数:block num 可以阻塞等待消息的返回, . 此时0-0应该替换为$,代表从尾部接收
四、Redis配置文件
4.1 redis.conf
4.1.1 单位
在redis.conf配置文件中指出:单位是之间是不同的取值,没有B的时候取整
1k = 1000b 1kb = 1024b 1M = 1000kb 1MB = 1024kb 1G = 1000MB 1GB = 1024MB
4.1.2 网络相关
bind 127.0.0.1 绑定ip地址,能够访问服务端的地址,当前redis服务只能被本机访问 protect-mode yes 开启保护模式,当bind没有配置且登录不需要密码时,启动保护模式, . 保护模式下只能被本地访问 port 6379 redis服务的端口号 timeout 0 客户端超时时间,单位秒,0代表一直保持 tcp-keepalive 300 单位秒,每300秒去检查一次客户端是否健康,避免服务端阻塞 tcp-backlog 511 队列数量(未完成握手和已完成握手的)
4.1.3 通用相关
daemonize no 后台运行开关,改为yes后重启redis验证 . redis启动指定配置文件的方式为:redis-server /root/myredis/redis.conf pidfile /var/run/redis_6379.pid 当守护进程开启时,写入进程id的文件地址 loglevel notice 日志等级,四种级别,notice代表生产环境 logfile “” 日志储存的位置 databases 16 初始化数据库是16个
4.2 安全和限制
config命令是一种从客户端查看配置信息的命令,使用config get xxx 属于危险命令,可以限制使用
安全校验配置:requirepass 代表是否设置密码
如果需要设置密码:config set requirepass 密码
此时设置密码后,输入命令前就需要校验密码:auth 密码,然后才能执行其他操作
恢复初始状态,只需要重新将密码设置为空字符串即可:config set requirepass “”
4.2.1 危险命令限制
危险命令包括:config、flushdb、flushall、keys
使用rename-command + 命令 + “”,将此命令重命名,置为空字符串代表禁用这些命令
4.2.2 其他限制
maxclients 10000 客户端并发数的限制
maxmemory 最大内存
maxmemory-policy 缓存淘汰策略,默认值noeviction(不删除只报错)
其他策略主要分为两种情况:
allkeys 所有键值都可能被删除
volatile 只删除设置了过期时间的键值
五、Redis持久化
持久化——将数据(如内存中的对象)保存到可永久保存的存储设备中。 <br />方式一之RDB:在指定的时间间隔内对数据进行快照存储。先将数据集写入临时文件,写入成功后, 再替换之前的文件,用二进制压缩存储,是一次的全量备份。 <br />方式二之AOF:以日志文本的形式记录服务器所处理的每一个数据更改指令,然后通过重放来恢复 数据,是连续的增量备份。
5.1 RDB(Redis DataBase)
5.1.1 命令触发
- save,会阻塞当前Redis服务器,直到持久化完成,线上应该禁止使用。
bgsave,该触发方式会fork一个子进程,由子进程负责持久化过程,因此阻塞只会发生在fork子进程的时候。
5.1.2 自动触发
根据我们的 save m n 配置规则自动触发;
- 从节点全量复制时,主节点发送rdb文件给从节点完成复制操作,主节点会触发 bgsave;
- 执行 debug reload 时;
-
5.1.3 RDB在redis.conf中的配置
5.1.4 RDB-Fork原理
执行RDB时,服务器执行以下操作: redis调用系统函数fork() ,创建一个子进程
- 子进程将数据集写入到一个临时 RDB 文件中
- 当子进程完成对临时RDB文件的写入时,redis 用新的临时RDB 文件替换原来的RDB 文件,并删 除旧 RDB 文件。 执行fork时,操作系统会使用写时复制(copy- on-write)策略,即fork函数发生的一刻父子进 程共享同一内存数据,当父进程要更改其中某片数 据时(如执行一个写命令 ),操作系统会将该片 数据复制一份以保证子进程的数据不受影响。新的RDB文件存储的是执行fork那一刻的内存数据。
- 在进行快照的过程中不会修改RDB文件,只有快 照结束后才会将旧的文件替换成新的。任何时候 RDB文件都是完整的。
5.1.5 RDB性能分析
优点:
- 通过rdb文件恢复数据比较快。
- rdb文件非常紧凑,适合于数据备份。
- 通过RDB进行数据备份,由于使用子进程生成,所以对Redis服务器性能影响较小。
缺点:
- 采用RDB的方式可能会造成某个时段内数据的丢失,比如还没达到 触发条件时服务器死机了,那么这个时间段的数据会丢失。
- 使用save命令会造成服务器阻塞,直接数据同步完成才能接收后 续请求。
- 使用bgsave命令在forks子进程时,如果数据量太大,forks的过 程也会发生阻塞,另外,forks子进程会耗费内存。
5.2 AOF (Append Only File)
以日志的形式记录服务器所处理的每一个更改操作,但操作过多,aof文件过大时,加载文件恢复数据会非常慢,为解决这个问题,Redis支持aof文件重写,通过重写aof,可以生成一个恢复当前数据的最少命令集。所以整个流程大体分为两步:一是命令的实时写入,二是对aof文件的重写。5.2.1 命令写入流程
命令写入 -> 追加到aof_buf -> 同步到aof磁盘 (考虑到磁盘IO心性能增加了缓冲)5.2.2 命令触发
命令触发:bgrewriteaof5.2.3 自动触发
根据配置规则来触发 (整体时间还跟Redis的定时任务频率有关系)
在写入aof日志文件时,如果redis服务器宕机,则日志文件会出格式错误,重启服务器时,服务器 会拒绝载入这个aof文件,此时可以通过以下命令修复aof并恢复数据。
redis -check-aof -fix file.aof 修复aof文件并恢复数据
5.2.4 AOF在redis.conf中的配置
5.2.5 AOF原理
- 在重写期间,由于主进程依然在响应命令, 为了保证最终备份的完整性;因此它依然 会写入旧的AOF file中,如果重写失败, 能够保证数据不丢失。
- 为了把重写期间响应的写入信息也写入到 新的文件中,因此也会为子进程保留一个 buf,防止新写的file丢失数据。
- 重写是直接把当前内存的数据生成对应命 令,并不需要读取老的AOF文件进行分析、 命令合并。
AOF文件直接采用的文本协议,主要是兼 容性好、追加方便、可读性高可认为修改 修复。
5.2.6 AOF性能分析
优点:
AOF只是追加日志文件,因此对 服务器性能影响较小,速度比 RDB要快,消耗的内存较少。
- 数据安全性较高。
缺点:
- AOF方式生成的日志文件太大,即使通过AFO重写,文件体积仍然很大。
- 恢复数据的速度比RDB慢。
5.3 Redis持久化注意事项
- 当RDB与AOF两种方式都开启时,Redis启动时会优先使用AOF日志来恢复数据,因为AOF保存的文件比RDB文件更完整。
- Redis4.0提供了一个混合持久化的选择,将rdb文件的内容和增量的aof日志存在一起,重启时先加载rdb,再重放aof,以达到最大效率。
六、Redis管道与事务
6.1 管道
- Redis的管道技术是客户端提供的,与服务器无关,服务器始终使用收到-执行-回复的顺序处理消息,而客户端通过对管道的指令改变读写顺序,而节省大幅的IO时间,指令越多,效果越好。
- 管道测试的命令:redis-benchmark
-
6.2 事务
一个成熟的数据库都要支持事务,以保障多个操作的原子性,同时,事务还能保证在一个事务中命令依次执行,而不会被其他命令插入。所有事务的基本用法都是包含三个部分,开始事务(begin),提交事务(commit),回滚事务(rollback)。
- 但是Redis对事务的处理是:multi(开启事务),exec(执行事务),discard(取消事务),Redis可以使用discard命令来取消事务,但是不支持事务的回滚。
- 当输入MULTI命令后,服务器返回OK表示事务开始成功,然后依次输入需要在本次事务中执行的所有命令,每次 输入一个命令服务器并不会马上执行,而是返回“QUEUED”,这表示命令已经被服务器接受并且暂时保存起来, 最后输入EXEC命令后,本次事务中的所有命令才会被依次执行。
Redis事务对错误的处理:
可以使用WATCH命令来监测一个或多个键,一旦在事务开启的过程中,有一个或多个被监测的键的值被修改或删除,那么之后的事务都不会执行,监控一直持续到EXEC命令,执行EXEC命令之后会取消监控使用WATCH命令监控的键,如果不想执行事务中的命令,也可以使用UNWATCH命令来取消监控。
- 使用方式分为如下步骤:watch -> multi -> command -> exec
- 注意:由于WATCH命令的作用只是当被监控的键被修改后取消之后的事务,并不能保证其他客户端不修改监控的 值,所以当EXEC命令执行失败之后需要手动重新执行整个事务。
- 事务的本质其实是一种乐观锁。
七、Redis分布式锁
7.1 乐观锁与悲观锁
7.1.1 乐观锁
乐观锁(Optimistic Lock), 就是很乐观,每次去拿数据的时候都认为别人不会修改。所以不会上锁,不会上锁! 但是如果想要更新数据,则会在更新前检查在读取至更新这段时间别人有没有修改过这个数据。如果修改过,则重 新读取,再次尝试更新,循环上述步骤直到更新成功。
7.1.2 悲观锁
悲观锁(Pes simis tic Lock), 就是很悲观,每次去拿数据的时候都认为别人会修改。所以每次在拿数据的时候都 会上锁。这样别人想拿数据就被挡住,直到悲观锁被释放。
7.1.3 乐观锁VS悲观锁
- 悲观锁阻塞事务,乐观锁回滚重试。
- 乐观锁适用于写比较少的情况下,即冲突很少发生时,可以省去锁的开销,加大了系统吞吐量。
悲观锁适用于写比较多的情况下,因为如果乐观锁经常冲突,应用要不断进行重试,反倒降低性能。
7.1.4 CAS算法
CAS算法,Compare-and-Swap,即比较并替换,也有叫做Compare-and-Set的,比较并设置。
- 比较:读取到了一个值A,在将其更新为B之前,检查原值是否仍为A(未被其他线程改动)。
- 设置:如果是,将A更新为B,结束。 如果不是,则什么都不做。
- 允许多个线程同时读取(因为根本没有加锁操作),但是只有一个线程可以成功更新数据,并导致其他要更新数据 的线程回滚重试。 也叫非阻塞同步(Non-block ing Synchronization)。
- 乐观锁策略也被称为无锁编程。换句话说,乐观锁其实不是“锁”,它仅仅是一个循环重试CAS的算法而已!
- 乐观锁的缺点——ABA 问题:如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 “ABA”问题。
7.2 分布式锁
- 在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。有时,我们需要保证一个方法在同一时间内只能被同一个线程执行。在单机环境中,Java中其实提供了很多并发处 理相关的API,但是这些API在分布式场景中就无能为力了。也就是说单纯的Java Api并不能提供分布式锁的能力。 所以针对分布式锁的实现目前有多种方案。
- 分布式锁是控制分布式系统之间同步访问共享资源的一种方式。
- 分布式锁一般有三种实现方式:
- 数据库乐观锁;
- 基于Redis的分布式锁;
- 基于ZooKeeper的分布式锁。
- 分布式锁的可靠性需满足以下四个条件:
- 互斥性。在任意时刻,只有一个客户端能持有锁。
- 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
- 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
- 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
7.3 Setnx+Lua
使用Redis实现分布式锁原理:
- Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系, 基于此,Redis中可以使用SETNX命令实现分布式锁。
- SETNX——SET if Not eXist s(如果不存在,则设置)
- 若给定的 key 已经存在,则 SETNX 不做任何动作;如果需要解锁,使用 key 命令就能释放锁
八、Redis集群
8.1 主从模式
为了保证高可用,redis-cluster集群引入了主从模式,一个主节点对应一个或者多个从节点,当主节点宕机的时候,就会启用从节点。 当其它主节点ping一个主节点A时,如果半数以上的主节点与A通信超时,那么认为主节点A宕机了。如果主节点A和它的从节点A1都宕机了,那么该集群就无法再提供服务了。
优点:
- 支持主从复制,主机会自动将数据同步到从机,可以进行读写分离
- 为了分载Master的读操作压力,Slave服务器可以为客户端提供只读操作的服务,写服务仍然必须由Master来完成
- Slave同样可以接受其它Slaves的连接和同步请求,这样可以有效的分载Master的同步压力。
缺点:
- Redis不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的IP才能恢复。
- 主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性。
- Redis较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂
8.2 主从复制
在Redis中,用户可以通过执行SLAVEOF命令或者设置slaveof选项,让一个服务器去复制(replicate)另一个服务器,我们称呼被复制的服务器为主服务器(master),而对主服务器进行复制的服务器则被称为从服务器(slave)。配置的口诀为:配从不配主。
假如,现在有两个服务器:
127.0.0.1:6379
127.0.0.1:12345
此时我们向127.0.0.1:12345服务器发送命令: SLAVEOF 127.0.0.1 6379,返回OK
表示,我们在从服务器配置了主服务器的ip地址和端口号,表示现在12345端口号的服务器已经挂载到了6379端口号的服务器上了,现在6379端口号的服务器是主服务器(master),而12345端口号的服务器是从服务器(slave)。
8.3 哨兵模式
当主服务器中断服务后,可以将一个从服务器升级为主服务器,以便继续提供服务,但是这个过程需要人工手动来操作。 为此,Redis 2.8中提供了哨兵工具来实现自动化的系统监控和故障恢复功能。
哨兵的作用就是监控Redis系统的运行状况。它的功能包括以下两个:
- 监控主服务器和从服务器是否正常运行。
- 主服务器出现故障时自动将从服务器转换为主服务器。
哨兵模式的优缺点:
- 优点:哨兵模式是基于主从模式的,所有主从的优点,哨兵模式都具有。主从可以自动切换,系统更健壮,可用性更高。
- 缺点:Redis较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。
哨兵模式的使用方式:
- 新建sentinel. conf文件
文件内容填写如下:
sentinel monitor master6379 127.0.0.1 6379 1 // master6379 是自定义的数据库名称 // 1代表 如果主机master宕机后,从机slave的票数超过1就会被票选为master
启动哨兵工具:
redis-sentinel /root/myredis/sentinel. conf
角色的查看命令
info replication
如果master宕机后重启回来,会担任slave从机的角色继续运行
九、Redis客户端
Redis的各种语言客户端列表,请参见https://redis.io/clients。其中Java客户端在github上start最高的是Jedis和Redisson。Jedis提供了完整Redis命令,而Redisson有更多分布式的容器实现。
首先需要引入Jedis的依赖:
<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.7.0</version>
</dependency>
关于jedis的连接问题,如果连接超时,在虚拟机中执行如下命令:
/sbin/iptables -I INPUT -p tcp —dport 6379 -j ACCEPT
9.1 Jedis的基本使用
public class TestJedis {
public static void main(String[] args) {
//创建Redis服务,通过IP地址和端口号
Jedis jedis = new Jedis("192.168.31.215",6379);
//获取所有key
Set<String> keys = jedis.keys("*");
System.out.println("keys = " + keys);
//设置key值
jedis.set("ocean","xiaojiang");
//关闭服务
jedis.close();
}
}
9.2 List的基本使用
public class RedisList {
public static void main(String[] args) {
//创建Jedis服务,通过IP地址和端口号
Jedis jedis = new Jedis("192.168.31.215",6379);
//设置List类型的值
jedis.lpush("jedisList","key1","key2","key3");
//获取List类型的值
List<String> jedisList = jedis.lrange("jedisList", 0, -1);
//遍历List
for (String s : jedisList) {
System.out.println("s = " + s);
}
//关闭Jedis服务
jedis.close();
}
}
9.3 JedisPool的使用
public class TestJedisPool {
public static void main(String[] args) {
//创建Jedis连接池配置
JedisPoolConfig config = new JedisPoolConfig();
//设置最大连接为10个
config.setMaxTotal(10);
//创建Jedis连接池
JedisPool pool = new JedisPool(config,"192.168.31.215", 6379);
Jedis jedis = null;
try {
//通过连接池获取Jedis服务
jedis = pool.getResource();
//设置键值
jedis.set("pool","this is pool");
//获取键值
System.out.println("pool = " + jedis.get("pool"));
} catch (Exception e) {
e.printStackTrace();
} finally {
if (jedis != null) {
jedis.close();
}
}
//关闭连接池
pool.close();
}
}
9.4 JedisPool的常用配置
#最大活动对象数
redis.pool.maxTotal=1000
#最大能够保持idel状态的对象数
redis.pool.maxIdle=100
#最小能够保持idel状态的对象数
redis.pool.minIdle=50
#当池内没有返回对象时,最大等待时间
redis.pool.maxWaitMillis=10000
#当调用borrow Object方法时,是否进行有效性检查
redis.pool.testOnBorrow=true
#当调用return Object方法时,是否进行有效性检查
redis.pool.testOnReturn=true
#“空闲链接”检测线程,检测的周期,毫秒数。如果为负值,表示不运行“检测线程”。默认为-1.
redis.pool.timeBetweenEvictionRunsMillis=30000
#向调用者输出“链接”对象时,是否检测它的空闲超时;
redis.pool.testWhileIdle=true
# 对于“空闲链接”检测线程而言,每次检测的链接资源的个数。默认为3.
redis.pool.numTestsPerEvictionRun=50
#redis服务器的IP
redis.ip=xxxxxx
#redis服务器的Port
redis1.port=6379
9.5 Jedis对管道的基本使用
public class JedisPipeTest {
public static void main(String[] args) {
Jedis jedis = new Jedis("192.168.31.215",6379);
String key = "pipe1";
// 初始化为数值时 仍传输字符串类型
jedis.set(key,"10");
//使用管道
Pipeline pipeline = jedis.pipelined();
for (int i = 0; i < 10; i++) {
pipeline.incr(key);
}
//统一提交的命令
pipeline.sync();
System.out.println(jedis.get(key));
//关闭服务
jedis.close();
}
}
9.6 Jedis对事务的基本使用
public class JedisTransferTest {
public static void main(String[] args) {
//开启Redis服务
Jedis jedis = new Jedis("192.168.31.215",6379);
//设置两个初始值
jedis.set("zhangsan","1000");
jedis.set("lisi","500");
//监控这两个key
jedis.watch("zhangsan","lisi");
//转账业务,希望张三转账给李四200元
if (Integer.parseInt(jedis.get("zhangsan")) >= 200) {
//开启事务
Transaction t = jedis.multi();
//业务逻辑
t.decrBy("zhangsan",200);
t.incrBy("lisi",200);
//确定执行
t.exec();
}
//验证结果
List<String> moneyList = jedis.mget("zhangsan", "lisi");
for (String s : moneyList) {
System.out.println("s = " + s);
}
//关闭服务
jedis.close();
}
}