Redis 基础以及进阶的两篇已经全部更新好了,为了字数限制以及阅读方便,分成两篇发布。(算是对一年多前 Redis 文章的一篇重制哇)
如果觉得移动端阅读对于表格等一些内容支持不是很好,可以访问我的博客(稍后一些会更新)或者 掘金社区的账号(BWH_Steven)
博客:www.ideal-20.cn
掘金:
https://juejin.cn/post/6921509748273577998
本篇主要内容为:NoSQL 引入 Redis ,以及在 Linux7 环境下的安装,配置,以及总结了非常详细的类型,用法以及例子,最后通过 Jedis 以及 Springboot 中的 RedisTemplate 在 IDEA 中远程操作 Redis。
第二篇会主要涉及到配置文件,发布订阅,主从复制,哨兵等一些进阶用法的一个基本使用。
最近在参加 2020 年度创作者榜单打榜,【昵称为:BWH_Steven】 希望大家可以支持一下哈~(微信投票 + APP端投票,每天都可以投票,手中的票数根据日期会变多喔!)
Redis是一个使用ANSI C编写的开源、支持网络、基于内存、可选持久性的键值对存储数据库 ——维基百科
可以简单的说,Redis就是一款高性能的NoSQL数据库
我们前面所学习的MySQL数据库是典型的的SQL数据库也就是传统的关系型数据库,而我们今天学习的Redis数据库则是一款NoSQL数据库,也叫作非关系型数据库,它与我们熟悉的MySQL等的概念完全是不一样的,它是一项全新的数据库理念,我们帖一组百度百科的解释
NoSQL,泛指非关系型的数据库。随着互联网web2.0网站的兴起,传统的关系数据库在处理web2.0网站,特别是超大规模和高并发的SNS类型的web2.0纯动态网站已经显得力不从心,出现了很多难以克服的问题,而非关系型的数据库则由于其本身的特点得到了非常迅速的发展。NoSQL数据库的产生就是为了解决大规模数据集合多重数据种类带来的挑战,尤其是大数据应用难题 ——百度百科
说明:我们现在所看到的的博客,RSS,P2P,微博,抖音等均属于 Web2.0的产物,Web2.0相比较过去的Web1.0更加注重于用户的交互,用户不仅可以浏览,还可以上传一些资源到网站上,例如图片文字或者说短视频等,使得用户也参与到了网站内容的制造中去了
有人会说,NoSQL = Not SQL ,但是我更倾向这样理解 NoSQL = Not only SQL ,我们不能以一个绝对的结论来判定两项技术的好坏,每一项技术的产生都有其特定的原因,在我看来,NoSQL更适合作为SQL数据库的补充,由于海量数据的出现,性能的要求高了起来,而NoSQL这种产物,对于结构简单但是数据量大的数据处理起来要比传统的SQL快很多,但是同样的,其逻辑运算就必须很简单,否则它也是力不从心的
在我看来,可以简单的说,NoSQL就是以功能换取性能,但是需要处理复杂的业务逻辑还需要使用关系型数据库,所以说想要在模型中完全用NoSQL替代SQL是不现实的,两者更像是互补的关系
SQL的好处:
市面上的NoSQL产品非常多,我们今天所要介绍的就是其中一款基于键值存储的数据库——Redis
分类 | Examples举例 | 典型应用场景 | 数据模型 | 优点 | 缺点 |
---|---|---|---|---|---|
键值(key-value) | Tokyo Cabinet/Tyrant, Redis, Voldemort, Oracle BDB | 内容缓存,主要用于处理大量数据的高访问负载,也用于一些日志系统等等。 | Key 指向 Value 的键值对,通常用hash table来实现 | 查找速度快 | 数据无结构化,通常只被当作字符串或者二进制数据 |
列存储数据库 | Cassandra, HBase, Riak | 分布式的文件系统 | 以列簇式存储,将同一列数据存在一起 | 查找速度快,可扩展性强,更容易进行分布式扩展 | 功能相对局限 |
文档型数据库 | CouchDB, MongoDb | Web应用(与Key-Value类似,Value是结构化的,不同的是数据库能够了解Value的内容) | Key-Value对应的键值对,Value为结构化数据 | 数据结构要求不严格,表结构可变,不需要像关系型数据库一样需要预先定义表结构 | 查询性能不高,而且缺乏统一的查询语法。 |
图形(Graph)数据库 | Neo4J, InfoGrid, Infinite Graph | 社交网络,推荐系统等。专注于构建关系图谱 | 图结构 | 利用图结构相关算法。比如最短路径寻址,N度关系查找等 | 很多时候需要对整个图做计算才能得出需要的信息,而且这种结构不太好做分布式的集群方案。 |
我们在一开始提到了,Redis就是一款高性能的NoSQL数据库,那么它的应用场景是什么呢?
由于其基于键值存储,那么可以支持的存储的类型有什么呢?
说明:
推荐使用 Linux 进行部署,所以我们后面也会详细介绍 Linux 中的安装配置方式,但是如果只是想快速学习语法,也可以勉强使用 Windows 版本,安装会简单很多。
Redis is written in ANSI C and works in most POSIX systems like Linux,
*BSD
, and OS X, without external dependencies. Linux and OS X are the two operating systems where Redis is developed and tested the most, and we recommend using Linux for deployment . Redis may work in Solaris-derived systems like SmartOS, but the support is best effort. There is no official support for Windows builds. 官网说明地址:https://redis.io/topics/introduction
官网:https://redis.io(推荐)
中文网:http://www.redis.net.cn
# 下载 redis-6.0.9 压缩包
wget http://download.redis.io/releases/redis-6.0.9.tar.gz
补充:
/home
目录下一般来说,我们程序都会放在 /opt
目录下,所以我们先将这个压缩文件移动过去再解压
# 移动此文件到根目录下的 opt 目录中
mv redis-6.0.9.tar.gz /opt
# 解压此文件
tar -zxvf redis-6.0.9.tar.gz
解压后 opt 目录下就多出一个 redis-6.0.9 的文件夹,我们打开它,就可以看到一些文件在其中,其中 redis.conf 是我们一会要用的配置文件,暂时先不理会
解压后的文件貌似也不能运行啊,这是当然的,因为这些文件还没有经过编译和安装,在编译之前,首先要检查一下 GCC 的版本
如果你选择的是 Redis 6 以上的版本,例如这里选择的 6.0.9,你的 gcc 版本如果太低就会导致后面编译出错,最起码你的 gcc 要到 5.3 的版本以上
如果没有 gcc 先进行安装
yum -y install gcc
yum -y install gcc-c++
安装完成后,通过 gcc -v
查看到安装到的版本是 4.x.x 版本的,所以要升级,旧版本的 Redis 可以不去做升级这一步
依次执行下面每一条命令
# 升级到gcc 9.3
yum -y install centos-release-scl
yum -y install devtoolset-9-gcc devtoolset-9-gcc-c++ devtoolset-9-binutils
# scl命令启用只是临时的,退出shell或重启就会恢复原系统gcc版本
scl enable devtoolset-9 bash
# 长期使用 gcc 9.3 还需要进行如下操作
echo "source /opt/rh/devtoolset-9/enable" >>/etc/profile
source /etc/profile
查看一下更新后的版本
依次执行编译和安装,
# 编译
make
# 安装
make install
make 会慢一下,耐心等待一下,如果出了错误,一般都是 gcc 的问题
安装后的内容一般都在 /usr/local/bin
下
我们把原来的配置文件就放在那个解压文件中,我们自己用的,单独复制一份出来,方便我们操作和更改
我们先去 /usr/local/bin
中创建一个新的文件夹,然后把之前解压后的文件夹中的 redis.conf
拷贝过来
# 跳转到指定目录下
cd /usr/local/bin
# 新建一个文件夹
mkdir myconfig
# 复制 /opt/redis-6.0.9/redis.conf 到 当前目录的 myconfig 文件夹下
cp /opt/redis-6.0.9/redis.conf myconfig
看一下过程
为了保证我们的redis可以后台运行,我们去编辑拷贝过来的 redis.conf 配置文件
vim redis.conf
在其中找到 daemonize no
将 no 修改为 yes,保存退出
下面先运行一下其服务端(保证当前在 usr/local/bin
目录下)
# 运行服务端
redis-server myconfig/redis.conf
接着运行其客户端
# 运行客户端
redis-cli -p 6379
可以简单测试一下,例如 set get 一下,能拿到值就代表成功了
先看一下运行中时,进程的存在情况
# 查看redis 进程
ps -ef|grep redis
在客户端中,可以通过 shutdown 和 exit 执行关闭(这个是在Redis客户端中执行)
# 关闭
127.0.0.1:6379> shutdown
not connected> exit
# 再次查看一下进程状况
[root@centos7 bin]# ps -ef|grep redis
我们可以去github中寻找windows版本,不过版本会有所滞后,官方起码是没有支持更新的,可能微软还想着能拽他一把。最新的版本好像也都是好几年前的了
https://github.com/microsoftarchive/redis/releases
解压即可用:分别启动 redis-server.exe 和 redis-cli.exe 就能直接测试使用了吗,有问题修改redis.windows.conf 配置文件
redis-server [--port 6379]
有时候参数会过多,建议使用配置文件启动
redis-server [xx/redis.conf]
例如:redis-server myconfig/redis.conf
redis-cli [-h 127.0.0.1 -p 6379]
例如 :redis-cli -p 6379
在客户端中(标志有 127.0.0.1:6379>)直接输入 shutown 等即可
# 关闭
127.0.0.1:6379> shutdown
not connected> exit
若在目录中(前面为 $
等),可以执行
redis-cli shutdown
kill redis-pid
返回 PONG 即连通了
127.0.0.1:6379> ping
PONG
注:每一种类型的存储方式是不太一样的,所以这里的操作不会讲到添加存储,下面会在每种类型中详细讲解。
只是想简单先测试,可以先暂时用这几个命令(这是 String 类型的)
set key value
添加set test ideal-20
get key
获取到值127.0.0.1:6379> keys *
1) "test"
2) "test2"
*
作为通配符,表示任意字符,因为其会遍历所有键,然后显示所有键列表,时间复杂度O(n),数据量过大的环境,谨慎使用127.0.0.1:6379> dbsize
(integer) 2
127.0.0.1:6379> exists test
(integer) 1
127.0.0.1:6379> type test
string
127.0.0.1:6379> move test2 3
(string) 1
127.0.0.1:6379> select 3
OK
127.0.0.1:6379[3]> keys *
1) "ideal-20-2"
127.0.0.1:6379> del test2
(integer) 1
127.0.0.1:6379> expire test 120
(integer) 1
127.0.0.1:6379> ttl test
(integer) 116
127.0.0.1:6379> persist test
(integer) 1
127.0.0.1:6379> ttl test
(integer) -1
127.0.0.1:6379> rename test ideal
OK
127.0.0.1:6379> keys *
1) "ideal"
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> flushall
OK
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379>
127.0.0.1:6379> set address beijing 5000
OK
127.0.0.1:6379> get address
“beijing”
127.0.0.1:6379> del address
(string) 1
如果字符串中的值为数字类型,可以进行递增递减,其它类型会报错
127.0.0.1:6379> set age 21
OK
127.0.0.1:6379> incr age # 递增
(integer) 22
127.0.0.1:6379> incrby age 5 # 递增 5
(integer) 27
127.0.0.1:6379> decr age # 递减
(integer) 26
127.0.0.1:6379> decrby age 5 # 递减 5
(integer) 21
127.0.0.1:6379> set ideal hello
OK
127.0.0.1:6379> append ideal ,ideal-20 # 追加内容
(integer) 14
127.0.0.1:6379> get ideal
"hello,ideal-20"
127.0.0.1:6379> get ideal
"hello,ideal-20"
127.0.0.1:6379> getrange ideal 0 3
"hell"
127.0.0.1:6379> getrange ideal 0 -1
"hello,ideal-20"
127.0.0.1:6379> get ideal
"hello,ideal-20"
127.0.0.1:6379> setrange ideal 6 bwh # 从下标为6的位置开始替换
(integer) 14
127.0.0.1:6379> get ideal
"hello,bwhal-20"
127.0.0.1:6379> strlen addr1
(integer) 7
127.0.0.1:6379> setnx address guangdong # address键 不存在,则创建
(integer) 1
127.0.0.1:6379> get address
"guangdong"
127.0.0.1:6379> setnx address beijing # address键 存在,则失败
(integer) 0
127.0.0.1:6379> get address
"guangdong"
127.0.0.1:6379> mset addr1 beijing addr2 guangdong addr3 shanghai # 同时存储多个值
OK
127.0.0.1:6379> keys *
1) "addr3"
2) "addr2"
3) "addr1"
127.0.0.1:6379> mget addr1 addr2 addr3 # 同时获取多个值
1) "beijing"
2) "guangdong"
3) "shanghai"
127.0.0.1:6379> msetnx age1 20 age2 25 age3 30 # 第一次同时存储多个值(保证不存在)
(integer) 1
127.0.0.1:6379> msetnx age4 35 age5 40 age1 45 # 第二次同时存储多个值(保证不存在),失败了
(integer) 0
127.0.0.1:6379>
127.0.0.1:6379> set user:1 {name:zhangsan,age:20} # 存一个对象
OK
127.0.0.1:6379> keys *
1) "user:1"
127.0.0.1:6379> get user:1
"{name:zhangsan,age:20}"
127.0.0.1:6379> mset user:1:name lisi user:1:age 25
OK
127.0.0.1:6379> mget user:1:name user:1:age
1) "lisi"
2) "25"
127.0.0.1:6379> getset addr beijing # 原先没有值,返回 nil
(nil)
127.0.0.1:6379> get addr
"beijing"
127.0.0.1:6379> getset addr guangdong # 原先有值,返回原先的值,然后覆盖新值
"beijing"
127.0.0.1:6379> get addr
"guangdong"
下面演示添加到左边的,右边的是一样的就不演示了
127.0.0.1:6379> lpush list1 A
(integer) 1
127.0.0.1:6379> lpush list1 B
(integer) 2
127.0.0.1:6379> lpush list1 C
(integer) 3
127.0.0.1:6379> lrange list1 0 -1
1) "C"
2) "B"
3) "A"
127.0.0.1:6379> lrange list1 0 -1
1) "A"
2) "B"
3) "C"
127.0.0.1:6379> linsert list1 before C XXX # 在 C 前插入 XXX
(integer) 4
127.0.0.1:6379> lrange list1 0 -1
1) "A"
2) "B"
3) "XXX"
4) "C"
127.0.0.1:6379> lrange list1 0 -1 # 获取所有值
1) "C"
2) "B"
3) "A"
127.0.0.1:6379> lrange list1 0 1 # 获取指定区间的值
1) "C"
2) "B"
127.0.0.1:6379> lrange list1 0 -1
1) "C"
2) "B
127.0.0.1:6379> lindex list1 0
"C"
127.0.0.1:6379> lindex list1 1
"B"
127.0.0.1:6379> llen list1
(integer) 1
127.0.0.1:6379> lrange list1 0 -1
1) "D"
2) "C"
3) "B"
4) "A"
127.0.0.1:6379> lpop list1 # 删除列表最左边的元素,且返回元素
"D"
127.0.0.1:6379> rpop list1 # 删除列表最右边的元素,且返回元素
"A"
127.0.0.1:6379> lrange list1 0 -1
1) "C"
2) "B"
127.0.0.1:6379> lrange list1 0 -1
1) "C"
2) "C"
3) "B"
4) "A"
127.0.0.1:6379> lrem list1 1 A # 删除1个A
(integer) 1
127.0.0.1:6379> lrange list1 0 -1
1) "C"
2) "C"
3) "B"
127.0.0.1:6379> lrem list1 2 C # 删除2个C
(integer) 2
127.0.0.1:6379> lrange list1 0 -1
1) "B"
127.0.0.1:6379>
127.0.0.1:6379> lrange list1 0 -1
1) "A"
2) "B"
3) "C"
127.0.0.1:6379> rpoplpush list1 list2 # 移除 list1 中最后一个元素,且添加到list2 中去
"C"
127.0.0.1:6379> lrange list1 0 -1
1) "A"
2) "B"
127.0.0.1:6379> lrange list2 0 -1
1) "C"
127.0.0.1:6379> rpush list1 A
(integer) 1
127.0.0.1:6379> rpush list1 B
(integer) 2
127.0.0.1:6379> rpush list1 C
(integer) 3
127.0.0.1:6379> rpush list1 D
(integer) 4
127.0.0.1:6379> ltrim list1 1 2 # 截取下标为1到2的值
OK
127.0.0.1:6379> lrange list1 0 -1
1) "B"
2) "C"
语法:lset list 下标 value
127.0.0.1:6379> exists list1 # 判断是否存在此list
(integer) 0
127.0.0.1:6379> lset list1 0 beijing # 不存在,替换报错
(error) ERR no such key
127.0.0.1:6379> lpush list1 guangdong # 创建一个list
(integer) 1
127.0.0.1:6379> lindex list1 0
"guangdong"
127.0.0.1:6379> lset list1 0 beijing # 存在,替换成功
OK
127.0.0.1:6379> lindex list1 0
"beijing"
set:一种无序(不保证有序)集合,且元素不能重复
127.0.0.1:6379> sadd set1 A
(integer) 1
127.0.0.1:6379> sadd set1 B
(integer) 1
127.0.0.1:6379> sadd set1 C
(integer) 1
127.0.0.1:6379> sadd set1 C # set的值不能重复
(integer) 0
127.0.0.1:6379> smembers set1 # 查询指定set的所有值,乱序
1) "B"
2) "A"
3) "C"
127.0.0.1:6379> smesmbers set1 # 查询指定set的所有值,乱序
1) "B"
2) "A"
3) "C"
127.0.0.1:6379> scard set1
(integer) 3
127.0.0.1:6379> smembers set1
1) "D"
2) "B"
3) "A"
4) "C"
127.0.0.1:6379> srandmember set1 # 获取一个随机元素
"D"
127.0.0.1:6379> srandmember set1 # 获取一个随机元素
"B"
127.0.0.1:6379> srandmember set1 2 # 获取两个随机元素
1) "A"
2) "D"
127.0.0.1:6379> srem set1 C # 删除 C 这个元素
(integer) 1
127.0.0.1:6379> smembers set1
1) "B"
2) "A"
127.0.0.1:6379> smembers set1
1) "D"
2) "B"
3) "A"
4) "C"
127.0.0.1:6379> spop set1 # 随机删除一个元素
"A"
127.0.0.1:6379> spop set1 # 随机删除一个元素
"B"
127.0.0.1:6379> smembers set1
1) "D"
2) "C"
127.0.0.1:6379> smembers set1
1) "D"
2) "C"
127.0.0.1:6379> smove set1 set2 D # 从 set1 移动 D 到 set2
(integer) 1
127.0.0.1:6379> smembers set1
1) "C"
127.0.0.1:6379> smembers set2
1) "D"
127.0.0.1:6379> sadd set1 A
(integer) 1
127.0.0.1:6379> sadd set1 B
(integer) 1
127.0.0.1:6379> sadd set1 C
(integer) 1
127.0.0.1:6379> sadd set2 B
(integer) 1
127.0.0.1:6379> sadd set2 C
(integer) 1
127.0.0.1:6379> sadd set2 D
(integer) 1
127.0.0.1:6379> sadd set2 E
(integer) 1
127.0.0.1:6379> sinter set1 set2 # 交集
1) "B"
2) "C"
127.0.0.1:6379> sunion set1 set2 # 并集
1) "D"
2) "E"
3) "C"
4) "B"
5) "A"
127.0.0.1:6379> sdiff set1 set2 # 差集
1) "A"
此类型和 set 一样也是 string 类型元素的集合,且不允许重复的元素
不同的是每个元素都会关联一个double类型的分数,redis正是通过分数来为集合中的成员进行从小到大的排序
有序集合的成员是唯一,但分数(score)却可以重复
127.0.0.1:6379> zadd sortedset1 20 zhangsan # 添加一个
(integer) 1
127.0.0.1:6379> zadd sortedset1 10 lisi 60 wangwu # 添加多个
(integer) 2
127.0.0.1:6379> zrange sortedset1 0 -1
1) "lisi"
2) "zhangsan"
3) "wangwu"
127.0.0.1:6379> zrangebyscore sortedset1 -inf +inf # 从小到大
1) "lisi"
2) "zhangsan"
3) "wangwu"
127.0.0.1:6379> zrevrange sortedset1 0 -1 # 从大到小
1) "wangwu"
2) "zhangsan"
3) "lisi"
127.0.0.1:6379> zrangebyscore sortedset1 -inf +inf withscores # 显示从小到大且附带值
1) "lisi"
2) "10"
3) "zhangsan"
4) "20"
5) "wangwu"
6) "60"
127.0.0.1:6379> zrangebyscore sortedset1 -inf 20 withscores # 显示从小到大,且数值小于20的
1) "lisi"
2) "10"
3) "zhangsan"
4) "20"
127.0.0.1:6379>
127.0.0.1:6379> zcard sortedset1
(integer) 2
127.0.0.1:6379> zcount sortedset1 10 60
(integer) 3
127.0.0.1:6379> zrange sortedset1 0 -1
1) "lisi"
2) "zhangsan"
3) "wangwu"
127.0.0.1:6379> zrem sortedset1 wangwu # 删除 wangwu 这个元素
(integer) 1
127.0.0.1:6379> zrange sortedset1 0 -1
1) "lisi"
2) "zhangsan"
127.0.0.1:6379> hset hash1 username admin
(integer) 1
127.0.0.1:6379> hset hash1 password admin
(integer) 1
127.0.0.1:6379> hsetnx hash1 username admin888 # 已存在,失败
(integer) 0
127.0.0.1:6379> hsetnx hash1 code 666 # 不存在,成功
(integer) 1
127.0.0.1:6379> hget hash1 password
"admin"
127.0.0.1:6379> hgetall hash1
1) "username"
2) "admin"
3) "password"
4) "admin"
127.0.0.1:6379> hlen hash1
(integer) 2
127.0.0.1:6379> hkeys hash1 # 获取所有 field 字段
1) "username"
2) "password"
127.0.0.1:6379> hvals hash1 # 获取所有 value 值
1) "admin"
2) "admin"
127.0.0.1:6379> hdel hash1 username
(integer) 1
127.0.0.1:6379> hsetnx hash1 code 666
(integer) 1
127.0.0.1:6379> hincrby hash1 code 2
(integer) 668
127.0.0.1:6379> hincrby hash1 code -68
(integer) 600
使用经纬度,作为地理坐标,然后存储到一个有序集合 zset/sortedset 中去保存,所以 zset 中的命令也是可以使用的
命令列表:
127.0.0.1:6379> geoadd china:city 116.413384 39.910925 beijing
(integer) 1
127.0.0.1:6379> geoadd china:city 113.271431 23.135336 guangzhou
(integer) 1
127.0.0.1:6379> geoadd china:city 113.582555 22.276565 zhuhai
(integer) 1
127.0.0.1:6379> geoadd china:city 112.556391 37.876989 taiyuan
(integer) 1
127.0.0.1:6379> geopos china:city beijing zhuhai
1) 1) "116.41338318586349487"
2) "39.9109247398676743"
2) 1) "116.41338318586349487"
2) "39.9109247398676743"
127.0.0.1:6379> geodist china:city guangzhou taiyuan
"1641074.3783"
127.0.0.1:6379> geodist china:city guangzhou taiyuan km
"1641.0744"
georadius
就可以完成 附近的人功能(例如这个位置我们输入的是人当前的位置)127.0.0.1:6379> georadius china:city 113.582555 22.276565 500 km
1) "zhuhai"
2) "guangzhou"
127.0.0.1:6379> georadius china:city 113.582555 22.276565 500 km withdist withcoord count 1
1) 1) "zhuhai"
2) "0.0002"
3) 1) "113.58255296945571899"
2) "22.27656546780746538"
127.0.0.1:6379> georadius china:city 113.582555 22.276565 500 km withdist withcoord count 2
1) 1) "zhuhai"
2) "0.0002"
3) 1) "113.58255296945571899"
2) "22.27656546780746538"
2) 1) "guangzhou"
2) "100.7111"
3) 1) "113.27143281698226929"
2) "23.13533660075498233"
127.0.0.1:6379> georadiusbymember china:city zhuhai 500 km
1) "zhuhai"
2) "guangzhou"
127.0.0.1:6379> geohash china:city zhuhai
1) "weby8xk63k0"
HyperLogLog 是用来做基数(数据集中不重复的元素的个数)统计的算法,其底层使用string数据类型
HyperLogLog 的优点是:
因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素
一个常见的例子:
含义:添加指定元素到 HyperLogLog 中
语法:PFADD key element1 [elememt2..]
127.0.0.1:6379> pfadd test1 A B C D E F G
(integer) 1
127.0.0.1:6379> pfadd test2 C C C D E F G
(integer) 1
含义:返回给定 HyperLogLog 的基数估算值
语法:PFCOUNT key [key]
127.0.0.1:6379> pfcount test1
(integer) 7
127.0.0.1:6379> pfcount test2
(integer) 5
含义:将多个 HyperLogLog 合并为一个 HyperLogLog
语法:PFMERGE destkey sourcekey [sourcekey..]
127.0.0.1:6379> pfmerge test test1 test2
OK
127.0.0.1:6379> pfcount test
(integer) 9
BitMaps 使用位存储,信息状态只有 0 和 1
127.0.0.1:6379> setbit sign 0 1
(integer) 0
127.0.0.1:6379> setbit sign 1 0
(integer) 0
127.0.0.1:6379> setbit sign 2 0
(integer) 0
127.0.0.1:6379> setbit sign 3 0
(integer) 0
127.0.0.1:6379> setbit sign 4 1
(integer) 0
127.0.0.1:6379> setbit sign 5 1
(integer) 0
127.0.0.1:6379> setbit sign 6 1
(integer) 0
127.0.0.1:6379> getbit sign 4
(integer) 1
127.0.0.1:6379> getbit sign 2
(integer) 0
127.0.0.1:6379> bitcount sign
(integer) 4
定义:Redis 事务的本质是一组命令的集合
即:redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令
首先
可能受到关系型数据库的影响,大家会将事务一概而论的认为数据库中的事务都是原子性的,但其实,Redis 中的事务是非原子性的。
原子性:所有的操作要么全做完,要么就全不做,事务必须是一个最小的整体,即像化学组中的原子,是构成物质的最小单位。
非原子性:与原子性反之
多数事务失败是由语法错误或者数据结构类型错误导致的,而语法的错误是在命令入队前就进行检测,而类型错误是在执行时检测的,Redis为提升性能而采用这种简单的事务
批量操作在发送 EXEC 命令前被放入队列缓存,并不会被实际执行,也就不存在事务内的查询要看到事务里的更新,事务外查询不能看到
含义:开启事务,下一步就会将内容逐个放入队列中去,然后通过 exec 命令,原子化的执行
命令:multi
由于 multi 和 exec 需要搭配使用,所以在第二点一起演示示例
含义:执行事务中的所有操作
命令:exec
首先先存一个 k1 和 k2,开启事务后,对这两个值进行修改,然后将这个事务执行,最后发现两个 OK ,然后用 get 查看一下值的变化
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> set k2 v2
OK
127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379> set k1 v11 # 修改 k1
QUEUED
127.0.0.1:6379> set k2 v22 # 修改 k2
QUEUED
127.0.0.1:6379> exec # 执行事务
1) OK
2) OK
127.0.0.1:6379> get k1
"v11"
127.0.0.1:6379> get k2
"v22"
语法错误(编译器错误)导致的事务失败,会使得保持原值,例如下文事务中,修改 k1 没问题,但是修改 k2 的时候出现了语法错误,set 写成了 sett ,执行 exec 也会出错,最后发现 k1 和 k2 的值都没有变
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> set k2 v2
OK
127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379> set k1 v11 # 修改正常
QUEUED
127.0.0.1:6379> sett k2 v22 # 修改有语法错误
(error) ERR unknown command `sett`, with args beginning with: `k2`, `v22`,
127.0.0.1:6379> exec # 执行事务
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k1
"v1"
127.0.0.1:6379> get k2
"v2"
类型错误(运行时错误)导致的事务异常,例如下面 k2 被当做了一个 list 处理,这样在运行时就会报错,最后事务提交失败,但是并不会回滚,结果就是 k1 修改成功,而 k2 失败
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> set k2 v2
OK
127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379> set k1 v11
QUEUED
127.0.0.1:6379> lpush k2 v22 # 类型错误
QUEUED
127.0.0.1:6379> exec # 执行事务
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
127.0.0.1:6379> get k1
"v11"
127.0.0.1:6379> get k2
"v2"
这个没什么好说的,就是取消执行
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 k11
QUEUED
127.0.0.1:6379> set k2 k22
QUEUED
127.0.0.1:6379> discard
OK
Redis的命令是原子性的,而事务是非原子性的,通过 watch 这个命令可以实现 Redis 具有回滚的一个效果
做法就是,在 multi 之前先用 watch 监控某些键值对,然后继续开启以及执行事务
我们监控 k1,然后再事务开启之前修改了 k1,又想在事务中修改 k1 ,可以看到最后结果中,事务中的操作就都没有执行了
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> set k2 v2
OK
127.0.0.1:6379> watch k1 # 监控 k1
OK
127.0.0.1:6379> set k1 v111111 # k1 被修改了
OK
127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379> set k1 v11
QUEUED
127.0.0.1:6379> set k2 v22
QUEUED
127.0.0.1:6379> exec # 执行事务
(nil)
127.0.0.1:6379> get k1
"v111111"
127.0.0.1:6379> get k2
"v2"
取消后,就可以正常执行了
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> set k2 v2
OK
127.0.0.1:6379> watch k1 # 监控
OK
127.0.0.1:6379> set k1 v111111
OK
127.0.0.1:6379> unwatch # 取消监控
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v11
QUEUED
127.0.0.1:6379> set k2 v22
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK
127.0.0.1:6379> get k1
"v11"
127.0.0.1:6379> get k2
"v22"
Jedis is a blazingly small and sane Redis java client. Jedis was conceived to be EASY to use.
Jedis 是一款可以让我们在java中操作redis数据库的工具,下载其jar包,或者引入到 maven 中即可,使用还是非常简单的
我这里创建了一个空项目,然后创建一个普通的 maven 模块用来演示 jedis
首先引入 jedis 依赖,后面需要所以还引入了fastjson
版本自己去 maven 中去查就可以了,因为我们 linux 中安装的 redis 是一个新的版本,所以我们依赖也用了最新的
<dependencies>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.75</version>
</dependency>
</dependencies>
创建测试类
public class Demo01 {
public static void main(String[] args) {
// 远程 linux(虚拟机)
Jedis jedis = new Jedis("192.168.122.1", 6379);
// 测一下是否连通
System.out.println(jedis.ping());
}
}
如果直接输入ip和端口,会连接不上,所以需要先做以下操作
① 保证 6379 端口开放
以 centos 7.9 为例,其他版本例如 6.x 可以查一下具体命令,以及防火墙是不是给拦截了,只要保证端口能访问到就可以了
firewall-cmd --zone=public --add-port=6379/tcp --permanent
# 显示
succss
firewall-cmd --reload
# 显示
success
firewall-cmd --query-port=6379/tcp
# 显示
yes
[root@centos7 bin]# redis-cli shutdown
[root@centos7 bin]# redis-server myconfig/redis.conf
你还可以在 window 的机器上,使用 telnet 192.168.122.1 6379 的方式测试一下是否能访问到,如果出现错误,请检查端口和防火墙的问题
② 修改 redis 配置文件
再次在 IDEA 中访问,就可以访问到了
// 存储
jedis.set("address","beijing");
// 获取
String address = jedis.get("address");
// 关闭连接
jedis.close();
补充:setex() 方法可以存储数据,并且指定过期时间
// 将aaa-bbb存入,且10秒后过期
jedis.setex("aaa",10,"bbb")
// 存储
jedis.lpush("listDemo","zhangsan","lisi","wangwu");//从左
jedis.rpush("listDemo","zhangsan","lisi","wangwu");//从右
// 获取
List<String> mylist = jedis.lrange("listDemo", 0, -1);
// 删除,并且返回元素
String e1 = jedis.lpop("listDemo");//从左
String e2 = jedis.rpop("listDemo");//从右
// 关闭连接
jedis.close();
// 存储
jedis.sadd("setDemo","zhangsan","lisi","wangwu");
// 获取
Set<String> setDemo = jedis.smembers("setDemo");
// 关闭连接
jedis.close();
// 存储
jedis.zadd("sortedsetDemo",20,"zhangsan");
jedis.zadd("sortedsetDemo",10,"lisi");
jedis.zadd("sortedsetDemo",60,"wangwu");
// 获取
Set<String> sortedsetDemo = jedis.zrange("sortedsetDemo", 0, -1);
// 关闭连接
jedis.close();
// 存储
jedis.hset("hashDemo","name","lisi");
jedis.hset("hashDemo","age","20");
// 获取
String name = jedis.hget("hashDemo", "name");
// 获取所有数据
Map<String, String> user = jedis.hgetAll("hashDemo");
Set<String> keySet = user.keySet();
for (String key : keySet) {
//获取value
String value = user.get(key);
System.out.println(key + ":" + value);
}
// 关闭连接
jedis.close();
public class Demo01 {
public static void main(String[] args) {
// 远程 linux(虚拟机)
Jedis jedis = new Jedis("192.168.122.1", 6379);
JSONObject jsonObject = new JSONObject();
jsonObject.put("name", "zhangsan");
jsonObject.put("age", "21");
// 开启事务
Transaction multi = jedis.multi();
String result = jsonObject.toJSONString();
try {
multi.set("userA", result);
multi.set("userB", result);
// 执行事务
multi.exec();
} catch (Exception e) {
// 放弃事务
multi.discard();
} finally {
System.out.println(jedis.get("userA"));
System.out.println(jedis.get("userB"));
// 关闭连接
jedis.close();
}
}
}
为了将结果显示出来,在关闭前添加两句输出语句
执行结果:
{"name":"zhangsan","age":"21"} {"name":"zhangsan","age":"21"}
为什么我们要使用连接池呢?
我们要使用Jedis,必须建立连接,我们每一次进行数据交互的时候,都需要建立连接,Jedis虽然具有较高的性能,但建立连接却需要花费较多的时间,如果使用连接池则可以同时在客户端建立多个连接并且不释放,连接的时候只需要通过一定的方式获取已经建立的连接,用完则归还到连接池,这样时间就大大的节省了
下面就是我们直接创建了一个连接池,不过我们使用时一般都会封装一个工具类
@Test
public void testJedisPool(){
// 0.创建一个配置对象
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(50);
config.setMaxIdle(10);
// 1.创建Jedis连接池对象
JedisPool jedisPool = new JedisPool(config,"192.168.122.1",6379);
// 2.获取连接
Jedis jedis = jedisPool.getResource();
// 3. 存储
jedis.set("name","zhangsan");
// 4. 输出结果
System.out.println(jedis.get("name"));
//5. 关闭 归还到连接池中
jedis.close();;
}
直接使用工具类就可以了
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
/**
* JedisPool工具类
* 加载配置文件,配置连接池的参数
* 提供获取连接的方法
*/
public class JedisPoolUtils {
private static JedisPool jedisPool;
static {
//读取配置文件
InputStream is = JedisPoolUtils.class.getClassLoader().getResourceAsStream("jedis.properties");
//创建Properties对象
Properties pro = new Properties();
//关联文件
try {
pro.load(is);
} catch (IOException e) {
e.printStackTrace();
}
//获取数据,设置到JedisPoolConfig中
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(Integer.parseInt(pro.getProperty("maxTotal")));
config.setMaxIdle(Integer.parseInt(pro.getProperty("maxIdle")));
//初始化JedisPool
jedisPool = new JedisPool(config, pro.getProperty("host"), Integer.parseInt(pro.getProperty("port")));
}
/**
* 获取连接方法
*/
public static Jedis getJedis() {
return jedisPool.getResource();
}
}
别忘了配置文件
host=192.168.122.1
port=6379
maxTotal=50
maxIdle=100
调用代码
@Test
public void testJedisPoolUtil(){
// 0. 通过连接池工具类获取
Jedis jedis = JedisPoolUtils.getJedis();
// 1. 使用
jedis.set("name","lisi");
System.out.println(jedis.get("name"));
// 2. 关闭 归还到连接池中
jedis.close();;
}
① 创建 Springboot项目或模块,引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
可以看到,这个官方的 starter 引入了 Redis,但是并没有引入 Jedis,而是引入了 Lettuce
Jedis:采用的直连,多个线程操作的话,是不安全的。如果要避免不安全,使用jedis pool连接池!更像BIO模式 Lettuce:采用netty,实例可以在多个线程中共享,不存在线程不安全的情况!可以减少线程数据了,更像NIO模式
② 编写配置文件
# 配置redis
spring.redis.host=192.168.122.1
spring.redis.port=6379
③ 测试代码
@SpringBootTest
class Redis02BootApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
@Test
void contextLoads() {
redisTemplate.opsForValue().set("name","zhangsan");
System.out.println(redisTemplate.opsForValue().get("name"));
}
}
运行结果:
zhangsan
上述操作,在 IDEA 中的结果肯定是没问题的,但是我们去 Linux 中去看一下 Redis 的内容,却发现 key 都是乱码,例如存储的 name 却变成了如下内容
127.0.0.1:6379> keys *
1) "\xac\xed\x00\x05t\x00\x04name"
这就是序列化的问题,下面我们会分析这个问题,这里先给出解决方案,即自定义 RedisTemplate 模板
① 自定义 RedisConfig 类
@Configuration
public class RedisConfig {
/**
* 自定义 RedisTemplate 怒ban
*
* @param factory
* @return
*/
@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
// 为开发方便,一般直接使用 <String, Object>
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
template.setConnectionFactory(factory);
// Json序列化配置
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);
// String 的序列化
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
② 调用
@SpringBootTest
class Redis02BootApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
@Test
void contextLoads() {
redisTemplate.opsForValue().set("address","beijing");
System.out.println(redisTemplate.opsForValue().get("address"));
}
}
我们分别存储了 name2 和 address 这两个key,去终端中查看一下
127.0.0.1:6379> keys *
1) "address"
2) "\xac\xed\x00\x05t\x00\x04name"
3) "name2"
可以看到,问题被解决了
我们操作追求的是便捷,而每次都是用一大堆 redisTemplate.opsForValue().xxxxx 很长的命令,所以封装一个工具类能更加事半功倍,具体工具类我就不在这里贴了,因为太长了,后面我会传到 github上去然后更新链接,当然了百度上其实一搜一大把
例如下面,我们使用工具类后就可以很简单的使用封装 redisTemplate 后的方法了
@Autowired
private RedisUtil redisUtil;
@Test
void contextLoads() {
redisUtil.set("address2", "zhuhai");
System.out.println(redisUtil.get("address2"));
}
这一块的简单分析,主要想要弄清楚四个内容
在以前 Springboot 的文章中,关于自动配置的原理中可知,整合一个内容的时候,都会有一个自动配置类,然后再 spring.factories 中能找到它的完全限定类名
进入 spring.factories,查找关于 redis 的自动装配类
进入 RedisAutoConfiguration 类后,在注解中就能看到 RedisProperties 类的存在,这很显然是关于配置文件的类
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
// 这个注解!!!
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {
// ...... 省略
}
进入 RedisProperties 类
alt + 7 可以在 IDEA 中查看到这个类中的属性方法等,这里查看一下其属性
例如地址端口,超时时间等等配置都清楚了,例如我们当时的配置文件是这样配的
# 配置redis
spring.redis.host=192.168.122.1
spring.redis.port=6379
继续回到类,查看下面一个注解,发现有两个类
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
// 这个注解!!!
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {
// ...... 省略
}
先来进入 Jedis 这个看看,GenericObjectPool 和 Jedis 这两个内容都是默认缺失的,所以是不会生效的
再来看看 LettuceConnectionConfiguration,是没有问题的,所以确实现在默认的实现都是使用 Lettuce 了
继续回到 RedisAutoConfiguration
其 Bean 只有两个
这种 XxxTemplate ,例如 JdbcTemplate、RestTemplate 等等都是通过 Template 来操作这些组件,所以这里的两个 Template 也是这也昂,分别用来操作 Redis 和 Redis 的 String 数据类型(因为 String 类型很常用)
// 注解略
public class RedisAutoConfiguration {
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
特别注意这一个注解
@ConditionalOnMissingBean(name = "redisTemplate")
它的意思就是说如果没有自定义,就默认使用这个,这也就是告诉我们,我们可以自己自定义Template,来覆盖掉默认的,前面的使用中我们知道,使用默认的 Template 会涉及到乱码,也就是序列化问题
因为在网络中传输的对象需要序列化,否则就是乱码
我们进入默认的 RedisTemplate 看看
首先看到的就是一些关于序列化的参数
往下看,可以看到默认的序列化方式使用的是 Jdk 序列化,而我们自定义中使用的是 Json 序列化
而默认的RedisTemplate中的所有序列化器都是使用这个序列化器
RedisSerializer 中为我们提供了多种序列化方式
所以后来我们就自定了 RedisTemplate 模板,重新定义了各种类型的序列化方式,这也是我们推荐的做法
邮箱:ideal_bwh@163.com
如果帮到你的话,那就来关注我吧!
如果您更喜欢微信文章的阅读方式,可以关注我的公众号
如果您更加喜欢PC端的阅读方式,可以访问我的个人博客
域名:www.ideal-20.cn
在这里的我们素不相识,却都在为了自己的梦而努力 ❤ 一个坚持推送原创开发技术文章的公众号:理想二旬不止