Redis-笔记

零、参考资料

一、Redis 的安装

1、安装步骤

1、安装GCC环境:

1
2
3
yum install gcc

gcc --vserion

2、上传 Redis的压缩包/opt/目录下

3、解压 Redis的压缩包

1
tar -zxvf 压缩包名

4、进入解压后的目录,编译并安装:

1
2
3
4
5
cd 压缩包名

make

make install

5、安装完毕(默认安装在/usr/local/bin/目录下)

注意:

此时,Redis有两个主要的目录,一个是解压目录(/opt/redis-6.2.2/),一个是安装目录/usr/local/bin/

2、安装过程中可能出现的报错

问题:

若没有准备好C语言的编译环境,make编译时,会产生–Jemalloc/jemalloc.h:没有那个文件的报错。

解决:

/opt/redis解压目录/目录下, 执行命令make distclean

3、Redis 安装目录介绍

目录名 说明
redis-benchmark 性能测试工具
redis-check-aop 修复AOP文件
redis-check-dump 修复dump.rdb文件
redis-sentinel redis集群使用
redis-server Redis服务器启动命令
redis-cli 客户端,操作入口

4、Redis 启动服务端步骤

Redis有 前台启动后台启动 两种。前台启动就是shell窗口不能关;后台启动就是后台默默运行。

推荐使用Redis的后台启动。

1、备份redis.conf文件:

1
2
3
4
5
6
7
# 进入redis的解压目录
cd /opt/redis-6.2.6/


# 将redis.conf 文件备份到 /etc目录下
cp ./redis.conf /etc/redis.conf

2、将/etc/redis.conf 文件(128行)中的daemonize no改为yes

1
2
3
4
5
vi /etc/redis.conf

# 找到 daemonize 所在的行,输入 i 进入插入模式,将no改为yes

# 按 esc 键,输入 :wq 保存

3、进入redis的安装目录/usr/local/bin/,启动:

1
2
3
4
5
6
7
8
# 进入redis的安装目录
cd /usr/local/bin

# 启动redis的服务端,并指定配置文件(刚才备份到了 /etc 目录下)
redis-server /etc/redis.conf

# 查看是否启动成功
ps -ef | grep redis

5、Redis 启动客户端步骤

进入redis的安装目录,执行启动redis客户端的命令:

1
2
3
cd /usr/local/bin/

redis-cli

6、Redis 关闭客户端步骤

Redis关闭客户端有三种方式。

单例关闭:

1
redis-cli shutdown

进入终端关闭:

1
shutdown

多例关闭:

1
redis-cli -p 6379 shutdown

7、Redis 相关知识的介绍

Redis的端口是6379,默认有16个数据库(编号0~15),默认0号库。

redis 基于key-value。

redis 具有统一的密码管理(所有库的密码都相同)。

reids 是单线程 + 多路IO复用技术。

redis 与 Memcache 的不同:数据类型更多、支持持久化、单线程+IO复用

切换数据库:

1
select 数据库编号

查看当前数据库的key数:

1
dbsize

清空库:

1
flushdb

清空所有库:

1
flushall

二、Redis 常用数据类型

1、通用的操作

常用操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

# 切换redis数据库,redis默认有16个数据库,编号为0~15
select 数据库编号


# 查看当前的数据库有多少key
dbsize


# 清空当前库
flushdb


# 清空所有库
flushall


# 查看当前库中的所有key
keys *


# 判断某个key是否存在
exits 键名


# 判断key 的数据类型
type 键名


# 删除指定的key及其对应的数据
del 键名


# 根据value值选择非阻塞删除,仅将keys从keyspace元数据中删除,真正的删除会在后续的异步操作
unlink 键名


# 给某个key设定过期时长
expire 键名 秒数


# 查看还有多少秒过期,-1表示永不过期,-2表示已过期
ttl 键名

Redis 常用的五大数据类型:

  • string
  • list
  • set
  • zset
  • hash

2、string 类型

string 类型是 redis 的一个基本类型,一个 key 对应一个 value 。每个value最大为 512 MB

string 类型是 二进制安全的,可以包含任何数据,如:图片、序列化对象。

常用命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

# 设置/修改数据
set 键名 值


# 读取数据
get 键名


# 追加数据,没有键则新建,有键则追加到原数据的末尾
append 键名 值


# 获取键所对应的value的长度
strlen 键名


# 只有不存在key时,才设置键值对
setnx 键名 值


# 让数值value自增1(只有是数值时才生效),若为空,则设置后为1
incr 键名


# 让数值value自减1(只有是数值时才生效),若为空,则设置后为1
decr 键名


# 指定步长的自增
incrby 键名 步长


# 指定步长的自减
decrby 键名 步长


# 一次性设置多个key-value
mset 键名1 值1 键名2 值2


# 一次性获取多个key-value
mget 键名1 键名2


# 当所指定的key不存在的时候,一次性设置多个key-value
msetnx 键名1 值1 键名2 值2


# 截取子串,类型Java中的substring()函数,左右均包括
getrange 键名 开始下标 结束下标


# 设置key-value的同时指定过期时间
setex 键名 过期时间 值


# 以新换旧,获取旧值、设置新值
getset 键名 新值

原子性:不会被线程调度打断。单线程时,都是原子操作;多线程时,不被多线程打断操作的就是原子性。(一个失败,则全部失败)

Redis的原子性得益于Redis的单线程。

string 类型就是一个简单动态字符串(SDS),类型Java中ArrayList的效果。

string 内部的数据小于1 MB时,翻倍扩容;数据大于1 MB时,每次最多扩容1 MB。最大长度512 MB

3、list 类型

list 类型是单键多值,一个键多个值。在底层使用的是双向链表,因此可以在任意位置进行插入和删除。

list 列表元素较少时,使用空间连续的压缩列表ziplist

list 列表元素较少时,使用空间连续的压缩列表ziplist

常用命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

# 查看连续的多个数据,结束下标=-1时,表示开始下标到最后一个元素
lrange 键名 开始下标 结束下标

# (从左边开始)按下标查看列表的元素
lindex 键名 下标


# 获取列表长度
llen 键名


# 从左边插入
lpush 键名1 值1 键名2 值2


# 从右边插入
rpush 键名1 值1 键名2 值2


# 从左边删除(list左边的1个值),可以在命令最后指定要删除的元素个数
lpop 键名


# 从右边删除(list右边的1个值)
rpop 键名


# list1右边的数据移到list1左边
rpoplpush list1的键名 list2的键名



# 在第一个指定值的(前面brfore|后边after)插入新值
linsert 键名 before 指定值 新值
linsert 键名 after 指定值 新值


# 从左边删除n个指定值
lrem 键名 n 指定值


# 替换指定下标的值
lset 键名 下标 新值

4、set 类型

set 类型是一个可去重的 string类型的无序集合

set 类型的底层是 value为 null 的 hash 表,所以其增删改查的复杂度都是O(1)

常用操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

# 添加多个值到set集合中
sadd 键 值1 值2


# 删除set集合中指定的元素
srem 键 值1 值2


# 随机删除set集合中的一个值
spop 键


# 取出set集合中的所有值
smembers 键


# 随机查询set集合中的n个值(不会删除)
srandmember 键 n


# 判断指定的set集合中是否存在该值,存在:1 ;不存在:0
sismember 键 值


# 返回set集合中的元素个数
scard 键



# 将值从源set集合移动到目标集合
smove src的key dst的key 值


# 返回两个集合的交集
sinter 键1 键2


# 返回两个集合的并集
sunion 键1 键2


# 返回两个集合的差集
sdiff 键1 键2

5、hash 类型

hash 类型对应的数据结构:压缩列表ziplist、哈希表hashtable

field-value长度较短、且数量较少时,使用 ziplist。否则,使用 hashtable

hash 结构

常用操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# 添加数据(hash的数据部分是哈希表,field相当于数据部分的哈希表的键)
hset 键 字段 值


# 获取数据
hget 键 字段


# 设置hash表数据部分的多个值
hmset 键 字段1 值1 字段2 值2


# 设置hash表数据部分的多个值
hexists 键 字段


# 获取指定的hash表所有的字段
hkeys 键


# 获取指定的hash表中所有的值
hvals 键


# 给指定的hash表中指定的字段所对应的值增加指定的增量
hincrby 键 字段 增量


# 当指定的字段不存在时,添加值
hsetnx 键 字段 值

6、zset 类型

zset 类型的没有重复元素有序集合,是元素均为字符串。

zset 集合的每个成员都关联的一个评分(score),按 “评分” 升序将集合的的元素排列。

zset 集合的成员不能重复,但“评分”可以重复

因为元素是有序的,所以可以很快根据 “评分” 、“次序”来获取一个范围的元素。

在底层使用跳跃表实现,既有红黑树的效率,又比红黑树实现起来简单。

常用操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 添加元素
zadd 键 评分 值1 值2 值3


# 返回范围在[起始下标 , 终止下标]之间的元素,若结尾有withscores,表示返回:评分+值
zrange 键 起始下标 终止下标 withscores


# 返回zset集合中,所有score值在[min,max]之间的集合值,按score升序排列(最大值后的参数可省略)
zrangebyscore 键 评分的最小值 评分的最大值 withscore limit 偏移量 个数


#返回zset集合中,所有score值在[min,max]之间的集合值,按score降序排列(最大值后的参数可省略)
zrevrangebyscore 键 评分的最小值 评分的最大值 withscore limit 偏移量 个数


# 给元素的score评分加上增量
zincrby 键 增量 值


# 删除指定的元素
zrem 键 值


# 统计zset集合内,score在 [min,max]内的元素个数
zcount 键 最小值 最大值


# 返回该值在集合中的排名,从0开始
zrank 键 值

7、bitmaps 类型

常用于统计用户的访问量。

活跃的用户越多,(相比 set 类型)使用 bitmaps 来统计活跃用户就越省空间。

  • bitmaps 类型 本质上还是字符串类型,但可以对字符串中的进行操作。

  • bitmaps 类型 单独提供了一套命令,所以在 Redis 中使用bitmaps和使用字符串的命令有所不同。

  • 可以把 bitmaps 类型 看作是以bit 位为单位的数组,数组的每个单元都只能存储 0 和 1,数组的下标在 bitmaps 类型中叫偏移量

常用命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 设置bitmap,偏移量是数组下标(一般用来表示用户的id),值是0或1
setbit 键 偏移量 值


# 获取
getbit 键 偏移量


# 统计 bitmaps 字符串中的有多少个bit位是1。开始、结束字节若为负数,表示从最后一个字节开始往前数
bitcount 键 开始字节 结束字节


# 位运算,可以做多个bitmaps的 and、or、not、xor等操作,并将结果保存到destkey所表示的bitmaps中
bitop 位运算符 运算结果的键 操作bitmaps1的键 操作bitmaps2的键

setbit:

bitop:

8、hyperloglog 类型

统计相关的功能需求,例如:网站的 PageView(PV)可以使用Redis 的 incr、incrby 实现。

但是,例如:网站的 UniqueView(UV)、独立IP数、搜索记录数等需要去重的问题应该如何解决?

求集合中不重复元素的个数的问题——基数问题

基数问题的解决方案:

  • 存储在MySQL中,使用distinct count(*)计算不重复的个数。
  • 使用 Redis 提供的 hash、set、bitmaps 等数据结构。

以上两种方法虽然结果精确,但随着数据量的增加,将导致占用的空间越来越多。

hyperloglog 类型 就是专门用于做基数统计的数据类型。

优点:

在输入的数据数量或体积非常大的时候,计算基数所需的空间很小且固定

在 hyperloglog 类型中,每个键只需花费12 KB的内存就可以计算接近 2^64^ 个不同的基数

缺点:

只会根据输入元素来计算基数,不会存储输入元素,因此不能返回输入的元素。

常用命令:

1
2
3
4
5
6
7
8
9
10
11
12

# 添加指定元素到 hyperloglog 中,添加成功返回1,失败返回0
pfadd 键 元素1 元素2 元素3 .......


# 统计基数
pfcount 键


# 将多个hyperloglog 类型的数据合并后保存到另一个 hyperloglog 中
pfmerge 合并后的结果键 待合并的htyperloglog1 待合并的htyperloglog2

9、geospatial 类型

geospatial 类型 存储的就是元素的二维坐标(经纬度)。

Redis 基于该数据类型,提供了 经纬度的设置、查询、范围查询、距离查询、经纬度hash等常见操作。

常用命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 添加
# 两极无法直接添加,一般使用时会下载城市数据然后通过Java一次性导入。不能重复添加,超出范围会报错。
# 有效的经度:-180~+180
# 有效的纬度:-85.05112878 ~ 85.05112878
geoadd 键 经度1 纬度1 地点的名称1 [经度2 纬度2 地点的名称2........]


# 获取
geopos 键 地点的名称


# 获取两点之间的直线距离,长度单位:m米(默认)、km千米、ft英尺、mi英里
geodist 键 地点的名称1 地点的名称2 长度单位


# 找出方圆多少距离的元素
geo 键 经度 纬度 b距离 长度单位

三、Redis 配置文件

        在安装时,我们将Redis的配置文件(redis.conf)复制到了/etc/目录下,并在启动redis-server时,指定了/etc/redis.conf配置文件。下面讲解Redis的配置文件。

0、常用设置

(1)注释掉 bind,使得 Redis 可以被外网PC访问:

1
# bind 127.0.0.1 -::1

(2)将 protected-mode 的值改为no,使得 Redis 可以被外网PC访问:

1
protected-mode no

(3)将 daemonize 的值改为yes,使得 Redis 可以后台运行:

1
daemonize yes

(4)设置maxmemory,防止内存不足时,服务器宕机:

1
maxmemory <bytes>

(5)修改密码requirepass

1
2
3
4
requirepass 你的密码

# 在Java代码中:
jedis.auth("密码")

(6)重启Redis

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 1.关闭redis,在 redis-cli 所在的目录下输入: 
redis-cli -a 密码


# 进入到 redis 指令模式,输入:
shutdown


# 然后再输入:
exit


# 2.启动redis
# 在 redis-server 所在的目录下输入:
redis-server redis.conf所在目录/redis.conf

# 可以通过指令 ps aux | grep redis 查看redis状态

# 开放端口:
firewall-cmd --add-port=6379/tcp --permanent

# 重启防火墙生效
firewall-cmd --reload
firewall-cmd --query-port=6379/tcp

1、units 单位

        Redis 的配置文件的开头定义了一些基本的度量单位,只支持 byte字节,不支持bit位。

        大小写不敏感

2、include 引入其他配置文件

1
2
3
include /path/to/local.conf

include /path/to/other.conf

3、network 网络设置

3.1、bind 绑定 IP 地址

建议注释掉,否则只能本地访问。

1
2
3
4
5
# bind 192.168.1.100 10.0.0.1     # listens on two specific IPv4 addresses
# bind 127.0.0.1 ::1 # listens on loopback IPv4 and IPv6
# bind * -::* # like the default, all available interfaces

bind 127.0.0.1 -::1

3.2、protected-mode 是否允许远程访问

1
2
3
# yes: 不能远程访问
# no: 可以远程访问
protected-mode yes

3.3、port 服务监听的端口

1
port 6379

3.4、 tcp-backlog 连接队列

tcp-backlog 是一个连接队列。

tcp-backlog 连接队列总和 = 未完成三次握手的队列 + 已完成三次握手的队列

高并发环境下,需要将 tcp-backlog 调高来避免慢客户端连接的问题。

注意:

        Linux 会将tcp-backlog的值减小到/proc/sys/net/core/somaxconn的值(128)。所以,需要确认增大/proc/sys/net/core/somaxconn/proc/sys/net/ipv4/tcp_max_syn_backlog(128)来达到想要的效果。

1
2
3
4
5
6
7
8
9
# TCP listen() backlog.
#
# In high requests-per-second environments you need a high backlog in order
# to avoid slow clients connection issues. Note that the Linux kernel
# will silently truncate it to the value of /proc/sys/net/core/somaxconn so
# make sure to raise both the value of somaxconn and tcp_max_syn_backlogprotected-mode yes
# in order to get the desired effect.

tcp-backlog 511

3.5、 time-out 超时

1
2
# Close the connection after a client is idle for N seconds (0 to disable)
timeout 0

3.6、tcp-keepalive

300 秒没有操作就断开

1
2
# A reasonable value for this option is 300 seconds, 
tcp-keepalive 300

4、general 通用配置

4.1、daemon 后台启动

1
2
3
4
# By default Redis does not run as a daemon. Use 'yes' if you need it.
# Note that Redis will write a pid file in /var/run/redis.pid when daemonized.
# When Redis is supervised by upstart or systemd, this parameter has no impact.
daemonize yes

4.2、pidfile 文件

1
2
3
4
5
6
# Creating a pid file is best effort: if Redis is not able to create it
# nothing bad happens, the server will start and run normally.
#
# Note that on modern Linux systems "/run/redis.pid" is more conforming
# and should be used instead.
pidfile /var/run/redis_6379.pid

4.3、loglevel 日志等级

1
2
3
4
5
6
7
8
# Specify the server verbosity level.
# This can be one of:
# debug (a lot of information, useful for development/testing)
# verbose (many rarely useful info, but not a mess like the debug level)
# notice (moderately verbose, what you want in production probably)
# warning (only very important / critical messages are logged)
# 生产环境
loglevel notice

4.4、database 默认的数据库数

1
2
3
4
5
# Set the number of databases. The default database is DB 0, you can select
# a different one on a per-connection basis using SELECT <dbid> where
# dbid is a number between 0 and 'databases'-1
# 范围:[0,databases-1]
databases 16

5、security 安全

5.1、设置密码

redis-cli客户端中,输入:

1
2
3
4
5
config get requirepass

config set requirepass "123456"

auth 123456

6、limit 限制

6.1、maxclients 最大客户端连接数

  • 该配置用于设置Redis最多同时可以连接多少个客户端。

  • 默认为10000个客户端。

  • 若达到此限制,则拒绝新的连接请求,并向连接的请求方发送max number of clients reached来做回应。

1
# maxclients 10000

6.2、maxmemory 最大内存

  • 建议必须设置,否则当内存满时,会造成服务器宕机。

  • 设置 redis 可以使用的内存大小。一旦达到内存的使用上限,则会移除内部的数据。

  • 移除数据的规则可以通过maxmemory-policy来指定。

1
maxmemory <bytes>

四、订阅与发布

1、什么是订阅和发布

Redis 发布和订阅是一种消息通信模式:

  • 发布者(pub)发送消息
  • 订阅者(sub)接受消息

Redis 客户端可以订阅任意数量的频道

发布:

订阅:

2、订阅与发布的实现案例

2.1、Redis 客户端1 订阅频道

1
2
# 订阅频道(channel_1)
subscribe channel_1

2.2、Redis 客户端2 发布消息

1
2
# 向redis客户端1所订阅的频道channel_1 发布消息
publish channel_1 hello—world

2.2、Redis 客户端1 接收消息

五、Jedis 操作

1、Maven 依赖

1
2
3
4
5
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.0.1</version>
</dependency>

2、Jedis 的连通

(1)修改Redis的设置:

(注释掉/etc/redis.conf配置文件中的bind语句,protected-mode设为no,重启 Redis 服务)

1
2
3
4
5
6
# 关闭Redis服务
kill -9 6379


# 打开Redis服务
/usr/local/bin/redis-server /etc/redis.conf

(2)关闭CentOS 的防火墙

1
2
3
4
5
6
# 查看防火墙
systemctl status firewalld


# 关闭防火墙
systemctl stop firewalld

(3)测试Jedis能否连通Redis服务器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.cyw;

import redis.clients.jedis.Jedis;

public class TestJedis {
public static void main(String[] args) {

// 根据ip地址、端口,创建Jedis对象
Jedis jedis = new Jedis("192.168.220.137",6379);

// 测试能否连通Redis服务器
String ping = jedis.ping();

// 打印 pong 表示已经连通
System.out.println(ping);

jedis.close();

}
}

3、Jedis 的 API-keys

1
2
3
4
5
6
7
8
9
@Test
public void demo1(){
Jedis jedis = new Jedis("192.168.220.137", 6379);

// 查询Redis的当前数据库中的所有键
// 相当于Redis原生的命令:keys *
Set<String> keys = jedis.keys("*");
System.out.println(keys);
}

4、Jedis 的 API-String

其他类型与String类型的用法类型,都是将原生的命令改为方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Test
public void demo1(){
Jedis jedis = new Jedis("192.168.220.137", 6379);

// 添加str类型的数据
String ok = jedis.set("k1", "abc");
System.out.println(ok);


// 获取str类型的数据
String k1 = jedis.get("k1");
System.out.println(k1);


// 判断某个键是否存在
boolean isExist = jedis.exists("k1");
System.out.println(isExist);


// 设置有效期(秒)
jedis.expire("k1", 30);


// 获取剩余有效期(秒):-1永不过期,-2已过期
long ttl = jedis.ttl("k1");
System.out.println(ttl);


// 批量设置
jedis.mset("k1","a1","k2","a2");
System.out.println(jedis.get("k1"));
System.out.println(jedis.get("k2"));


// 释放资源
jedis.close();

}

六、案例-模拟手机验证码

1、要求

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

2、实现思路

  • 随机生成6位数字的验证码:Java中的Random类的nextInt()方法。
  • 2分钟有效:使用 Jedis操作Redis设置验证码的有效期(jedis.expire("键",有效秒数)
  • 返回成功或失败:使用Jedis的jedis.get("键")获取Redis中的验证码,并将取出的值与用户的输入值比较
  • 每天只能输入3次:使用Redis的incrby命令

3、实现

3.1、生成验证码

1
2
3
4
5
6
7
8
9
// 生成6位数的验证码的方法
public static String getCode(){
Random rd = new Random();
String str = "";
for (int i = 0; i < 6; i++) {
str += rd.nextInt(10);
}
return str;
}

3.2、限制每个手机每天只能发送三次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 限制每个手机每天只能发送3次,
// 存储验证码到Redis,设置过期时间
public static void sendCode(String phone){

Jedis jedis = new Jedis("192.168.220.137", 6379);

// 拼接用户验证码生成次数的key
String countKey = "verifyCode"+phone+":count";

// 尝试去Redis中获取验证码的生成次数
String count = jedis.get(countKey);

if (count == null){
// 为null表示没有验证码(第一次生成验证码)
// 有效期:一天
jedis.setex(countKey,24*60*60,"1");
}else if (Integer.parseInt(count)<3){
jedis.incr(countKey);
}else if (Integer.parseInt(count)>=3){
System.out.println("今天的发送次数已达3次,不能再发送了");
jedis.close();
return;
}

// 拼接用户验证码的key
String codeKey = "verifyCode"+phone+":code";

// 后端生成验证码
String code = PhoneCode.getCode();

// 设置验证码的有效期为2分钟
jedis.setex(codeKey,120,code);

jedis.close();
}

3.3、校验验证码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 校验验证码
public static void verifyCode(String phone,String code){

Jedis jedis = new Jedis("192.168.220.137", 6379);

// 拼接用户验证码的key
String codeKey = "verifyCode"+phone+":code";

String redisCode = jedis.get(codeKey);

if (redisCode.equalsIgnoreCase(code)){
System.out.println("验证通过!");
}else{
System.out.println("验证失败!");
}
jedis.close();
}

3.4、测试

1
2
3
4
5
    public static void main(String[] args) {

// sendCode("123456");
verifyCode("123456","844525");
}

3.5、完整版代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
package com.cyw;

import redis.clients.jedis.Jedis;

import java.util.Random;

public class PhoneCode {
public static void main(String[] args) {

// sendCode("123456");
verifyCode("123456","844525");
}

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


// 限制每个手机每天只能发送3次,
// 存储验证码到Redis,设置过期时间
public static void sendCode(String phone){

Jedis jedis = new Jedis("192.168.220.137", 6379);

// 拼接用户验证码生成次数的key
String countKey = "verifyCode"+phone+":count";

// 尝试去Redis中获取验证码的生成次数
String count = jedis.get(countKey);

if (count == null){
// 为null表示没有验证码(第一次生成验证码)
// 有效期:一天
jedis.setex(countKey,24*60*60,"1");
}else if (Integer.parseInt(count)<3){
jedis.incr(countKey);
}else if (Integer.parseInt(count)>=3){
System.out.println("今天的发送次数已达3次,不能再发送了");
jedis.close();
return;
}

// 拼接用户验证码的key
String codeKey = "verifyCode"+phone+":code";

// 后端生成验证码
String code = PhoneCode.getCode();

// 设置验证码的有效期为2分钟
jedis.setex(codeKey,120,code);

jedis.close();
}


// 校验验证码
public static void verifyCode(String phone,String code){

Jedis jedis = new Jedis("192.168.220.137", 6379);

// 拼接用户验证码的key
String codeKey = "verifyCode"+phone+":code";

String redisCode = jedis.get(codeKey);

if (redisCode.equalsIgnoreCase(code)){
System.out.println("验证通过!");
}else{
System.out.println("验证失败!");
}
jedis.close();
}
}

七、Redis 整合 SpringBoot

1、Maven依赖

1
2
3
4
5
6
7
8
9
10
<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.10.0</version>
</dependency>

2、SpringBoot 的配置文件中的 Redis 部分

application.properties

1
2
3
4
5
6
7
8
spring.redis.host=192.168.220.137			# redis 服务器的地址
spring.redis.port=6379 # redis 服务器的端口
spring.redis.database=0 # redis服务器的数据库索引
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、SpringBoot 的 Redis 配置类

教程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {

@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance ,
ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
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<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
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;
}
}

解决乱码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Configuration
public class RedisConfig {

/**
* 解决Redis乱码问题
* @param redisConnectionFactory
* @return
*/
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);

jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

//重点在这四行代码
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);

redisTemplate.afterPropertiesSet();
return redisTemplate;
}

}

4、测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
@RequestMapping("/redisTest")
public class RedisTestController {
@Autowired
private RedisTemplate redisTemplate;

@GetMapping
public String testRedis() {
//设置值到redis
redisTemplate.opsForValue().set("name","学习aaa");
//从redis获取值
String name = (String)redisTemplate.opsForValue().get("name");
return name;
}
}

八、事务 与 锁机制

Redis 事务是一个单独的隔离操作:

  • 事务中的所有命令都会序列化、按顺序执行。
  • 事务在执行过程中,不会被其他客户端发送的命令请求打断。

Redis 事务的主要作用:串联多个命令,防止别的命令插队。

不支持ACID

1、基本操作

  • multi:排队(开启事务)

  • exec :执行

  • discard:放弃排队(回滚)

开启事务、执行事务:

回滚事务:

2、事务的错误处理

2.1、组队出错

组队阶段的出现错误,则队列内的命令都不会执行成功:

2.2、执行出错

执行阶段的出现错误,则队列内正确的命令可以执行成功:

3、悲观锁

悲观锁:每次取数据,都认为别人会修改,所以每次拿数据时都会上锁,其他人取数据时会进入阻塞状态。传统的关系型数据库里的行锁表锁读锁写锁等就利用了悲观锁机制。

缺点:效率低。

4、乐观锁

乐观锁:给数据加上一个版本号,更新数据的时候,检查版本号是否与数据库中的一致。适用于多读的情况(如:抢票),可以提高吞吐量。Redis 就是利用这种 check-and-set机制实现事务的。

乐观锁的使用:(watch 命令)

假设现在有2个Redis客户端,同时操作键名为“balance”的数据:

  • (双方)先执行watch命令,监视某些键(如:watch balance)。
  • (双方)再执行multi命令,开启事务。
  • (双方)数据更新操作加入到队列(如:incrby balance 10
  • 执行队列中的操作exec
  • 当第二个客户端执行操作时,返回nil表示Redis客户端2的更新操作执行执行失败。(乐观锁生效)
  • 取消监视unwatch

5、Redis 事务的三个特性

  • 单独的隔离操作(所有命令顺序执行,不会被其它客户端的命令打断)
  • 没有隔离级别的概念(队列中的所有命令,没提交前都不会被实际执行)
  • 不保证原子性(队列中的一条命令执行失败,其他正确的命令不会回滚,仍然执行)

6、案例-秒杀

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package com.cyw.kill;

import redis.clients.jedis.Jedis;

public class KillTest {
public static void main(String[] args) {
Jedis jedis = new Jedis("192.168.220.138", 6379);
System.out.println(jedis.ping());
}


public boolean doKill(String uid,String productId){
// 1、判断uid和productId是否为null
if (uid == null || productId == null){
return false;
}

// 2、连接Redis服务器
Jedis jedis = new Jedis("192.168.220.138", 6379);


// 3、拼接库存key
String remainNumKey= "kill_DB:"+productId+":qt";

// 4、拼接秒杀成功的用户key
String userKey = "kill_DB:"+uid+":user";


// 5、获取库存,若为null,表示还没开始
String remainNum = jedis.get(remainNumKey);

if (remainNum == null){
System.out.println("秒杀还没开始!");
jedis.close();
return false;
}

// 6、判断用户是否重复秒杀
if(jedis.sismember(userKey, uid)){
System.out.println("已经秒杀成功,不能重复秒杀");
jedis.close();
return false;
}

// 7、判断如果商品数量、库存小于1,秒杀结束
if (Integer.parseInt(remainNum) < 1){
System.out.println("秒杀已经结束!");
jedis.close();
return false;
}

// 秒杀过程,库存减一,把用户添加到名单里
jedis.decr(remainNumKey);
jedis.sadd(userKey,uid);
System.out.println("秒杀成功!");

return true;
}
}

7、ab工具模拟并发

(1)安装:yum install httpd-tools

(2)检查是否安装成功:ab --help

(3)请求1000次,并发量100:ab -n 1000 -c 100 http://192.168.220.138:8080/secondKill

(4)POST请求时,把请求体放到文件里,并指定请求参数的类型(请求路径不能写localhost):ab -n 1000 -c 100 -p postfile -T 'application/x-www-form-urlencoded' 需要测试的网站的url

连接超时问题:【使用连接池】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class JedisPoolUtil {
private static volatile JedisPool jedisPool = null;

private JedisPoolUtil() {
}

public static JedisPool getJedisPoolInstance() {
if (null == jedisPool) {
synchronized (JedisPoolUtil.class) {
if (null == jedisPool) {
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(200);
poolConfig.setMaxIdle(32);
poolConfig.setMaxWaitMillis(100*1000);
poolConfig.setBlockWhenExhausted(true);
poolConfig.setTestOnBorrow(true); // ping PONG,判断是否还存在

// 配置、ip、端口、超时时间
jedisPool = new JedisPool(poolConfig, "172.22.109.205", 6379, 60000 );
}
}
}
return jedisPool;
}

public static void release(JedisPool jedisPool, Jedis jedis) {
if (null != jedis) {
jedisPool.returnResource(jedis);
}
}

}

在【秒杀案例的代码中】通过创建连接池再来获取数据:

1
2
3
4
5
//2 连接redis
//Jedis jedis = new Jedis("192.168.44.168",6379);
//通过连接池得到jedis对象
JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
Jedis jedis = jedisPoolInstance.getResource();

最终的【秒杀案例代码】:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package com.cyw.kill;

import redis.clients.jedis.Jedis;

public class KillTest {
public static void main(String[] args) {
Jedis jedis = new Jedis("192.168.220.138", 6379);
System.out.println(jedis.ping());
}


public boolean doKill(String uid,String productId){
// 1、判断uid和productId是否为null
if (uid == null || productId == null){
return false;
}

// 2、连接Redis服务器
JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
Jedis jedis = jedisPoolInstance.getResource();

// 3、拼接库存key
String remainNumKey= "kill_DB:"+productId+":qt";

// 4、拼接秒杀成功的用户key
String userKey = "kill_DB:"+uid+":user";

// 监视库存(乐观锁)
jedis.watch(remainNumKey);

// 5、获取库存,若为null,表示还没开始
String remainNum = jedis.get(remainNumKey);

if (remainNum == null){
System.out.println("秒杀还没开始!");
jedis.close();
return false;
}

// 6、判断用户是否重复秒杀
if(jedis.sismember(userKey, uid)){
System.out.println("已经秒杀成功,不能重复秒杀");
jedis.close();
return false;
}

// 7、判断如果商品数量、库存小于1,秒杀结束
if (Integer.parseInt(remainNum) < 1){
System.out.println("秒杀已经结束!");
jedis.close();
return false;
}


// 开启事务
Transcation multi = jedis.multi();

// 秒杀过程,库存减一,把用户添加到名单里
multi.decr(remainNumKey);
multi.sadd(userKey,uid);
List<Object> rst = multi.exec();
if(rst==null || rst.size()==0){
System.out.println("秒杀失败!");
jedis.close();
return false;
}
System.out.println("秒杀成功!");

return true;
}
}

九、Redis 的持久化

Redis 中的持久化操作分为:

  • RDB
  • AOF

1、RDB

RDB 就是在指定的时间间隔内,将内存中的数据集快照写入磁盘,也就是Snapshot 快照,它回复时是将快照文件直接读到内存中。(每隔一段时间,就将内存数据写入磁盘)

如何备份:

  • Redis 会单独创建(fork)一个子进程来进行持久化
  • 先将数据写入临时文件,等到出啊结束后,再用临时文件替换上一次的持久化文件
  • 主进程不会有I/O 操作,确保了性能
  • RDB 比 AOF 更高效。
  • 缺点:最后一次持久化的数据可能丢失

。。。。。。

2、AOP

。。。。。。。。。。

报错解决

1、Redis—-(error) MISCONF

1
2
3
4
5
6
7
8
9
10
11
12
错误:Redis----(error) MISCONF Redis is configured to save RDB snapshots


解决方案:

直接修改redis.conf配置文件,但是更改后需要重启redis。
修改redis.conf文件:

(1)vim打开redis-server配置的redis.conf文件,
(2)使用快捷匹配模式:
/stop-writes-on-bgsave-error定位到stop-writes-on-bgsave-error字符串所在位置,
(3)把后面的yes设置为no。

2、解决SpringSecurity的自定义认证过滤器中无法注入 RedisTemplate的问题

解决SpringBoot项目,过滤器中注入redisTemplate(方案一)

1
2
3
4
5
6
7
8
9
10
11
@Component
public class RedisBean {
@Autowired
private RedisTemplate redisTemplate;

public static RedisTemplate redis;
@PostConstruct
public void getRedisTemplate(){
redis=this.redisTemplate;
}
}