Redis


一、Redis简介

1.认识NoSQL

1.1 什么是NoSQL


  • NoSQL最常见的解释是”non-relational“,泛指非关系型的数据库,很多人也说它是”Not Only SQL

  • NoSQL 不依赖业务逻辑方式存储,而以简单的key-value模式存储。因此大大的增加了数据库的扩展能力。

  • 区别于关系数据库,它们不保证关系数据的ACID特性

  • 常见的NoSQL数据库有:RedisMemCacheMongoDB

1.2 NoSQL适用场景

  • 对数据高并发的读写

  • 海量数据的读写

  • 对数据高可扩展性的

1.3 NoSQL不适用场景

  • 需要事务支持

  • 基于sql的结构化查询存储,处理复杂的关系,需要即席查询。

1.4 NoSQL与SQL的差异


SQL NoSQL
数据结构 结构化 非结构化
数据关联 关联的 无关联的
查询方式 SQL查询 非SQL
事务特性 ACID BASE
存储方式 磁盘 内存
扩展性 垂直 水平
使用场景 1)数据结构固定
2)相关业务对数据安全性、一致性要求较高
1)数据结构不固定
2)对一致性、安全性要求不高
3)对性能要求

2.认识Redis

Redis诞生于2009年全称是Remote Dictionary Server,远程词典服务器,是一个基于内存的键值型NoSQL数据库。

Redis的特征:

  • 键值(key-value)型,value支持多种不同数据结构,功能丰富
  • 单线程,每个命令具备原子性
  • 低延迟,速度快(基于内存、IO多路复用、良好的编码)
  • 支持数据持久化
  • 支持主从集群、分片集群
  • 支持多语言客户端

二、Redis安装

1.Mac安装Redis

1.1 安装redis

brew install redis

1.2 启动Redis

# 初次启动
brew services start redis
==> Successfully started `redis` (label: homebrew.mxcl.redis)

# 连接redis
redis-cli

# 关闭连接,并退出
127.0.0.1:6379> shutdown
not connected> quit

1.3 其它操作

远程连接Redis

redis-cli -h xxx.xx.xx.xx -p 6379 -a 123456
  • -h xxx.xx.xx.xx:指定要连接的redis节点的IP地址,默认是127.0.0.1
  • -p 6379:指定要连接的redis节点的端口,默认是6379
  • -a 123456:指定redis的访问密码

指定密码

127.0.0.1:6379> auth 123456 
Ok

心跳测试

127.0.0.1:6379> ping 
PONG

2.Linux安装Redis


本次安装Redis是基于Linux系统下安装的,因此需要一台Linux服务器或者虚拟机。如果您使用的是自己购买的服务器,请提前开放6379端口,避免后续出现的莫名其妙的错误!

2.1 安装依赖


Redis是基于C语言编写的,因此首先需要安装Redis所需要的gcc依赖

yum install -y gcc tcl

安装成功如下图所示:

2.2 安装Redis


redis-6.2.6.tar上传至/usr/local/src目录

在xShell中cd/usr/local/src目录执行以下命令进行解压操作

tar -xzf redis-6.2.6.tar.gz

解压成功后依次执行以下命令

cd redis-6.2.6
make
make install

安装成功后打开/usr/local/bin目录(该目录为Redis默认的安装目录)

2.3 启动Redis

Redis的启动方式有很多种,例如:前台启动后台启动开机自启

前台启动(不推荐)


这种启动属于前台启动,会阻塞整个会话窗口,窗口关闭或者按下CTRL + C则Redis停止。不推荐使用。

安装完成后,在任意目录输入redis-server命令即可启动Redis

redis-server

启动成功如下图所示

后台启动(不推荐)


如果要让Redis以后台方式启动,则必须修改Redis配置文件,配置文件所在目录就是之前我们解压的安装包下

因为我们要修改配置文件,因此我们需要先将原文件备份一份

cd /usr/local/src/redis-6.2.6
cp redis.conf redis.conf.bck

然后修改redis.conf文件中的一些配置

# 允许访问的地址,默认是127.0.0.1,会导致只能在本地访问。修改为0.0.0.0则可以在任意IP访问,生产环境不要设置为0.0.0.0
bind 0.0.0.0
# 守护进程,修改为yes后即可后台运行
daemonize yes 
# 密码,设置后访问Redis必须输入密码
requirepass 1325

Redis其他常用配置

# 监听的端口
port 6379
# 工作目录,默认是当前目录,也就是运行redis-server时的命令,日志、持久化等文件会保存在这个目录
dir .
# 数据库数量,设置为1,代表只使用1个库,默认有16个库,编号0~15
databases 1
# 设置redis能够使用的最大内存
maxmemory 512mb
# 日志文件,默认为空,不记录日志,可以指定日志文件名
logfile "redis.log"

启动Redis

# 进入redis安装目录 
cd /usr/local/src/redis-6.2.6
# 启动
redis-server redis.conf

停止Redis服务

# 通过kill命令直接杀死进程
kill -9 redis进程id
# 利用redis-cli来执行 shutdown 命令,即可停止 Redis 服务,
# 因为之前配置了密码,因此需要通过 -a 来指定密码
redis-cli -a 132537 shutdown

开机自启(推荐)


我们也可以通过配置来实现开机自启

首先,新建一个系统服务文件

vi /etc/systemd/system/redis.service

将以下命令粘贴进去

[Unit]
Description=redis-server
After=network.target

[Service]
Type=forking
ExecStart=/usr/local/bin/redis-server /usr/local/src/redis-6.2.6/redis.conf
PrivateTmp=true

[Install]
WantedBy=multi-user.target

然后重载系统服务

systemctl daemon-reload

现在,我们可以用下面这组命令来操作redis了

# 启动
systemctl start redis
# 停止
systemctl stop redis
# 重启
systemctl restart redis
# 查看状态
systemctl status redis

执行下面的命令,可以让redis开机自启

systemctl enable redis

三、Redis命令

通过Redis的中文文档学习:http://www.redis.cn/commands.html

通过菜鸟教程官网来学习:https://www.runoob.com/redis/redis-keys.html

Redis是一个key-value的数据库,key一般是String类型,不过value的类型多种多样

1.通用命令

指令 描述
keys 查看符合模板的所有key,不建议在生产环境设备上使用
del 删除一个指定的key
exists 判断key是否存在
expire 给一个key设置有效期,有效期到期时该key会被自动删除
ttl 查看一个KEY的剩余有效期,-1表示永不过期,-2表示已过期
type 查看你的key是什么类型
unlink 根据value选择非阻塞删除
select 切换数据库
dbsize 查看当前数据库的key的数量
flushdb 清空当前库
flushall 通杀全部库

可以通过help [command] 可以查看一个命令的具体用法!

2.基本类型

2.1 字符串-String

String类型是Redis最基本的数据类型,一个Redis中字符串value最多可以是512M

String的常见命令

命令 描述
set 添加或者修改已经存在的一个String类型的键值对
get 根据key获取String类型的value
mset 批量添加多个String类型的键值对
mget 根据多个key获取多个String类型的value
Strlen 获得value的长度
incr/decr 让一个整型的key自增/自减1
incrby/decrby 让一个整型的key自增/自减,并指定步长,例如:incrby num 2 让num值自增2
incrbyfloat 让一个浮点类型的数字自增并指定步长
setnx 添加一个String类型的键值对,前提是这个key不存在,才执行
setex 添加一个String类型的键值对,并且指定有效期
getrange 获得value的范围,例如:getrange name 0 4
setrange 从指定位置覆盖key存储的value,例如:setrange name 0 jian
getset 以新换旧,设置了新值同时获得旧值。例如:getset name xiaojian

数据结构:String的数据结构为简单动态字符串(Simple Dynamic String,缩写SDS)。是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配.

2.2 哈希-Hash

hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。类似Java里面的HashMap

每个 hash 可以存储 232 - 1 键值对(40多亿)。

Hash的常见命令

命令 描述
hset key field value 添加或者修改hash类型key的field的值
hget key field 获取一个hash类型key的field的值
hmset hmset 和 hset 效果相同 ,4.0之后hmset可以弃用了
hmget 批量获取多个hash类型key的field的值
hgetall 获取一个hash类型的key中的所有的field和value
hkeys 获取一个hash类型的key中的所有的field
hvals 获取一个hash类型的key中的所有的value
hincrby 让一个hash类型key的字段值自增并指定步长
hsetnx 添加一个hash类型的key的field值,前提是这个field不存在,否则不执行
127.0.0.1:6379> hset userkey name "jack" age 18 birth "2004-07"
(integer) 3
127.0.0.1:6379> hgetall userkey
1) "name"
2) "jack"
3) "age"
4) "18"
5) "birth"
6) "2004-07"

数据结构

Hash类型对应的数据结构是两种:ziplist(压缩列表),hashtable(哈希表)。当field-value长度较短且个数较少时,使用ziplist,否则使用hashtable。

2.3 列表-List

Redis中的List类型与Java中的LinkedList类似,可以看做是一个双向链表结构。既可以支持正向检索和也可以支持反向检索。

一个列表最多可以包含 232 - 1 个元素 (4294967295, 每个列表超过40亿个元素)。

特征也与LinkedList类似:

  • 有序
  • 元素可以重复
  • 插入和删除快
  • 查询速度一般

常用来存储一个有序数据,例如:朋友圈点赞列表,评论列表等。

List的常见命令

命令 描述
lpush key element … 向列表左侧插入一个或多个元素
lpop key 移除并返回列表左侧的第一个元素
rpush key element … 向列表右侧插入一个或多个元素
rpop key 移除并返回列表右侧的第一个元素
lrange key star end 返回一段角标范围内的所有元素
blpop和brpop 与LPOP和RPOP类似,只不过在没有元素时等待指定时间,而不是直接返回nil
rpoplpush <key1><key2> 从<key1>列表右边吐出一个值,插到<key2>列表左边。
lindex <key><index> 按照索引下标获得元素(从左到右)
llen <key> 获得列表长度
lrem <key><n><value> 从左边删除n个value
lset <key><index><value> 将列表key下标为index的值替换成value

数据结构

List的数据结构为快速链表quickList和压缩链表ziplist。

当列表元素较少的情况下会使用一块连续的内存存储,这个结构是ziplist。它将所有的元素紧挨着一起存储,分配的是一块连续的内存。

当数据量比较多的时候才会改成quicklist。因为普通的链表需要的附加指针空间太大,会比较浪费空间。比如这个列表里存的只是int类型的数据,结构上还需要两个额外的指针prev和next。

将链表和ziplist结合起来组成了quicklist。也就是将多个ziplist使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。

思考问题

  • 如何利用List结构模拟一个栈?

    • 先进后出,入口和出口在同一边
  • 如何利用List结构模拟一个队列?

    • 先进先出,入口和出口在不同边
  • 如何利用List结构模拟一个阻塞队列?

    • 入口和出口在不同边
    • 出队时采用BLPOP或BRPOP

2.4 集合-Set

Redis的Set是string类型的无序集合。它底层其实是一个value为null的hash表,与Java中的HashSet类似,因此具备与HashSet类似的特征

  • 无序
  • 元素不可重复
  • 查找快
  • 支持交集、并集、差集等功能

Set的常见命令有

命令 描述
sadd key member … 向set中添加一个或多个元素
srem key member … 移除set中的指定元素
scard key 返回set中元素的个数
sismember key member 判断一个元素是否存在于set中
smembers key 获取set中的所有元素
sinter key1 key2 … 求key1与key2的交集
sdiff key1 key2 … 求key1与key2的差集
sunion key1 key2 .. 求key1和key2的并集
spop key 随机从该集合中吐出一个值
srandmember <key><n> 随机从该集合中取出n个值。不会从集合中删除
smove <source><des>value 把集合中一个值从一个集合移动到另一个集合

交集、差集、并集图示

数据结构

Set数据结构是dict字典,字典是用哈希表实现的。Java中HashSet的内部实现使用的是HashMap,只不过所有的value都指向同一个对象。Redis的set结构也是一样,它的内部也使用hash结构,所有的value都指向同一个内部值。

2.5 有序集合-ZSet

Redis的ZSet(SortedSet)是一个可排序的set集合,与Java中的TreeSet有些类似,但底层数据结构却差别很大。SortedSet中的每一个元素都带有一个score属性,可以基于score属性对元素排序,底层的实现是一个跳表(SkipList)加 hash表。

SortedSet具备下列特性:

  • 可排序
  • 元素不重复
  • 查询速度快

因为SortedSet的可排序特性,经常被用来实现排行榜这样的功能。

SortedSet的常见命令

命令 描述
zadd key score member 添加一个或多个元素到sorted set ,如果已经存在则更新其score值
zrem key member 删除sorted set中的一个指定元素
zscore key member 获取sorted set中的指定元素的score值
zrankkey member 获取sorted set 中的指定元素的排名
zcard key 获取sorted set中的元素个数
zcount key min max 统计score值在给定范围内的所有元素的个数
zincrby key increment member 让sorted set中的指定元素自增,步长为指定的increment值
zrange key min max 按照score排序后,获取指定排名范围内的元素
zrangebyscore key min max 按照score排序后,获取指定score范围内的元素
zdiff、zinter、zunion 求差集、交集、并集

注意:所有的排名默认都是升序,如果要降序则在命令的Z后面添加REV即可

数据结构

SortedSet(zset)是Redis提供的一个非常特别的数据结构,一方面它等价于Java的数据结构Map<String, Double>,可以给每一个元素value赋予一个权重score,另一方面它又类似于TreeSet,内部的元素会按照权重score进行排序,可以得到每个元素的名次,还可以通过score的范围来获取元素的列表。

zset底层使用了两个数据结构

(1)hash,hash的作用就是关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score值。

(2)跳跃表,跳跃表的目的在于给元素value排序,根据score的范围获取元素列表。

3.特殊类型

3.1 Bitmaps

简介

Redis 提供了 Bitmaps 可以实现对位的操作,可以把 Bitmaps 想象成一个以位为单位的数组, 数组的每个单元只能存储 0 和 1, 数组的下标在 Bitmaps 中叫做偏移量。

Bitmaps的常见命令

  • setbit <key> <offset> <value> 设置Bitmaps中某个偏移量的值(0或1)

  • getbit <key> <offset> 获取Bitmaps中某个偏移量的值

  • bitcount <key> [start end] 统计字符串从start字节到end字节比特值为1的数量

  • bitop and(or/not/xor) <destkey> [key…] 可以做多个Bitmaps的and(交集) 、 or(并集) 、 not(非) 、 xor(异或) 操作并将结果保存在destkey中。

实例1

每个独立用户是否访问过网站,结果存放在Bitmaps中, 将访问的用户记做1, 没有访问的用户记做0, 用偏移量作为用户的id。 假设现在有20个用户,userid=1, 6, 11, 15, 19的用户对网站进行了访问, 那么当前Bitmaps初始化结果如图

users:20220717代表2022-07-17这天的独立访问用户的Bitmaps

127.0.0.1:6379> setbit users:20220717 1 1
(integer) 0
127.0.0.1:6379> setbit users:20220717 6 1
(integer) 0
127.0.0.1:6379> setbit users:20220717 11 1
(integer) 0
127.0.0.1:6379> setbit users:20220717 15 1
(integer) 0
127.0.0.1:6379> setbit users:20220717 19 1
(integer) 0

获取id=6,8的用户是否在2022-07-17这天访问过, 返回0说明没有访问过,返回1说明访问过

127.0.0.1:6379> getbit users:20220717 6
(integer) 1
127.0.0.1:6379> getbit users:20220717 8
(integer) 0

计算2022-07-17这天的独立访问用户数量

127.0.0.1:6379> bitcount users:20220717
(integer) 5

start和end代表起始和结束字节数, 下面计算用户id在第1个字节到第3个字节之间的独立访问用户数, 对应的用户id是11, 15, 19。

127.0.0.1:6379> bitcount users:20220717 1 3
(integer) 3

举例: K1 【01000001 01000000 00000000 00100001】,对应【0,1,2,3】

  • bitcount K1 1 2 : 统计下标1、2字节组中bit=1的个数,即 01000000 00000000 –》1

  • bitcount K1 1 3 : 统计下标1、3字节组中bit=1的个数,即01000000 00000000 00100001 –》3

  • bitcount K1 0 -2 : 统计下标0到下标倒数第2,字节组中bit=1的个数,即01000001 01000000 00000000 –》3

实例2

2022-07-02 日访问网站的userid=1,2,5,9。

setbit users:20220702 1 1

setbit users:20220702 2 1

setbit users:20220702 5 1

setbit users:20220702 9 1

2022-07-03 日访问网站的userid=0,1,4,9。

setbit users:20220703 0 1

setbit users:20220703 1 1

setbit users:20220703 4 1

setbit users:20220703 9 1

计算出两天都访问过网站的用户数量

127.0.0.1:6379> bitop and users:and:20220702_03 users:20220702 users:20220703
(integer) 2
127.0.0.1:6379> bitcount users:and:20220702_03
(integer) 2

计算出任意一天都访问过网站的用户数量(例如月活跃就是类似这种) , 可以使用or求并集

127.0.0.1:6379> bitop or users:or:20220702_03 users:20220702 users:20220703
(integer) 2
127.0.0.1:6379> bitcount users:or:20220702_03
(integer) 6

Bitmaps与set对比

假设网站有1亿用户, 每天独立访问的用户有5千万, 如果每天用集合类型和Bitmaps分别存储活跃用户可以得到表

数据类型 每个用户id占用空间 需要存储的用户量 全部内存量
集合类型 64位 5千万 64位*5千万 = 400MB
Bitmaps 1位 1亿 1位*1亿 = 12.5MB

很明显, 这种情况下使用Bitmaps能节省很多的内存空间, 尤其是随着时间推移节省的内存是非常可观的

数据类型 一天 一个月 一年
集合类型 400MB 12GB 144GB
Bitmaps 12.5MB 375MB 4.5GB

但Bitmaps并不是万金油, 假如该网站每天的独立访问用户很少, 例如只有10万(大量的僵尸用户) , 那么两者的对比如下表所示, 很显然, 这时候使用Bitmaps就不太合适了, 因为基本上大部分位都是0。

数据类型 每个userid占用空间 需要存储的用户量 全部内存量
集合类型 64位 10万 64位*10万 = 800KB
Bitmaps 1位 1亿 1位* 1亿 = 12.5MB

3.2 HyperLogLog

简介

在工作当中,我们经常会遇到与统计相关的功能需求,比如统计网站PV(PageView页面访问量),可以使用Redis的incr、incrby轻松实现。但像UV(UniqueVisitor,独立访客)、独立IP数、搜索记录数等需要去重和计数的问题如何解决?这种求集合中不重复元素个数的问题称为基数问题

解决基数问题有很多种方案:

  • (1)数据存储在MySQL表中,使用distinct count计算不重复个数

  • (2)使用Redis提供的hash、set、bitmaps等数据结构来处理

以上的方案结果精确,但随着数据不断增加,导致占用空间越来越大,当数据集非常大时是不切实际的。

能否能够降低一定的精度来平衡存储空间?Redis推出了HyperLogLog

  • HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。

  • 在 Redis 里面,每个 HyperLogLog 键只需要花费 12KB 内存,就可以计算接近 264 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。

  • 但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。

什么是基数?

比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素)为5。 基数估计就是在误差可接受的范围内,快速计算基数。

HyperLogLog的常见命令

  • pfadd <key> <element> [element …] 添加指定元素到 HyperLogLog 中
127.0.0.1:6379> pfadd hll "redis"
(integer) 1
127.0.0.1:6379> pfadd hll "mysql"
(integer) 1
127.0.0.1:6379> pfadd hll "redis"
(integer) 0
  • pfcount <key> [key …] 计算HLL的近似基数,可以计算多个HLL,比如用HLL存储每天的UV,计算一周的UV可以使用7天的UV合并计算即可
127.0.0.1:6379> pfcount hll
(integer) 2

127.0.0.1:6379> pfadd hll2 "mongodb"
(integer) 1
127.0.0.1:6379> pfadd hll2 "redis"
(integer) 1
127.0.0.1:6379> pfcount hll hll2
(integer) 3
  • pfmerge <destkey> <sourcekey> [sourcekey …] 将一个或多个HLL合并后的结果存储在另一个HLL中,比如每月活跃用户可以使用每天的活跃用户来合并计算可得
127.0.0.1:6379> pfmerge hllsum hll hll2
OK
127.0.0.1:6379> pfcount hllsum
(integer) 3

3.3 Geospatial

简介

Redis 3.2 中增加了对GEO类型的支持。GEO,Geographic,地理信息的缩写。该类型就是元素的2维坐标,在地图上就是经纬度。redis基于该类型,提供了经纬度设置,查询,范围查询,距离查询,经纬度Hash等常见操作。

命令

  • geoadd <key> < longitude> <latitude> <member> [longitude latitude member…] 添加地理位置(经度,纬度,名称)
geoadd china:city 121.47 31.23 shanghai
geoadd china:city 106.50 29.53 chongqing 114.05 22.52 shenzhen 116.38 39.90 beijing
  • geopos <key><member> [member…] 获得指定地区的坐标值
127.0.0.1:6379> geopos china:city shanghai
1) 1) "121.47000163793563843"
   2) "31.22999903975783553"
  • geodist <key> <member1><member2> [m|km|ft|mi] 获取两个位置之间的直线距离
    • m 表示单位为米[默认值]
    • km 表示单位为千米
    • ft 表示单位为英尺
    • mi 表示单位为英里
127.0.0.1:6379> geodist china:city shanghai beijing km
"1068.1535"
  • georadius <key> <longitude> <latitude> radius m|km|ft|mi 以给定的经纬度为中心,找出某一半径内的元素
127.0.0.1:6379> georadius china:city 110 30 1000 km
1) "chongqing"
2) "shenzhen"

四、Jedis

1.引入依赖

<!--引入Jedis依赖-->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>4.2.0</version>
</dependency>

<!--引入单元测试依赖-->
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.8.2</version>
    <scope>test</scope>
</dependency>

2.与Redis建立连接

public class JedisTest {
    public static void main(String[] args) {
        // 获取连接
        Jedis jedis = new Jedis("127.0.0.1",6379);
        // 如果 Redis 服务设置了密码,需要下面这行,没有就不需要
        // jedis.auth("123456"); 
        // 选择库(默认是下标为0的库)
        jedis.select(0);
        System.out.println("连接成功");
        //查看服务是否运行
        System.out.println("服务正在运行: "+jedis.ping());
        jedis.close();
    }
}

3.测试相关数据类型

Jedis-API: Key

jedis.set("k1", "v1");
jedis.set("k2", "v2");
jedis.set("k3", "v3");
Set<String> keys = jedis.keys("*");
System.out.println(keys.size());
for (String key : keys) {
    System.out.println(key);
}
System.out.println(jedis.exists("k1"));
System.out.println(jedis.ttl("k1"));                
System.out.println(jedis.get("k1"));

Jedis-API: String

jedis.mset("str1","v1","str2","v2","str3","v3");
System.out.println(jedis.mget("str1","str2","str3"));

Jedis-API: List

List<String> list = jedis.lrange("mylist",0,-1);
for (String element : list) {
    System.out.println(element);
}

Jedis-API: set

jedis.sadd("orders", "order01");
jedis.sadd("orders", "order02");
jedis.sadd("orders", "order03");
jedis.sadd("orders", "order04");
Set<String> smembers = jedis.smembers("orders");
for (String order : smembers) {
System.out.println(order);
}
jedis.srem("orders", "order02");

Jedis-API: hash

jedis.hset("hash1","userName","lisi");
System.out.println(jedis.hget("hash1","userName"));
Map<String,String> map = new HashMap<String,String>();
map.put("telphone","13735679666");
map.put("address","beijing");
map.put("email","abc@163.com");
jedis.hmset("hash2",map);
List<String> result = jedis.hmget("hash2", "telphone","email");
for (String element : result) {
    System.out.println(element);
}

Jedis-API: zset

jedis.zadd("zset01", 100d, "z3");
jedis.zadd("zset01", 90d, "l4");
jedis.zadd("zset01", 80d, "w5");
jedis.zadd("zset01", 70d, "z6");

List<String> zrange = jedis.zrange("zset01", 0, -1);
for (String e : zrange) {
  System.out.println(e);
}

4.Jedis连接池


Jedis本身是线程不安全的,并且频繁的创建和销毁连接会有性能损耗,因此推荐大家使用Jedis连接池代替Jedis的直连方式

public class JedisConnectionFactory {
    private static final JedisPool jedisPool;

    static {
        //配置连接池
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal(8);
        jedisPoolConfig.setMaxIdle(8);
        jedisPoolConfig.setMinIdle(0);
        jedisPoolConfig.setMaxWaitMillis(200);
        //创建连接池对象
        jedisPool = new JedisPool(jedisPoolConfig,"127.0.0.1",6379,1000,"132537");
    }

    public static Jedis getJedis(){
       return jedisPool.getResource();
    }
}

5、实例—手机验证码

要求:
1、输入手机号,点击发送后随机生成6位数字码,2分钟有效
2、输入验证码,点击验证,返回成功或失败
3、每个手机号每天只能输入3次

思路:

  1. 生成随机6位数字验证码:Random
  2. 验证码在2分钟内有效:把验证码放到redis里面,设置过期时间120秒
  3. 判断验证码是否一致:从redis获取验证码和输入的验证码进行比较
  4. 每个手机每天只能发送3次验证码:incr每次发送后+1,大于2的时候,提交不能发送

生成六位的验证码:

//1.生成6位数字验证码
public static String getCode() {
    Random random = new Random();
    String code = "";
    for(int i=0;i<6;i++) {
        int rand = random.nextInt(10);
        code += rand;
    }
    return code;
}

验证码只能发送三次:

//2 每个手机每天只能发送三次,验证码放到redis中,设置过期时间120
public static void verifyCode(String phone) {
    //连接redis
    Jedis jedis = new Jedis("127.0.0.1",6379);

    //拼接key
    //手机发送次数key
    String countKey = "VerifyCode"+phone+":count";
    //验证码key
    String codeKey = "VerifyCode"+phone+":code";

    //每个手机每天只能发送三次
    String count = jedis.get(countKey);
    if(count == null) {
        //没有发送次数,第一次发送
        //设置发送次数是1
        jedis.setex(countKey,24*60*60,"1");
    } else if(Integer.parseInt(count)<=2) {
        //发送次数+1
        jedis.incr(countKey);
    } else if(Integer.parseInt(count)>2) {
        //发送三次,不能再发送
        System.out.println("今天发送次数已经超过三次");
        jedis.close();
    }

    //发送验证码放到redis里面
    String vcode = getCode();
    jedis.setex(codeKey,120,vcode);//120秒
    jedis.close();
}

判断验证码是否一致:

//3 验证码校验
public static void judgeCode(String phone,String code) {
    //从redis获取验证码
    Jedis jedis = new Jedis("127.0.0.1",6379);
    //验证码key
    String codeKey = "VerifyCode"+phone+":code";
    String redisCode = jedis.get(codeKey);
    //判断
    if(redisCode.equals(code)) {
        System.out.println("成功");
    }else {
        System.out.println("失败");
    }
    jedis.close();
}

完整功能代码展示

public class PhoneCode {

    public static void main(String[] args) {
        //模拟验证码发送
        verifyCode("13678765435");

        //模拟验证码校验
        //judgeCode("13678765435","217173");
    }


    //3 验证码校验
    public static void judgeCode(String phone,String code) {
        //从redis获取验证码
        Jedis jedis = new Jedis("127.0.0.1",6379);
        //验证码key
        String codeKey = "VerifyCode"+phone+":code";
        String redisCode = jedis.get(codeKey);
        //判断
        if(redisCode.equals(code)) {
            System.out.println("成功");
        }else {
            System.out.println("失败");
        }
        jedis.close();
    }

    //2 每个手机每天只能发送三次,验证码放到redis中,设置过期时间120
    public static void verifyCode(String phone) {
        //连接redis
        Jedis jedis = new Jedis("127.0.0.1",6379);

        //拼接key
        //手机发送次数key
        String countKey = "VerifyCode"+phone+":count";
        //验证码key
        String codeKey = "VerifyCode"+phone+":code";

        //每个手机每天只能发送三次
        String count = jedis.get(countKey);
        if(count == null) {
            //没有发送次数,第一次发送
            //设置发送次数是1
            jedis.setex(countKey,24*60*60,"1");//有效期1天
        } else if(Integer.parseInt(count)<=2) {
            //发送次数+1
            jedis.incr(countKey);
        } else if(Integer.parseInt(count)>2) {
            //发送三次,不能再发送
            System.out.println("今天发送次数已经超过三次");
            jedis.close();
            return;//超过三次之后就会自动退出不会再发送了,不添加这一行,即使显示发送次数,但还会有验证码还是会改变
        }

        //发送验证码放到redis里面
        String vcode = getCode();//调用生成的验证码
        jedis.setex(codeKey,120,vcode);//设置生成的验证码只有120秒的时间
        jedis.close();
    }

    //1 生成6位数字验证码,code是验证码
    public static String getCode() {
        Random random = new Random();
        String code = "";
        for(int i=0;i<6;i++) {
            int rand = random.nextInt(10);
            code += rand;
        }
        return code;
    }
}

五、Redis与Spring Boot整合

1、引入redis相关依赖

<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!--连接池依赖-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.0</version>
</dependency>

2、编写配置文件

application.yml 版本

spring:
  redis:
    host: 127.0.0.1 #指定redis所在的host
    port: 6379  #指定redis的端口
    password: 123456  #设置redis密码
    lettuce:
      pool:
        max-active: 8 #最大连接数
        max-idle: 8 #最大空闲数
        min-idle: 0 #最小空闲数
        max-wait: 100ms #连接等待时间

application.properties 版本

#Redis服务器地址
spring.redis.host=127.0.0.1
#Redis服务器连接端口
spring.redis.port=6379
#Redis数据库索引(默认为0)
spring.redis.database= 0
#连接超时时间(毫秒)
spring.redis.timeout=1800000
#连接池最大连接数(使用负值表示没有限制)
spring.redis.lettuce.pool.max-active=20
#最大阻塞等待时间(负数表示没限制)
spring.redis.lettuce.pool.max-wait=-1
#连接池中的最大空闲连接
spring.redis.lettuce.pool.max-idle=5
#连接池中的最小空闲连接
spring.redis.lettuce.pool.min-idle=0

3、编写测试类

@SpringBootTest
class SpringRedisApplicationTests {
    @Resource
    private RedisTemplate redisTemplate;
    @Test
    void testString() {
        // 1.通过RedisTemplate获取操作String类型的ValueOperations对象
        ValueOperations ops = redisTemplate.opsForValue();
        // 2.插入一条数据
        ops.set("name","jianjian");
        // 3.获取数据
        String name = (String) ops.get("name");
        System.out.println("name = " + name);
    }
}

4、RedisSerializer配置


RedisTemplate可以接收任意Object作为值写入Redis,只不过写入前会把Object序列化为字节形式,默认是采用JDK序列化,得到的结果是这样的

缺点:

  • 可读性差
  • 内存占用较大

那么如何解决以上的问题呢?我们可以通过自定义RedisTemplate序列化的方式来解决。

编写一个配置类RedisConfig

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){
        // 1.创建RedisTemplate对象
        RedisTemplate<String ,Object> redisTemplate = new RedisTemplate<>();
        // 2.设置连接工厂
        redisTemplate.setConnectionFactory(factory);

        // 3.创建序列化对象
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();

        // 4.设置key和hashKey采用String的序列化方式
        redisTemplate.setKeySerializer(stringRedisSerializer);
        redisTemplate.setHashKeySerializer(stringRedisSerializer);

        // 5.设置value和hashValue采用json的序列化方式
        redisTemplate.setValueSerializer(genericJackson2JsonRedisSerializer);
        redisTemplate.setHashValueSerializer(genericJackson2JsonRedisSerializer);

        return redisTemplate;
    }
}

此时我们已经将RedisTemplate的key设置为String序列化,value设置为Json序列化的方式,再来执行方法测试

由于我们设置的value序列化方式是Json的,因此我们可以直接向redis中插入一个对象

@Test
void testSaveUser() {
    redisTemplate.opsForValue().set("user:100", new User("Vz", 21));
    User user = (User) redisTemplate.opsForValue().get("user:100");
    System.out.println("User = " + user);
}

尽管Json序列化可以满足我们的需求,但是依旧存在一些问题。

如上图所示,为了在反序列化时知道对象的类型,JSON序列化器会将类的class类型写入json结果中,存入Redis,会带来额外的内存开销。

那么我们如何解决这个问题呢?我们可以通过下文的StringRedisTemplate来解决这个问题。

5、StringRedisTemplate配置

为了节省内存空间,我们并不会使用JSON序列化器来处理value,而是统一使用String序列化器,要求只能存储String类型的key和value。当需要存储Java对象时,手动完成对象的序列化和反序列化。

Spring默认提供了一个StringRedisTemplate类,它的key和value的序列化方式默认就是String方式。省去了我们自定义RedisTemplate的过程

编写一个测试类使用StringRedisTemplate来执行以下方法

@SpringBootTest
class RedisStringTemplateTest {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Test
    void testSaveUser() throws JsonProcessingException {
        // 1.创建一个Json序列化对象
        ObjectMapper objectMapper = new ObjectMapper();
        // 2.将要存入的对象通过Json序列化对象转换为字符串
        String userJson1 = objectMapper.writeValueAsString(new User("Vz", 21));
        // 3.通过StringRedisTemplate将数据存入redis
        stringRedisTemplate.opsForValue().set("user:100",userJson1);
        // 4.通过key取出value
        String userJson2 = stringRedisTemplate.opsForValue().get("user:100");
        // 5.由于取出的值是String类型的Json字符串,因此我们需要通过Json序列化对象来转换为java对象
        User user = objectMapper.readValue(userJson2, User.class);
        // 6.打印结果
        System.out.println("user = " + user);
    }
}

执行完毕回到Redis的图形化客户端查看结果

参考

Sponsor❤️

您的支持是我不断前进的动力,如果您感觉本文对您有所帮助的话,可以考虑打赏一下本文,用以维持本博客的运营费用,拒绝白嫖,从你我做起!🥰🥰🥰

支付宝 微信

文章作者: 简简
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 简简 !
评论
填上邮箱会收到评论回复提醒哦!!!
 本篇
Redis Redis
一、Redis简介1.认识NoSQL1.1 什么是NoSQL NoSQL最常见的解释是”non-relational“,泛指非关系型的数据库,很多人也说它是”Not Only SQL“ NoSQL 不依赖业务逻辑方式存储,而以简单
2022-07-03
下一篇 
MyBatis-Plus MyBatis-Plus
MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。 一、快速入门1.建库建表现有一张 User 表,其表结构如下: id name age
2022-06-26
  目录