JAVA入门基础_Redis
Redis 能够为我们解决什么问题
-
减轻 CPU 和内存压力
-
减轻IO 压力
-
访问 redis 数据库是直接从内存中读取数据,比直接进行 IO 读取速度要快的多
-
适用场景
- 对数据高并发的读写
- 海量数据的读写
- 对数据高扩展的
-
不适用场景
- 需要事物支持的
- 处理复杂的关系需要即时查询的
-
用不着 SQL 和用了 SQL 也解决不了的问题时可以考虑使用 NOSQL 的数据库,例如 Redis
Redis 的下载与安装
-
将
redis-6.2.1.tar.gz
压缩包放在 Linux 系统的/opt
目录下,并完成解压 -
需要安装 gcc 环境
yum install gcc
-
进入到解压目录,使用
make
命令进行编译,编译完成后使用make install
命令安装- 如果中途出现了 —Jemalloc/jemalloc.h:没有那个文件 问题,可以 make distclean
- 尝试安装 gcc 后再次尝试
-
不出问题的话就已经安装好了,其安装目录在:
/usr/local/bin
前台启动(不推荐)与后台启动
-
前台启动,就是直接进入 /usr/local/bin 下执行 redis-server 即可(不推荐)
-
复制一个
/opt/redis解压目录/下的一个redis.conf
文件到/etc目录
下 -
修改 /etc/redis.conf 配置文件,将
daemonize no
的参数设置为yes
,大概 250 行左右 -
进入到
/usr/local/bin
目录下,执行redis-server /etc/redis.conf
即可完成后台启动
常用五大数据类型
Redis 键常用命令 (key)
命令 | 作用 | 示例 |
---|---|---|
keys * | 查看所有的 key | keys * |
exists key | 判断某个 key 是否存在 | exists k1 |
type key | 查看某个 key 的类型 | type k1 |
del key | 删除某个 key | del k1 |
unlink key | 删除某个 key,但是会调用异步线程 | unlink k2 |
expire key second | 设置某个 key 的过期时间 | expire k1 10 |
ttl key | 查看某个 key 的过期时间,-1 用不过期,-2 已过期 | ttl k1 |
4 个数据库操作命令
命令 | 作用 | 示例 |
---|---|---|
select 数据库编号 | 切换到指定的数据库默认为 0 号数据库 | select 10 |
dbsize | 查看当前数据库有多少个 key | dbsize |
flushdb | 清空当前数据库 | flushdb |
flushall | 清空所有数据库 | flushall |
String 字符串命令
命令 | 作用 | 示例 |
---|---|---|
set key value | 设置键值 | set k1 v1 |
get key | 获取某个 key | get k1 |
setnx key value | 设置键值、如果 key 已存在则设置失败 | setnx k1 v1 |
mset key1 value1 key2 value2 | 批量设置键值 | mset k3 v3 k4 v4 k5 v5 |
msetnx | 批量设置键值,任意一个 key 已存在则全部设置失败 | msetnx k8 v8 k1 v1 |
setex key second value | 设置键值并指定过期时间,单位为秒 | setex k1 10 v1 |
append key value | 在原有字符串中追加 value | append k1 nihao |
setrange key startIndex value | 在字符串指定位置设置 value,会覆盖原有字符串的范围内容 | setrange k1 1 abc |
getrange key startIndex endIndex | 获取指定范围内的 value(包含头和尾) | getrange k1 1 3 |
getset key value | 获取原有的值,并设置新的值 | getset k1 zhangsan |
strlen key | 获取值得长度 | strlen k1 |
incr key | 将 key 中存储到数字 +1 | incr k1 |
incr by key 步长 | 将 key 中存储到数字加步长 | incrby k1 10 |
decr key | 将 key 中存储到数字 -1 | decr k2 |
decr by key 步长 | 将 key 中存储到数字减步长 | decrby k1 10 |
String 的内存结构
- SDS(Simple Dynamic String)简单动态字符串,结构上类似于 Jva 的 ArrayList,预分配一些内存空间,避免频繁扩容
List 类型命令 (单键多值)
命令 | 作用 | 示例 |
---|---|---|
lpush key v1 v2 v3 | 从左边插入一个或多个值 | lpush k1 a b c d e |
rpush key v1 v2 v3 | 从右边插入一个或多个值 | rpush k2 a b c d e |
lpop key n | 从左边取出 n 个值并删除 | lpop k1 2 |
rpop key n | 从右边取出 n 个值并删除 | rpop k1 2 |
rpoplpush source dest | 从一个列表的右边取出一个值添加到另一个列表的左边 | rpoplpush k1 k2 |
lrange key 0 -1 | 按照索引下标获得元素,0,-1 代表获取全部 | lrange k2 0 -1 |
lindex key index | 按照索引下标获取元素,索引从 0 开始 | lindex k2 1 |
llen | 获得列表长度 | llen k2 |
linsert key before value newValue | 在某个 value 值的前或后添加一个元素 (若有多个,则插入到从左边开始找到的第一个) | linsert k2 before c zhangsan |
lrem key n value | 从左边删除 n 个相同的元素 value | lrem k2 2 value3 |
lset key index value | 将列表中索引位置的值替换成指定的值 | lset k2 0 wangwu |
List 列表类型数据结构
Set 集合命令(member 指的是 Set 集合 key 中的元素)
命令 | 作用 | 示例 |
---|---|---|
sadd key value1 value2 valve3 | 将一个或多个 member 值添加到集合当中 | sadd k1 v1 v2 v3 |
smembers key | 取出一个集合的所有值 | smembers k1 |
sismember key value | 判断集合 key 中是否还有某个 value 值,0 为没有,1 为有 | sismember k1 v1 |
scard key | 返回该集合的元素个数 | scard k1 |
srem key value1 value2 | 删除集合中的某些元素 | scard k1 |
spop key n | 随机从该集合中取出 n 个值并删除 | spop k1 2 |
srandmember key n | 随机从集合中取出多个值但不删除 | srandmember k1 2 |
smove key1 key2 value | 把集合中一个值移动到另一个集合中 | smove k1 k2 v3 |
sinter key1 key2 | 返回 2 个集合的交集 | sinter k1 k2 |
sunion key1 key2 | 返回 2 个集合的并集 | sunion k1 k2 |
sdiff key1 key2 | 返回 2 个集合的差集 | sdiff k1 k2 |
Set 集合数据结构
- 底层其实是一个 value 为 null(内部值)的 hash 表,所以增删改的时间复杂度为 O(1)
hash 数据类型常用命令
命令 | 作用 | 示例 |
---|---|---|
hset key fieldname fieldValue | 将一个或多个值添加到 hash 当中 | hset k1 name zhangsan age 18 |
hsetnx key fieldname fieldValue | 将一个值添加到 hash 当中,如果 field 已存在则添加失败 | hsetnx k1 birthday 2000-10-10 |
hget key fieldname | 取出 hash 中的一个元素,通过字段名 | hget k1 name |
hexist key fieldname | 查看 hash 中,指定字段是否存在 | hexists k1 age |
hkeys key | 列出 hash 中所有的 fieldname | hkeys k1 |
hvals key | 列出 hash 中所有的 fieldvalue | hvals k1 |
hincrby key field increment | 为 hash 中的某个字段进行数值增加或减少 | hincrby k1 age -5 |
hash 数据结构
-
field-name 的长度较短并且字段较少时,使用 ziplist
-
否则使用 hashtable,也就是哈希表
-
其内存结构跟 Java 中的 HashMap 差不多
Zset 常用命令(带分数排序的 Set 集合)
命令 | 作用 | 示例 |
---|---|---|
zadd key score1 value1 score2 value2 | 将一个或多个 member 值添加到 zset 集合中 | zadd k1 100 java 200 c++ 300 c# |
zrange key 0 -1 [withscores] | 返回下标在 start_end 之间的元素(默认不包括分数) | zrange k1 0 1 withscores |
zrangebyscore key min max [withscores] | 返回分数在 min ~ max 之间的元素 | zrangebyscore k1 200 300 withscores |
zrevrangebyscore key max min [withscores] | 同上,但是需要降序排列,而且是 max~min | zrevrangebyscore k1 300 200 withscores |
zincrby key increment value | 为元素的 score 加分或减分 | zincrby k1 -300 java |
zrem key value | 删除该集合下指定值的一个或多个元素 | zrem k1 c++ c# |
zcount key min max | 统计该集合分数区间内的元素个数 | zcount k1 100 300 |
zrand key value | 返回一个元素在集合中的排名,从 0 开始 | zrank k1 java |
zrandmember key n | 返回 zset 中指定个数的元素(按照排名) | zrandmember k1 2 |
Zset 数据结构
-
底层首先是一个 hash 表 (Map<String,Double>)
-
并且还存在跳跃表
-
生成的文件与命令中:运行命令的路径有关
Redis 配置文件介绍
###Units###
- 配置单位大小,开头定义了一些基本的度量单位,只支持bytes,并且大小写不敏感,不支持 bit(一个字节 8 位)
# 1k => 1000 bytes
# 1kb => 1024 bytes
# 1m => 1000000 bytes
# 1mb => 1024*1024 bytes
# 1g => 1000000000 bytes
# 1gb => 1024*1024*1024 bytes
###INCLUDES###
- 包含,类似于 JSP 或者 Thymeleaf 中的 include。例如可以包含一些配置文件
# include /path/to/local.conf
# include /path/to/other.conf
###NETWORK###
- 网络配置,其中常用的几个配置如下(如下配置均为默认,暂未修改)
# 标志当前可以访问的 IP 地址,如果想要 IP 都能访问,可以直接将其注释掉
bind 127.0.0.1 -::1
# 保护模式,如果开启了,那么在没有bind,redis 连接也没有密码时,redis 将会只接收本机的响应
protected-mode yes
# 服务的端口号
port 6379
# 是完成了 TCP 三次握手以及未完成 TCP 三次握手的连接队列
# Linux 内核会将这个值减少到 vim /proc/sys/net/core/somaxconn 的值 (128)
# 如果真的需要增加连接队列数量,则需要修改 vim /proc/sys/net/core/somaxconn
# 和 /proc/sys/net/ipv4/tcp_max_syn_backlog
tcp-backlog 511
# 超时时间,如果客户端连接到 redis 超过这个时间没有进行过任何操作(空闲时间),
# 则中断连接,0 表示永不超时
timeout 0
# 对访问客户端的心跳检测,单位为秒,(判断客户端是否存活),建议设置为 60
tcp-keepalive 300
###GENERAL###
- 一些通用的配置
# 是否以后台运行 redis
daemonize yes
# 记录当前 redis 启动的线程 ID,只会存储当前运行 redis 服务的线程 ID
# 如果 redis 服务关闭,会将该文件删除
pidfile /var/run/redis_6379.pid
# 日志级别,debug -> verbose -> notice -> warning
loglevel notice
# 日志文件的名称
logfile ""
# 数据库个数,从 0 开始
databases 16
###SECURITY###
- 与安全相关的配置,设置密码(永久设置)
# 设置当前 redis 的密码
requirepass foobared
-
设置密码后需要授权才能操作 redis 数据库
-
临时设置密码
# 查看当前密码
config get requirepass
# 设置密码
config set requirepass "123456"
# 授权
auth 123456
###CLIENTS###
- 最大的客户端连接数量,如果超过了此数量,会返回:max number of clients reached(已达到最大连接数)
maxclients 10000
###MEMORY MANAGEMENT###
- 内存管理常用配置
# 设置 Redis 可以使用的内存容量,建议** 必须设置 **,** 否则内存占满后将会造成服务器宕机 **
maxmemory <bytes>
# 达到最大内存容量的移除 key 策略。
# volatile-lru:使用 LRU 算法移除 key,只对设置了过期时间的键;(最近最少使用)
# allkeys-lru:在所有集合 key 中,使用 LRU 算法移除 key
# volatile-random:在过期集合中移除随机的 key,只对设置了过期时间的键
# allkeys-random:在所有集合 key 中,移除随机的 key
# volatile-ttl:移除那些 TTL 值最小的 key,即那些最近要过期的 key
# noeviction:不进行移除。针对写操作,只是返回错误信息
maxmemory-policy noeviction
# 设置样本数量,LRU 算法和最小 TTL 算法都并非是精确的数量
# 一般设置 3 到 7 的数字,数值越小样本越不精确,但性能消耗越小
maxmemory-samples 5
Redis 的订阅与发布
什么是订阅与发布(频道)
-
想想生活中的例子,我们订阅了一个频道,那么这个频道有消息的时候就会通知到我们
-
其实程序中的订阅与发布也是如此。
-
需要接收到消息的一方 (订阅者)订阅某个通道,发送消息 (发布者) 的一方就通过这个通道来发送消息,因此订阅者就可以接收到消息
开启一个 Redis 客户端,成为订阅者订阅一个或多个频道
# 连接 redis
redis-cli
# 订阅多个频道
subscribe channel1 channel2
开启一个 Redis 客户端,成为发布者,在某个频道发布消息
publish channel1 helloredis
订阅者收到消息
Redis 的新数据类型
Bitmaps
-
作用:统计用户活跃量
-
setbit
-
getbit
-
bitcount
-
bitop
HyperLogLog
-
作用:基数统计,例如独立访客,不允许重复
-
pfadd
-
pfcount
-
pfmerge
Geospatial
-
作用:统计经纬度,还能计算距离
-
geoadd 添加
-
geopos 获取指定地区的坐标值
-
geodist 获取直线距离
-
georadius 在距离范围内的
Redis_Jedis 连接 Redis 进行操作
修改 redis.conf 并且开放 Linux 的端口号
- 修改 redis.conf
# 注释掉bind
#bind 127.0.0.1 -::1
# 关闭保护模式
protected-mode no
- 开放端口 6379
# 永久开放端口 6379
firewall-cmd --permanent --add-port=6379/tcp
# 重启防火墙
systemctl restart firewalld.service
- 可以使用 telnet ip 地址 端口 来测试是否可以连接上
telnet 192.168.22.100 6379
创建一个 Maven 工程,引入 Jedis 的依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.2.0</version>
</dependency>
简单测试一下,其实方法跟命令行的差不多
public class JedisTest {
public static void main(String[] args) {
Jedis jedis = new Jedis("192.168.22.100", 6379);
<span class="hljs-comment">// 1、操作string的常用方法</span>
jedis.<span class="hljs-keyword">set</span>(<span class="hljs-string">"k1"</span>,<span class="hljs-string">"v1"</span>);
jedis.<span class="hljs-keyword">get</span>(<span class="hljs-string">"k1"</span>);
jedis.setnx(<span class="hljs-string">"k1"</span>,<span class="hljs-string">"v1"</span>);
jedis.mset(<span class="hljs-string">"k1"</span>,<span class="hljs-string">"v1"</span>,<span class="hljs-string">"k2"</span>,<span class="hljs-string">"v2"</span>);
jedis.msetnx(<span class="hljs-string">"k1"</span>,<span class="hljs-string">"v1"</span>,<span class="hljs-string">"k2"</span>,<span class="hljs-string">"v2"</span>);
jedis.setex(<span class="hljs-string">"k1"</span>, <span class="hljs-number">3</span>, <span class="hljs-string">"v1"</span>);
jedis.append(<span class="hljs-string">"k1"</span>, <span class="hljs-string">"111"</span>);
jedis.setrange(<span class="hljs-string">"k1"</span>, <span class="hljs-number">1</span>, <span class="hljs-string">"zhang"</span>);
jedis.getrange(<span class="hljs-string">"k1"</span>, <span class="hljs-number">0</span> , <span class="hljs-number">3</span>);
jedis.getSet(<span class="hljs-string">"k1"</span>,<span class="hljs-string">"v1"</span>);
jedis.strlen(<span class="hljs-string">"k1"</span>);
jedis.incr(<span class="hljs-string">"k1"</span>);
jedis.incrBy(<span class="hljs-string">"k1"</span>, <span class="hljs-number">3</span>);
jedis.decr(<span class="hljs-string">"k1"</span>);
jedis.decrBy(<span class="hljs-string">"k1"</span>, <span class="hljs-number">3</span>);
<span class="hljs-comment">// 2、操作List、Set、Hash、Zset的方式均与命令行敲命令时一致</span>
<span class="hljs-comment">// 3、关闭连接</span>
jedis.close();
}
}
练习使用 Jedis 完成一个手机验证码功能
-
模拟如下功能
1、输入手机号,点击发送后随机生成 6 位数字码,2 分钟有效
2、输入验证码,点击验证,返回成功或失败
3、每个手机号每天只能输入 3 次 -
思路:
-
首先方法中都必须做的事情
- (1)定义好手机号、验证码所对应的 key 名
-
发送验证码的思路(接收手机号)
- (1)判断当前手机号是否已经在 redis 中存在
- (1.1)如果不存在,则进行存储,将其数量设置为 1,并设置过期时间为次日凌晨,也就是24 小时减当前时间
- (1.2)如果已存在,判断是否小于 3,如果满足则代表发送验证码没有超过 3 次,为其发送验证码,否则告知每日发送验证码的次数不能超过 3 次
- (2)获取验证码
- (3)将验证码存储到 Redis 当中并设置超时时间为 2 分钟
- (1)判断当前手机号是否已经在 redis 中存在
-
检验验证码的思路(接收手机号、验证码)
- (1)根据当前的手机号、验证码拼接到对应的 key
- (2)通过对应的 key 去 Redis 中获取数据并进行响应判断即可
-
定义发送验证码的方法
- (1)6 次 for 循环
- (2)每次都拼接一个 1~9 的随机数即可
- (3)随机数由 Random 类生成
-
-
实际编码
public class RedisCode {
public static void main(String[] args) throws Exception{
// 发送验证码
sendCode("15577778888");
<span class="hljs-comment">// 校验验证码</span>
<span class="hljs-type">boolean</span> <span class="hljs-variable">b</span> <span class="hljs-operator">=</span> verifyCode(<span class="hljs-string">"15577778888"</span>, <span class="hljs-string">"735315"</span>);
System.out.println(b);
}
<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-type">boolean</span> <span class="hljs-title function_">verifyCode</span><span class="hljs-params">(String phone, String code)</span> {
<span class="hljs-type">Jedis</span> <span class="hljs-variable">jedis</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Jedis</span>(<span class="hljs-string">"192.168.22.100"</span>, <span class="hljs-number">6379</span>);
<span class="hljs-comment">// 1、定义手机号对应的验证码的key</span>
<span class="hljs-type">String</span> <span class="hljs-variable">codeKey</span> <span class="hljs-operator">=</span> <span class="hljs-string">"Verify:"</span> + phone + <span class="hljs-string">":code"</span>;
<span class="hljs-comment">// 2、从redis中获取验证码</span>
<span class="hljs-type">String</span> <span class="hljs-variable">redisCode</span> <span class="hljs-operator">=</span> jedis.get(codeKey);
<span class="hljs-comment">// 3、校验验证码是否已经失效</span>
<span class="hljs-keyword">if</span> (redisCode == <span class="hljs-literal">null</span> || <span class="hljs-string">""</span>.equals(redisCode)) {
System.out.println(<span class="hljs-string">"当前验证码失效,请重新获取"</span>);
jedis.close();
<span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;
}
<span class="hljs-comment">// 4、进行验证码的校验</span>
<span class="hljs-keyword">if</span>(redisCode.equals(code)) {
System.out.println(<span class="hljs-string">"验证码校验成功"</span>);
jedis.close();
<span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
}
<span class="hljs-comment">// 5、都走到这了,说明没有校验成功</span>
System.out.println(<span class="hljs-string">"验证码校验失败"</span>);
jedis.close();
<span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;
}
<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">sendCode</span><span class="hljs-params">(String phone)</span> {
<span class="hljs-type">Jedis</span> <span class="hljs-variable">jedis</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Jedis</span>(<span class="hljs-string">"192.168.22.100"</span>, <span class="hljs-number">6379</span>);
<span class="hljs-comment">// 1、定义手机号对应的key</span>
<span class="hljs-type">String</span> <span class="hljs-variable">phoneKey</span> <span class="hljs-operator">=</span> <span class="hljs-string">"Verify:"</span> + phone + <span class="hljs-string">":qt"</span>;
<span class="hljs-type">String</span> <span class="hljs-variable">codeKey</span> <span class="hljs-operator">=</span> <span class="hljs-string">"Verify:"</span> + phone + <span class="hljs-string">":code"</span>;
<span class="hljs-comment">// 2、去Redis中查询是否含有该key对应的value</span>
<span class="hljs-type">String</span> <span class="hljs-variable">phoneValue</span> <span class="hljs-operator">=</span> jedis.get(phoneKey);
<span class="hljs-comment">// 3、校验是否为null</span>
<span class="hljs-keyword">if</span>(phoneValue == <span class="hljs-literal">null</span>) {
<span class="hljs-comment">// 3.1 在redis中设置值,明天凌晨重置</span>
<span class="hljs-comment">// 获取到当前的时间戳</span>
<span class="hljs-type">long</span> <span class="hljs-variable">nowTimeStamp</span> <span class="hljs-operator">=</span> Instant.now().toEpochMilli();
<span class="hljs-comment">// 获取到明天凌晨的时间戳</span>
<span class="hljs-type">long</span> <span class="hljs-variable">tomorrowTimeStamp</span> <span class="hljs-operator">=</span> LocalDateTime.now().plusDays(<span class="hljs-number">1</span>).withHour(<span class="hljs-number">0</span>).withMinute(<span class="hljs-number">0</span>).withSecond(<span class="hljs-number">0</span>).withNano(<span class="hljs-number">0</span>)
.toInstant(ZoneOffset.of(<span class="hljs-string">"+8"</span>)).toEpochMilli();
<span class="hljs-comment">// 计算获得现在到明天凌晨的秒数</span>
<span class="hljs-type">int</span> <span class="hljs-variable">second</span> <span class="hljs-operator">=</span> (<span class="hljs-type">int</span>) ((tomorrowTimeStamp - nowTimeStamp) / <span class="hljs-number">1000</span>);
<span class="hljs-comment">// 往redis当中存储数据</span>
jedis.setex(phoneKey,second,<span class="hljs-string">"1"</span>);
}<span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (Integer.parseInt(phoneValue) < <span class="hljs-number">3</span>) {
<span class="hljs-comment">// 3.2 这个时候代表今天已经给他发送过验证码了,并且次数没有达到3次,次数加一</span>
jedis.incr(phoneKey);
}<span class="hljs-keyword">else</span> {
<span class="hljs-comment">// 3.3 说明次数已经达到3次了</span>
System.out.println(<span class="hljs-string">"今日发送验证码的次数已经达到3次,明天再来吧"</span>);
jedis.close();
<span class="hljs-keyword">return</span>;
}
<span class="hljs-comment">// 4、发送验证码,假装已经发送了</span>
<span class="hljs-type">String</span> <span class="hljs-variable">code</span> <span class="hljs-operator">=</span> getCode();
System.out.println(<span class="hljs-string">"当前验证码是:"</span> + code);
<span class="hljs-comment">// 5、存储到redis中,设置过期时间为2分钟</span>
jedis.setex(codeKey, <span class="hljs-number">60</span> * <span class="hljs-number">2</span> ,code);
jedis.close();
}
<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> String <span class="hljs-title function_">getCode</span><span class="hljs-params">()</span> {
<span class="hljs-type">StringBuilder</span> <span class="hljs-variable">sb</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">StringBuilder</span>();
<span class="hljs-type">Random</span> <span class="hljs-variable">random</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Random</span>();
<span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> <span class="hljs-variable">i</span> <span class="hljs-operator">=</span> <span class="hljs-number">0</span>; i < <span class="hljs-number">6</span>; i++) {
sb.append(random.nextInt(<span class="hljs-number">10</span>));
}
<span class="hljs-keyword">return</span> sb.toString();
}
}
Redis 整合 SpringBoot
创建一个 SpringBoot 工程并引入 redis 启动器和所需的 pool2
<!-- 引入一个 web 模块,用于测试 RedisTemplate -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<span class="hljs-comment"><!-- Redis启动器 --></span>
<span class="hljs-tag"><<span class="hljs-name">dependency</span>></span>
<span class="hljs-tag"><<span class="hljs-name">groupId</span>></span>org.springframework.boot<span class="hljs-tag"></<span class="hljs-name">groupId</span>></span>
<span class="hljs-tag"><<span class="hljs-name">artifactId</span>></span>spring-boot-starter-data-redis<span class="hljs-tag"></<span class="hljs-name">artifactId</span>></span>
<span class="hljs-tag"></<span class="hljs-name">dependency</span>></span>
<span class="hljs-comment"><!-- spring2.X集成redis所需common-pool2--></span>
<span class="hljs-tag"><<span class="hljs-name">dependency</span>></span>
<span class="hljs-tag"><<span class="hljs-name">groupId</span>></span>org.apache.commons<span class="hljs-tag"></<span class="hljs-name">groupId</span>></span>
<span class="hljs-tag"><<span class="hljs-name">artifactId</span>></span>commons-pool2<span class="hljs-tag"></<span class="hljs-name">artifactId</span>></span>
<span class="hljs-tag"></<span class="hljs-name">dependency</span>></span>
修改配置文件
#Redis 服务器地址
spring.redis.host=192.168.22.100
#Redis 服务器连接端口
spring.redis.port=6379
#Redis 数据库索引(默认为 0)
spring.redis.database= 0
#连接超时时间(毫秒),这里是 30 分钟
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
添加 Redis 的配置类(自动配置的不够我们用,所以需要自行扩展一下)
@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.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setConnectionFactory(factory);
//key 序列化方式
template.setKeySerializer(redisSerializer);
//value 序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
//value hashmap 序列化
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}
<span class="hljs-meta">@Bean</span>
public CacheManager cacheManager(RedisConnectionFactory <span class="hljs-keyword">factory</span>) {
RedisSerializer<<span class="hljs-built_in">String</span>> redisSerializer = <span class="hljs-keyword">new</span> StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = <span class="hljs-keyword">new</span> Jackson2JsonRedisSerializer(<span class="hljs-built_in">Object</span>.<span class="hljs-keyword">class</span>);
<span class="hljs-comment">//解决查询缓存转换异常的问题</span>
ObjectMapper om = <span class="hljs-keyword">new</span> ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
<span class="hljs-comment">// 配置序列化(解决乱码的问题),过期时间600秒</span>
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(<span class="hljs-built_in">Duration</span>.ofSeconds(<span class="hljs-number">600</span>))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(<span class="hljs-keyword">factory</span>)
.cacheDefaults(config)
.build();
<span class="hljs-keyword">return</span> cacheManager;
}
}
使用示例
@RestController
public class RedisTestController {
@Autowired
private RedisTemplate redisTemplate;
<span class="hljs-meta">@GetMapping("/testRedis")</span>
<span class="hljs-keyword">public</span> Object <span class="hljs-title function_">testRedis</span><span class="hljs-params">()</span> {
<span class="hljs-comment">// 1、string的操作类</span>
<span class="hljs-type">ValueOperations</span> <span class="hljs-variable">strOperations</span> <span class="hljs-operator">=</span> redisTemplate.opsForValue();
<span class="hljs-comment">// 2、list的操作类</span>
<span class="hljs-type">ListOperations</span> <span class="hljs-variable">listOperations</span> <span class="hljs-operator">=</span> redisTemplate.opsForList();
<span class="hljs-comment">// 3、Set的操作类</span>
<span class="hljs-type">SetOperations</span> <span class="hljs-variable">boundSetOperations</span> <span class="hljs-operator">=</span> redisTemplate.opsForSet();
<span class="hljs-comment">// 4、hash的操作类</span>
<span class="hljs-type">HashOperations</span> <span class="hljs-variable">hashOperations</span> <span class="hljs-operator">=</span> redisTemplate.opsForHash();
<span class="hljs-comment">// 5、zset的操作类</span>
<span class="hljs-type">ZSetOperations</span> <span class="hljs-variable">zSetOperations</span> <span class="hljs-operator">=</span> redisTemplate.opsForZSet();
<span class="hljs-keyword">return</span> <span class="hljs-string">"hello spring-boot-starter-redis"</span>;
}
}
Redis 的事物
Redis 事物的定义
-
Redis 的事物并不支持 ACID
-
Redis 事物是一个单独的隔离操作:事物中所有的命令都会序列化、按顺序的执行。事物在执行的过程中,不会被其他客户端发送来的请求所打断。
-
Redis 中事物的主要作用就是串联多个命令防止别的命令插队。
Multi、Exec、Discard
-
在执行了Multi 命令之后,输入的命令都会被加入到一个队列(组)当中,但是不会执行。(组队阶段)
-
当执行 Exec 命令后,队列中的命令将会按照顺序执行,执行的过程中不会被其他客户端的请求所打断。因为这是一个单独的隔离操作。(执行阶段)
-
在 Multi 命令的中途如果想要放弃当前的队列,则可以执行 discard命令。
组队阶段与执行阶段出现错误时
-
当组队阶段出现了错误时,那么只要执行了 exec 进入执行阶段时,所有组队的命令都不会被执行。
-
当执行阶段出现错误时:指令依然会按照顺序执行,成功的就成功,失败的就失败,并没有原子性。
Redis 对于事物冲突的解决方案(乐观锁)
-
Redis 中使用 watch 来监视某个 key,依次来达到乐观锁的效果,在修改被监视的 key 后,key 的版本号会发生改变,因此当再次修改时,若没有获取到最新的数据,则会导致更新失败。
-
watch 的监视需要执行在 multi 之前
使用示例(2 台客户端)
- 客户端 1 号
# 开启乐观锁监视 k1
watch k1
# 开启事物
multi
- 客户端 2 号
# 开启乐观锁监视 k1
watch k1
# 开启事物
multi
- 客户端 1 号修改了 k1 的值并执行
# 修改 k1 的值
set k1 clientOne
# 执行
exec
- 客户端 2 号也尝试修改 k1 的值并执行
set k1 clientTwo
exec
# 返回的结果,表示修改失败
(nil)
Watch 配合 Redis 事物的总结
-
当一个客户端使用 wath 开始了 key 的监视(可以监视 1 个或多个 key)后
-
那么当前客户端开启的事物,只要在最后 exec 执行之前
-
unwatch: 取消对 key 的监控。(如果已经执行了 exec 或 discard 后则会自动取消)
-
发现watch 监视的任何一个 key 发生了变化后,则会导致当前的事物失效(所有的指令全都无法执行)
Redis 事物的三特性
-
单独的隔离操作
- 当执行了 exec 后,将会把队列中的指令序列化,按照顺序的执行这些指令,在此期间不会被其他客户端的请求打断。
-
没有隔离级别的概念
- 队列中的命令在没有进行 exec 之前都不会被执行,只是放在队列当中。
-
不保证原子性
- 事物中如果有一条命令执行失败,并不会导致其他命令回滚
Redis 事物秒杀案例
-
要求
- 同一个用户最多只能秒杀成功一次
-
实现思路(库存使用 string 存,秒杀成功的人数使用 set 存储)
- (1)接收到客户端发来的用户名与商品 ID
- (2)根据商品 ID 去 Redis 中查询是否为 null
- (2.1)如果为 null 代表秒杀还没有开始
- (2.2)如果不为 null,则判断当前的用户是否已经秒杀过了,若已经秒杀过则直接提示后结束当前方法
- (3)根据第二步查询到的商品库存判断当前库存容量
- (3.1)如果库存容量够,则秒杀成功的列表中加入当前用户 ID,然后库存 -1 ,告知秒杀成功
- (3.2)如果库存不够,则提示秒杀已结束
-
编码实现
@PostMapping("/secKill")
public void secKill(String productId) {
Jedis jedis = new Jedis("192.168.22.100", 6379);
<span class="hljs-comment">// 模拟不同的用户,随机4位用户ID</span>
<span class="hljs-title class_">String</span> userId = <span class="hljs-title function_">getRandomUserId</span>();
<span class="hljs-comment">// 1、拼接当前的key</span>
<span class="hljs-title class_">String</span> productCountKey = <span class="hljs-string">"sk:"</span> + productId + <span class="hljs-string">":qt"</span>;
<span class="hljs-title class_">String</span> successSetKey = <span class="hljs-string">"sk:"</span> + productId + <span class="hljs-string">":user"</span>;
<span class="hljs-comment">// 2、判断当前是否开始了秒杀</span>
<span class="hljs-title class_">Integer</span> productCount = <span class="hljs-title class_">Integer</span>.<span class="hljs-built_in">parseInt</span>(jedis.<span class="hljs-title function_">get</span>(productCountKey));
<span class="hljs-comment">// 2.1 判断当前秒杀是否开始</span>
<span class="hljs-keyword">if</span> (productCount == <span class="hljs-literal">null</span>) {
<span class="hljs-title class_">System</span>.<span class="hljs-property">out</span>.<span class="hljs-title function_">println</span>(<span class="hljs-keyword">new</span> <span class="hljs-title class_">Result</span>(<span class="hljs-literal">false</span>, <span class="hljs-string">"秒杀还未开始"</span>));
<span class="hljs-keyword">return</span>;
}
<span class="hljs-comment">// 3、判断当前用户是否已经秒杀过了</span>
<span class="hljs-keyword">if</span> (jedis.<span class="hljs-title function_">sismember</span>(successSetKey, userId)) {
<span class="hljs-title class_">System</span>.<span class="hljs-property">out</span>.<span class="hljs-title function_">println</span>(<span class="hljs-keyword">new</span> <span class="hljs-title class_">Result</span>(<span class="hljs-literal">false</span>, <span class="hljs-string">"您已经秒杀成功过了,不能再秒杀了"</span>));
<span class="hljs-keyword">return</span>;
}
<span class="hljs-comment">// 4、判断当前库存是否已经没有了</span>
<span class="hljs-keyword">if</span>(productCount <= <span class="hljs-number">0</span>) {
<span class="hljs-title class_">System</span>.<span class="hljs-property">out</span>.<span class="hljs-title function_">println</span>(<span class="hljs-keyword">new</span> <span class="hljs-title class_">Result</span>(<span class="hljs-literal">false</span>, <span class="hljs-string">"非常抱歉,秒杀已经结束了"</span>));
<span class="hljs-keyword">return</span>;
}
<span class="hljs-comment">// 5、库存 - 1,秒杀成功的用户列表加上当前用户</span>
jedis.<span class="hljs-title function_">decr</span>(productCountKey);
jedis.<span class="hljs-title function_">sadd</span>(successSetKey, userId);
jedis.<span class="hljs-title function_">close</span>();
<span class="hljs-title class_">System</span>.<span class="hljs-property">out</span>.<span class="hljs-title function_">println</span>(<span class="hljs-keyword">new</span> <span class="hljs-title class_">Result</span>(<span class="hljs-literal">true</span>, <span class="hljs-string">"恭喜你秒杀成功"</span>));
}
<span class="hljs-keyword">private</span> <span class="hljs-title class_">String</span> <span class="hljs-title function_">getRandomUserId</span>(<span class="hljs-params"></span>) {
<span class="hljs-title class_">StringBuilder</span> sb = <span class="hljs-keyword">new</span> <span class="hljs-title class_">StringBuilder</span>();
<span class="hljs-keyword">for</span> (int i = <span class="hljs-number">0</span>; i < <span class="hljs-number">4</span>; i++) {
sb.<span class="hljs-title function_">append</span>(<span class="hljs-keyword">new</span> <span class="hljs-title class_">Random</span>().<span class="hljs-title function_">nextInt</span>(<span class="hljs-number">10</span>));
}
<span class="hljs-keyword">return</span> sb.<span class="hljs-title function_">toString</span>();
}
Linux 系统中安装压力测试工具 httpd-tools
yum install httpd-tools
ab 命令的使用示例
-
在任意目录创建一个需要传递的参数文件: vim /opt/postfile
-
修改其中的内容,放上需要传递的参数,以 & 结尾
productId=1010&
- 输入如下指令完成压力测试
# 2000 个线程,存在 200 个并发。注意 ip 地址和端口号别写错了
ab -n 2000 -c 200 -k -p /opt/postfile -T application/x-www-form-urlencoded http://192.168.31.71:8080/secKill
如上编码出现的问题
连接超时问题
-
采用连接池,之后用连接池来获取 Jedis
-
连接池编码
public class JedisPoolUtils {
private volatile static JedisPool jedisPool = null;
<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> JedisPool <span class="hljs-title function_">getInstance</span><span class="hljs-params">()</span> {
<span class="hljs-keyword">if</span>(jedisPool == <span class="hljs-literal">null</span>) {
<span class="hljs-keyword">synchronized</span> (JedisPoolUtils.class) {
<span class="hljs-keyword">if</span>(jedisPool == <span class="hljs-literal">null</span>) {
<span class="hljs-type">JedisPoolConfig</span> <span class="hljs-variable">jedisPoolConfig</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">JedisPoolConfig</span>();
<span class="hljs-comment">// 最大连接</span>
jedisPoolConfig.setMaxTotal(<span class="hljs-number">200</span>);
<span class="hljs-comment">// 最大空闲</span>
jedisPoolConfig.setMaxIdle(<span class="hljs-number">32</span>);
<span class="hljs-comment">// 最大等待时间</span>
jedisPoolConfig.setMaxWaitMillis(<span class="hljs-number">100</span> * <span class="hljs-number">1000</span>);
<span class="hljs-comment">// 表示当pool中的jedis 实例都被分配完时,是否要进行阻塞</span>
jedisPoolConfig.setBlockWhenExhausted(<span class="hljs-literal">true</span>);
<span class="hljs-comment">// 每次获取连接时候都要到数据库验证连接有效性</span>
jedisPoolConfig.setTestOnBorrow(<span class="hljs-literal">true</span>);
jedisPool = <span class="hljs-keyword">new</span> <span class="hljs-title class_">JedisPool</span>(jedisPoolConfig, <span class="hljs-string">"192.168.22.100"</span>);
}
}
}
<span class="hljs-keyword">return</span> jedisPool;
}
}
超卖问题
-
原因:由于整个流程判断,以及对数据的操作都是多线程的,会导致多个线程同时判断到了库存大于 0 而往下执行了,这多个线程往往超出实际的库存量,因此出现超卖问题
-
解决思路:使用 Redis 的乐观锁机制来解决事物问题
-
修改后的代码
@PostMapping("/secKill2")
public void secKillNew(String productId) {
Jedis jedis = new Jedis("192.168.22.100", 6379);
<span class="hljs-comment">// 模拟不同的用户,随机4位用户ID</span>
<span class="hljs-title class_">String</span> userId = <span class="hljs-title function_">getRandomUserId</span>();
<span class="hljs-comment">// 1、拼接当前的key</span>
<span class="hljs-title class_">String</span> productCountKey = <span class="hljs-string">"sk:"</span> + productId + <span class="hljs-string">":qt"</span>;
<span class="hljs-title class_">String</span> successSetKey = <span class="hljs-string">"sk:"</span> + productId + <span class="hljs-string">":user"</span>;
<span class="hljs-comment">// -- 解决超卖1: 监视库存是否发生变化</span>
jedis.<span class="hljs-title function_">watch</span>(productCountKey);
<span class="hljs-comment">// 2、判断当前是否开始了秒杀</span>
<span class="hljs-title class_">Integer</span> productCount = <span class="hljs-title class_">Integer</span>.<span class="hljs-built_in">parseInt</span>(jedis.<span class="hljs-title function_">get</span>(productCountKey));
<span class="hljs-comment">// 2.1 判断当前秒杀是否开始</span>
<span class="hljs-keyword">if</span> (productCount == <span class="hljs-literal">null</span>) {
<span class="hljs-title class_">System</span>.<span class="hljs-property">out</span>.<span class="hljs-title function_">println</span>(<span class="hljs-keyword">new</span> <span class="hljs-title class_">Result</span>(<span class="hljs-literal">false</span>, <span class="hljs-string">"秒杀还未开始"</span>));
<span class="hljs-keyword">return</span>;
}
<span class="hljs-comment">// 3、判断当前用户是否已经秒杀过了</span>
<span class="hljs-keyword">if</span> (jedis.<span class="hljs-title function_">sismember</span>(successSetKey, userId)) {
<span class="hljs-title class_">System</span>.<span class="hljs-property">out</span>.<span class="hljs-title function_">println</span>(<span class="hljs-keyword">new</span> <span class="hljs-title class_">Result</span>(<span class="hljs-literal">false</span>, <span class="hljs-string">"您已经秒杀成功过了,不能再秒杀了"</span>));
<span class="hljs-keyword">return</span>;
}
<span class="hljs-comment">// 4、判断当前库存是否已经没有了</span>
<span class="hljs-keyword">if</span>(productCount <= <span class="hljs-number">0</span>) {
<span class="hljs-title class_">System</span>.<span class="hljs-property">out</span>.<span class="hljs-title function_">println</span>(<span class="hljs-keyword">new</span> <span class="hljs-title class_">Result</span>(<span class="hljs-literal">false</span>, <span class="hljs-string">"非常抱歉,秒杀已经结束了"</span>));
<span class="hljs-keyword">return</span>;
}
<span class="hljs-comment">// 解决超卖2: 开启事物,使如下命令编程串行执行</span>
<span class="hljs-title class_">Transaction</span> multi = jedis.<span class="hljs-title function_">multi</span>();
<span class="hljs-comment">// 5、库存 - 1,秒杀成功的用户列表加上当前用户</span>
multi.<span class="hljs-title function_">decr</span>(productCountKey);
multi.<span class="hljs-title function_">sadd</span>(successSetKey, userId);
<span class="hljs-comment">// 解决超卖3: 执行组队好了的指令</span>
<span class="hljs-title class_">List</span><<span class="hljs-title class_">Object</span>> execResult = multi.<span class="hljs-title function_">exec</span>();
<span class="hljs-keyword">if</span> (execResult == <span class="hljs-literal">null</span> || execResult.<span class="hljs-title function_">size</span>() == <span class="hljs-number">0</span>) {
<span class="hljs-title class_">System</span>.<span class="hljs-property">out</span>.<span class="hljs-title function_">println</span>(<span class="hljs-string">"秒杀失败了"</span>);
jedis.<span class="hljs-title function_">close</span>();
<span class="hljs-keyword">return</span>;
}
jedis.<span class="hljs-title function_">close</span>();
<span class="hljs-title class_">System</span>.<span class="hljs-property">out</span>.<span class="hljs-title function_">println</span>(<span class="hljs-keyword">new</span> <span class="hljs-title class_">Result</span>(<span class="hljs-literal">true</span>, <span class="hljs-string">"恭喜你秒杀成功"</span>));
}
如上虽解决了超卖问题,但是又出现了库存遗留问题
-
明明已经显示秒杀结束了,但是却还有库存
-
出现的原因:因为乐观锁问题,导致大部分线程在抢到了商品而进行库存减少时,都失败了。
-
使用 lua 脚本来实现事务的控制,当使用 lua 脚本时,redis 执行单个脚本是无法被其他客户端的请求所中断的。
-
脚本如下
local productId=KEYS[1];
local userId=KEYS[2];
local productCountKey="sk:"..productId..":qt";
local successSetKey="sk:"..productId..":user";
local userExists=redis.call("sismember",successSetKey,userId);
if tonumber(userExists)==1 then
return 2;
end
local num= redis.call("get" ,productCountKey);
if tonumber(num)<=0 then
return 0;
else
redis.call("decr",productCountKey);
redis.call("sadd",successSetKey,userId);
end
return 1;
- 修改后的代码如下
static String str = "local productId=KEYS[1];\n" +
"local userId=KEYS[2]; \n" +
"local productCountKey=\"sk:\"..productId..\":qt\";\n" +
"local successSetKey=\"sk:\"..productId..\":user\"; \n" +
"local userExists=redis.call(\"sismember\",successSetKey,userId);\n" +
"if tonumber(userExists)==1 then \n" +
" return 2;\n" +
"end\n" +
"local num= redis.call(\"get\" ,productCountKey);\n" +
"if tonumber(num)<=0 then \n" +
" return 0; \n" +
"else \n" +
" redis.call(\"decr\",productCountKey);\n" +
" redis.call(\"sadd\",successSetKey,userId);\n" +
"end\n" +
"return 1;\n";
<span class="hljs-meta">@PostMapping</span>(<span class="hljs-string">"/secKill3"</span>)
<span class="hljs-keyword">public</span> void secKillNew3(<span class="hljs-type">String</span> productId) {
<span class="hljs-type">Jedis</span> jedis <span class="hljs-operator">=</span> <span class="hljs-type">JedisPoolUtils</span>.getInstance().getResource();
<span class="hljs-comment">// 模拟不同的用户,随机4位用户ID</span>
<span class="hljs-type">String</span> userId <span class="hljs-operator">=</span> getRandomUserId();
<span class="hljs-comment">// 加载Lua脚本并执行</span>
<span class="hljs-type">String</span> sha1 <span class="hljs-operator">=</span> jedis.scriptLoad(str);
<span class="hljs-type">Object</span> obj <span class="hljs-operator">=</span> jedis.evalsha(sha1, <span class="hljs-number">2</span>, productId, userId);
<span class="hljs-type">String</span> result <span class="hljs-operator">=</span> <span class="hljs-type">String</span>.valueOf(obj);
<span class="hljs-keyword">if</span>(<span class="hljs-string">"2"</span>.equals(result)) {
<span class="hljs-type">System</span>.out.println(<span class="hljs-string">"您已经秒杀过了,不能再次秒杀"</span>);
}<span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span>(<span class="hljs-string">"0"</span>.equals(result)) {
<span class="hljs-type">System</span>.out.println(<span class="hljs-string">"库存已经没有了,秒杀结束了~"</span>);
}<span class="hljs-keyword">else</span> {
<span class="hljs-type">System</span>.out.println(<span class="hljs-string">"秒杀成功~"</span>);
}
<span class="hljs-comment">// 一定要关闭连接</span>
jedis.close();
}
Redis 之持久化
RDB 持久化
RDB 文件备份的流程
-
Redis 在进行 RDB 文件备份时,会单独创建一个Fork 进程来进行数据的持久化
-
Fork 进程会先将数据写到一个临时的文件中,当持久化结束后,再将该临时文件替换掉原本的 dump.rdb 文件(又称为写时复制技术),这是出于数据的完整性考虑,不然如果直接往磁盘上备份,突然宕机将会导致数据的不完整性。(一般情况父进程和子进程会共用同一段物理内存)
-
在这个过程中,redis 的主进程是不会进行任何 IO 操作的,因此如果进行大规模的数据恢复,RDB 是很不错的选择
-
不过也有个弊端,RDB 最后一次持久化后的数据可能会丢失。
配置文件中关于 RDB 的问题、如何触发 RDB 快照(保持策略)
# rdb 备份的文件名
dbfilename dump.rdb
# 备份文件的路径,与 AOF 共享。 该路径指的是启动 redis-server 的路径
# 如果在当前路径直接执行 redis-server,则是当前路径
# 如果是 /local/usr/bin/redis-server 的话,则为 /local/usr/bin 目录下
dir ./
# 默认当 3600 秒之内有一个 key 改变了,则进行一次持久化
# 当 300 秒之内有 100 个 key 改变了,则进行一次持久化
# 当 60 秒之内有 10000 个 key 改变了,则进行一次持久化,按照间隔时间来
# Redis 会在后台异步进行快照工作,快照的同时还能响应客户端请求
# 当周期的间隔时间到了时会自动触发 bgsave 自动保存
save 3600 1
save 300 100
save 60 10000
# 当 redis 无法写入磁盘的时候,直接关闭 redis 的写操作,推荐 yes
stop-writes-on-bgsave-error yes
# redis 会采用 LZF 算法进行压缩
rdbcompression yes
# 检查快照的完整性,推荐 yes
rdbchecksum yes
RDB 是如何完成数据恢复的 ###SNAPSHOTTING###
- 当 redis 服务启动时,则会按照配置文件中所设置的备份文件的路径、文件名自动完成数据恢复的工作
2 个命令(停止和查看最后一次备份时间)
-
通过 lastsave 命令可以查看最后一次快照时间
-
redis-cli config set save "",禁用保存策略
RDB 的优势与劣势
-
优势
- 恢复数据快
- 节省磁盘空间
- 适合大规模的数据恢复
- 对数据完整性要求不高更适合使用
-
劣势
- 每次 fork 都会写一次临时文件,导致 2 倍的膨胀性
- 虽然 redis 在 fork 时使用了写时复制技术,但是如果数据库庞大还是比较消耗性能
- RDB 是在备份周期的间隔时间做一次备份,如果 redis 意外的 down 掉的话,将会损失最后一次持久化后的数据。
AOF 持久化
AOF 持久化的流程
-
客户端只要执行了写的命令,那么该命令就会被追加到 AOF 缓冲区中
-
AOF 的缓冲区根据 AOF 的持久化策略来决定何时写入到磁盘的 AOF 备份文件当中
-
当 AOF 文件的大小超过重写策略或手动重写时,会对 AOF 文件进行 rewrite 重写,压缩 AOF 文件容量
-
Redis 启动时,会自动的加载 AOF 文件,执行 AOF 文件中的写指令,以达到数据恢复的目的
配置文件中的 AOF 文件 ###APPEND ONLY MODE###
# 是否开启 AOF 功能
appendonly no
# AOF 文件的名称
appendfilename "appendonly.aof"
# AOF 文件的路径跟 RDB 文件的路径一致
dir ./
# AOF 在缓冲区时的同步策略, always 每次写入时都直接同步到磁盘。everysec 每秒。
# no 不主动进行同步操作,由操作系统决定何时同步
appendfsync everysec
与重写相关的配置
# 如果设置为 yes,重写时数据将只写入缓存,不写入 aof 文件,性能更高但可能导致数据丢失
# 设置为 no,则会把数据往磁盘里刷,将会导致主线程处于阻塞状态
no-appendfsync-on-rewrite=yes
# 重写的基准值,文件达到 100% 时开始重写(文件是原来重写后文件的 2 倍时触发)
auto-aof-rewrite-percentage 100
# 设置重写的基准值,当文件大小达到该值后开始重写
auto-aof-rewrite-min-size 64mb
例如:文件达到70MB开始重写,降到50MB,下次什么时候开始重写?100MB
系统载入时或者上次重写完毕时,Redis会记录此时AOF大小,设为base_size,
如果Redis的AOF当前大小>= base_size +base_size*100% (默认)且当前大小>=64mb(默认)的情况下,Redis会对AOF进行重写。
当 AOF 与 RDB 同时存在时,优先选用谁?
- 当 AOF 与 RDB 同时存在时,Redis 会使用 AOF 作为数据恢复的文件
AOF 文件修复命令
redis-check-aof--fix appendonly.aof
AOF 的优势和劣势
-
优势
- 备份机制更加文件,丢失数据的概率更低
- 可读的日志文本,通过操作 AOF 文件,可以处理误操作
-
劣势
- 比起 RDB 更加耗费磁盘空间
- 恢复、备份的速度较慢
- 每次读写都进行同步的话,有一定的性能压力,因为不断的 IO
- 存在个别 bug 将会导致恢复不能
使用建议
-
当更追求速度且对数据完整性要求不高,可以考虑使用 RDB
-
对数据完整性要求高时,可以考虑 AOF
-
官方推荐是 2 个都开启
-
不建议单独使用 AOF,可能会出现 bug
主从复制
- 主机负责读,从机负责写,一般都是一主多从
能够为我们解决的问题
-
容灾快速恢复
-
读写分离,性能扩展
采用模拟的方式完成主从复制,实现一主多从
在 /opt 文件下创建一个 myredis 文件夹,将 redis.conf 文件复制到这修改成公共的配置文件
-
mkdir /opt/myredis
-
cp /etc/redis.conf /opt/myredis/
-
修改配置文件
- 1、bind ip 地址绑定注释掉
- 2、protected no 关闭保护模式
- 3、daemonize yes 开启后台进程
创建 3 个配置文件,分别为 redis6379.conf、redis6380.conf、redis6381.conf
- redis6379.conf
include /opt/myredis/redis.conf
pidfile /opt/myredis/redis6379.pid
port 6379
dbfilename dump6379.rdb
- redis6380.conf
include /opt/myredis/redis.conf
pidfile /opt/myredis/redis6380.pid
port 6380
dbfilename dump6380.rdb
- redis6381.conf
include /opt/myredis/redis.conf
pidfile /opt/myredis/redis6381.pid
port 6381
dbfilename dump6381.rdb
启动 3 个 redis 并查看当前进程
# 执行这些命令的时候,都是在 /opt/myredis 下进行的
redis-server redis6379.conf
redis-server redis6380.conf
redis-server redis6381.conf
使用 3 个 xshell 分别连接到不同的 redis 服务,并查看当前的服务器状态
redis-cli -p 6379
redis-cli -p 6380
redis-cli -p 6381
# 查看服务器的主从复制信息
info replication
操作 2 个从机,使用命令使其连接上主机后再查看 3 台从机的主从信息
- 在 2 个从机分别执行如下命令,由于我 3 台都在本机,所以是 127.0.0.1
slaveof 127.0.0.1 6379
-
主机 6379
-
从机 6380
-
从机 6381
主从复制的几个特点
一主二仆
-
主机可以进行读写的操作
-
从机只能进行读的操作
- 当从机与主机建立上关系后,会向主机发送一个 sync 命令,让主机同步数据文件过来,完成数据的同步
- 之后就是主机自动发请求过来,从机接收。
- 只有第一次连接时,是从机主动发请求让主机发送数据
-
综上所述,主机只要进行了写的操作,从机也可以拿到数据
薪火相传
-
由于每次主机写数据的时候,都需要向所有的从机发送数据进行同步,那么主机的压力就会不断增大
-
因此可以这样:主机底下永远只有几个从机,但是从机又是其他服务器的主机
-
类似于领导下有 2 个直系管理的人员,而这 2 个人员又是其他人的管理者
-
这里演示一下,6381 从机将其主机设置为 6380
127.0.0.1:6381> slaveof localhost 6380
-
此时我们看看 6380 的主从复制信息
-
此时当 6379 再进行写操作时,就只会向 6380 发送数据了。而 6380 再向 6381 发送数据来进行同步
-
有一个问题需要注意:就是当 6380 宕机了,竟会导致 6381 也无法同步到数据。
反客为主
-
在一主一从、或者一主多从的时候,如果主机宕机了,那么从机可以反客为主晋升为主机
- 从机手动执行:
slaveof no one
命令,晋升为主机
- 从机手动执行:
-
如果从机不反客为主,那么当挂掉的主机重启时,他们的关系依然是主从关系。
-
而当之前的主机再次开机时,会发现自己还是主机,但是那台反客为主的从机已经没了。
复制原理
-
当从机连接上主机后,会向主机发送一个 sync 命令,然后主机将会被整个的数据文件发送给从机以此来完成数据同步
-
而仅仅是第一次连接时,是从机向主机发送同步命令,之后就全都由主机来主动完成同步
-
全量复制: 每次重新连接上主机时,都会进行一次全量复制
-
增量复制: 已经连接上了主机后,由主机主动发送过来的同步数据为增量复制
哨兵模式
-
可以理解为反客为主的升级版
-
可以监视主从服务器的状态,当主机宕机时,会根据策略选取一名从机来充当主机
-
而当主机再次上线时,会发现自己变成了从机。
创建哨兵启动时所需的配置文件
-
在 /opt/myredis/ 下创建一个 sentinel.conf 配置文件
-
修改其中的内容
# mymaster 为监控对象起的服务器名称,1 为至少有多少个哨兵同意迁移的数量
# 啥意思呢?意思是如果需要迁移主机,只需要一个及一个以上哨兵同意即可
# 开启了哨兵模式,那么所有的服务器都成了哨兵
sentinel monitor mymaster 127.0.0.1 6379 1
启动哨兵,开启后的默认端口号为 26379
redis-sentinel /opt/myredis/sentinel.conf
-
此时当主机宕机时,比如关闭掉 6379 这台主机
-
此时发现我们的哨兵进程已经监控到,并且完成了容灾的处理
哨兵选举新主机的策略
-
(1)根据每台服务器配置文件中的
slave-priority
配置来决定,数值越小优先级越高 -
(2)选择偏移量最大的(指的是哪个从机复制的数据最多最全的)
-
(3)选择 runid 最小的从机,每个 redis 服务在运行时都会随机生成一个 40 位的 runid
哨兵模式小总结
-
当监控线程发现了主机宕机后,会选取一个从机晋升为主机
-
并且将之前主机之下的从机切换成新晋升的主机
-
当之前的主机再次上线时,发现自己也变成了新主机的从机
在 Java 程序中使用哨兵模式
- 修改一下之前配置的线程池,改成 JedisSetinelPool
public class JedisPoolUtils {
private volatile static JedisSentinelPool jedisPool = null;
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> JedisSentinelPool <span class="hljs-title">getInstance</span>()</span> {
<span class="hljs-keyword">if</span>(jedisPool == <span class="hljs-literal">null</span>) {
synchronized (JedisPoolUtils.<span class="hljs-keyword">class</span>) {
<span class="hljs-keyword">if</span>(jedisPool == <span class="hljs-literal">null</span>) {
<span class="hljs-comment">// 创建一个set集合,保存哨兵线程的 ip和端口号</span>
Set<String> sentinelSet=<span class="hljs-keyword">new</span> HashSet<>();
sentinelSet.<span class="hljs-keyword">add</span>(<span class="hljs-string">"192.168.22.100:26379"</span>);
JedisPoolConfig jedisPoolConfig = <span class="hljs-keyword">new</span> JedisPoolConfig();
<span class="hljs-comment">// 最大连接</span>
jedisPoolConfig.setMaxTotal(<span class="hljs-number">200</span>);
<span class="hljs-comment">// 最大空闲</span>
jedisPoolConfig.setMaxIdle(<span class="hljs-number">32</span>);
<span class="hljs-comment">// 最大等待时间</span>
jedisPoolConfig.setMaxWaitMillis(<span class="hljs-number">100</span> * <span class="hljs-number">1000</span>);
<span class="hljs-comment">// 表示当pool中的jedis 实例都被分配完时,是否要进行阻塞</span>
jedisPoolConfig.setBlockWhenExhausted(<span class="hljs-literal">true</span>);
<span class="hljs-comment">// 每次获取连接时候都要到数据库验证连接有效性</span>
jedisPoolConfig.setTestOnBorrow(<span class="hljs-literal">true</span>);
jedisPool = <span class="hljs-keyword">new</span> JedisSentinelPool(<span class="hljs-string">"mymaster"</span>, sentinelSet,jedisPoolConfig);
}
}
}
<span class="hljs-keyword">return</span> jedisPool;
}
}
Redis 集群
集群提供了什么好处
-
Redis 集群实现了对 Redis 的水平扩容,实现多台服务器分担客户端请求压力
-
Redis 中的集群将会有一个slots 插槽值,集群中的每个节点都刚好能存储数据的 1/N,这个 N 指的是节点的数量。而插槽 slots 则规定了整个集群能够存储数据的插槽范围,每个节点都会有对应的那一段范围。 在存储一个 key 时,会计算该 key 对应的插槽位置,存储到指定的节点。
-
并且 Redis 为我们提供了 无中心化集群 配置,使得集群中的任何一个节点都能够访问到其他的节点,也就是客户端请求服务时,即便请求的服务是节点 B 提供的,访问节点 A 也可以获取到节点 B 的提供的服务。
制作 6 个实例,分别实现刚好能让他们一主一从,3 个节点
-
(1)复制原先 /conf/redis.conf 文件到 /opt/myredis/ 目录下
-
(2)修改该配置文件
- (2.1)开启后台启动
- (2.2)关闭 ip 绑定
- (2.3)关闭保护模式 -
(3)创建 6 个配置文件,分别为 redis6379.conf、redis6380.conf、redis6381.conf、redis6389.conf、redis6390.conf、redis6391.conf
- (3.1)配置基本信息(pidfile、port、dump.rdb、log 日志文件名、aof 功能)
- (3.2)配置集群信息(打开集群模式、设置节点配置文件名称、设置节点失联时间,超时自动进行主从切换)
- 提示:可以使用
%s/6379/6380
这样的命令来一次性替换
# 引入 redis 配置文件
include /opt/myredis/redis.conf
# 当前服务的进程 id 存放地址
pidfile "/opt/myredis/redis6379.pid"
port 6379
# rdb 备份文件名称
dbfilename "dump6379.rdb"
# 后台启动
daemonize yes
# 关闭保护模式
protected-mode no
# 开启集群功能
cluster-enabled yes
# 设置集群的节点配置文件名
cluster-config-file nodes-6379.conf
# 配置节点失联时间,这里是毫秒,换算成秒就是 15 秒
cluster-node-timeout 15000
启动 6 个 redis 服务并查看进程
redis-server redis6379.conf
redis-server redis6380.conf
redis-server redis6381.conf
redis-server redis6389.conf
redis-server redis6390.conf
redis-server redis6391.conf
将 6 个节点合并成一个集群
-
进入到之前解压 redis 的目录:
cd /opt/redis-6.2.1/src/
-
执行如下命令
# --replicas 1采用最简单的方式配置集群
redis-cli --cluster create --cluster-replicas 1 192.168.22.100:6379 192.168.22.100:6380 192.168.22.100:6381 192.168.22.100:6389 192.168.22.100:6390 192.168.22.100:6391
中途会有点提示,询问是否按照默认分配的主机与从机进行集群配置。
- 最后还能看到插槽的数量是 16384 个,下面最大是 16383 是因为,插槽是从 0 开始算的
- 6379 这个节点的插槽是:0~5460
- 6380 节点:5461~10922
- 6381 节点:10923~16383
连接客户端开始使用(采用集群方式连接)
-
如果不采用集群方式连接,存储数据时,如果 key 计算的插槽不在当前节点,则会导致出错
-
因此应该采用集群的方式连接客户端
redis-cli -c -p 6379
-
此时由于无中心化集群配置的原因,设置 key 和获取 key 对应的值时,都会根据 key 所对应的插槽使当前连接的客户端连接到指定节点
集群的故障恢复以及相关配置
-
综上的配置,当前的集群一共有 3 个主机,3 个从机
-
如果一个主机宕机,那么 15 秒内从机将会晋升为主机,当主机再次上线时,就变成了从机
-
redis 的配置文件中有如下配置
# 如果该配置设置为 yes,那么如果某个节点的主机和从机全部挂掉时,整个集群都会直接挂掉
# 如果设置为 no,那么剩下的主从节点将会继续提供它们插槽范围内的服务
cluster-require-full-coverage yes
Jedis 的集群开发
- 使用 JedisCluster 工具来完成集群操作(别忘了关闭 Linux 的防火墙,或者开放端口)
public class JedisClusterTest {
public static void main(String[] args) {
// 注意:访问任何一个节点都是可以的,因为是无中心化集群
JedisCluster jedisCluster = new JedisCluster(
new HostAndPort("192.168.22.100", 6391));
jedisCluster.<span class="hljs-keyword">set</span>(<span class="hljs-string">"k3"</span>, <span class="hljs-string">"wangming"</span>);
String result = jedisCluster.<span class="hljs-keyword">get</span>(<span class="hljs-string">"k3"</span>);
System.<span class="hljs-keyword">out</span>.println(result);
}
}
集群的好处和不足
-
好处
- 减轻了单台服务器的压力
- 实现扩容
- 无中心化配置比较简单
-
不足
- 多键操作是不被支持的,虽然可以靠着分组完成,但是并不方便
- 多键的 Redis 事务是不被支持的,lua 脚本不被支持
Redis 应用问题及解决方案
缓存穿透
-
问题描述
- (1)服务器的压力突然剧增
- (2)访问的接口全部都无法靠缓存处理,key 对应的数据源并不存在
- (3)访问的请求甚至是无效请求
- (4)一般遇到这种情况都是黑客攻击,可以考虑直接报网警
-
解决方案
- (1)对空值进行缓存。如果查询返回的数据为 null,将其缓存到 redis 数据库中,设置极短的过期时间,一般不超过 5 分钟(应急方案)
- (2)设置可以访问的白名单。使用 bitMaps 定义一个可以访问的名单,名单 id 作为 bitMaps 的偏移量,每次访问时都通过 bitMaps 来查询是否在白名单范围内
- (3)采用布隆过滤器
- (4)进行实时监控,当发现 Redis 的缓存命中率急剧下降时,需要排查访问对象,与运维人员配合。
- (5)直接报警。
缓存击穿
-
问题描述
- (1)服务器的压力突然增加
- (2)此时缓存服务器中的某个热门 key 突然过期了
- (3)而大量的请求都在访问这个热门 key,最终导致大量请求都在一瞬间访问服务器,导致服务器压力过大直接宕机。
-
解决方案
- (1)预先设置好一些热门数据
- (2)实时调整:现场监控哪些热门数据,实时调整 key 的过期时间
- (3)使用锁的方式。
- 当缓存失效时,不是立即去访问服务器
- 使用某些操作成功会带返回值的操作,例如 redis 的 setnx,来设置一个互斥的 key
- 当操作返回成功的时候,再进行访问服务器的操作,并回头设置缓存,最后删除互斥 key
- 当操作返回失败,证明已经有线程正在 load db,可以当线程睡眠一会后直接 get 整个缓存
缓存雪崩
-
问题描述
- (1)服务器的压力突然增加
- (2)缓存服务器中的多个 key 在同一时间过期
- (3)而刚好在这时有大量的客户端请求发送过来,并且大部分都没有办法命中缓存
- (4)因此全都会去访问服务器,最终服务器不堪重负宕机
-
解决方案
- (1)构建多级缓存结构: nginx 缓存 + redis 缓存 + 其他缓存(ehcache 等)
- (2)使用锁或队列:不适用大量并发情况
- (3)设置过期标识更新缓存
- 记录缓存是否过期(设置提前量),如果过期会触发通知另外的线程完成对缓存的更新。
- (4)将缓存的过期时间分散开来。
- 比如我们可以在原有缓存的失效时间基础上添加一个随机值,比如 1~5 分钟,这样就可以减少大量的缓存在同一时间过期的概率。
分布式锁
-
指的是在不同的机器提供的服务,需要实现一把锁对所有机器提供服务的控制,为了解决这个问题,就需要一种技术来实现跨 JVM 的互斥机制来控制共享资源的访问。上锁之后对所有机器都有效,这就叫做分布式锁
-
解决方案
- (1)基于数据库实现分布式锁
- (2)基于缓存(Redis 等)
- (3)给予 Zookeeper
-
优缺点
- (1)性能: redis 性能最高
- (2)可靠性: Zookeeper 可靠性最高
使用 Redis 实现分布式锁
redis 命令:# set sku:1:info "OK" NX PX 10000
-
EX second:
设置键的过期时间为秒 -
PX millisecond
:设置键的过期时间为毫秒 -
NX
:只有当键不存在时,才能对键进行操作 -
XX
:只有当键已经存在时,才对键进行操作
实现 redis 中 num 数字的增加,靠 setnx 完成
@RestController
public class NumRedisLockController {
@Autowired
private RedisTemplate redisTemplate;
<span class="hljs-comment">// 1、压力测试前: 先在redis当中设置string类型的 num = 0</span>
<span class="hljs-meta">@RequestMapping</span>(<span class="hljs-string">"numLockTest"</span>)
<span class="hljs-keyword">public</span> <span class="hljs-built_in">void</span> <span class="hljs-title function_">numLockTest</span>(<span class="hljs-params"></span>) {
<span class="hljs-comment">// 2、上锁</span>
<span class="hljs-keyword">if</span> (redisTemplate.<span class="hljs-title function_">opsForValue</span>().<span class="hljs-title function_">setIfAbsent</span>(<span class="hljs-string">"numLock"</span>, <span class="hljs-string">"ok"</span>)){
<span class="hljs-comment">// 执行num + 1</span>
redisTemplate.<span class="hljs-title function_">opsForValue</span>().<span class="hljs-title function_">increment</span>(<span class="hljs-string">"num"</span>);
<span class="hljs-comment">// 3、解锁</span>
redisTemplate.<span class="hljs-title function_">delete</span>(<span class="hljs-string">"numLock"</span>);
}<span class="hljs-keyword">else</span> {
<span class="hljs-keyword">try</span> { <span class="hljs-title class_">TimeUnit</span>.<span class="hljs-property">NANOSECONDS</span>.<span class="hljs-title function_">sleep</span>(<span class="hljs-number">20</span>);} <span class="hljs-keyword">catch</span> (<span class="hljs-title class_">InterruptedException</span> e) {e.<span class="hljs-title function_">printStackTrace</span>(); }
<span class="hljs-comment">// 重试</span>
<span class="hljs-title function_">numLockTest</span>();
}
}
}
优化之添加锁的过期时间
-
为什么要这么做?
- 因为如果一个线程上了锁之后,在里面发生了异常,可能最终导致都无法解锁,那么其他线程将会一直等待。
-
实现代码
@RestController
public class NumRedisLockController {
@Autowired
private RedisTemplate redisTemplate;
<span class="hljs-comment">// 1、压力测试前: 先在redis当中设置string类型的 num = 0</span>
<span class="hljs-meta">@RequestMapping</span>(<span class="hljs-string">"numLockTest"</span>)
<span class="hljs-keyword">public</span> <span class="hljs-built_in">void</span> <span class="hljs-title function_">numLockTest</span>(<span class="hljs-params"></span>) {
<span class="hljs-comment">// 2、上锁 ,设置过期时间</span>
<span class="hljs-keyword">if</span> (redisTemplate.<span class="hljs-title function_">opsForValue</span>().<span class="hljs-title function_">setIfAbsent</span>(<span class="hljs-string">"numLock"</span>, <span class="hljs-string">"ok"</span>, <span class="hljs-number">1</span>, <span class="hljs-title class_">TimeUnit</span>.<span class="hljs-property">SECONDS</span>)){
<span class="hljs-comment">// 执行num + 1</span>
redisTemplate.<span class="hljs-title function_">opsForValue</span>().<span class="hljs-title function_">increment</span>(<span class="hljs-string">"num"</span>);
<span class="hljs-comment">// 3、解锁</span>
redisTemplate.<span class="hljs-title function_">delete</span>(<span class="hljs-string">"numLock"</span>);
}<span class="hljs-keyword">else</span> {
<span class="hljs-keyword">try</span> { <span class="hljs-title class_">TimeUnit</span>.<span class="hljs-property">NANOSECONDS</span>.<span class="hljs-title function_">sleep</span>(<span class="hljs-number">20</span>);} <span class="hljs-keyword">catch</span> (<span class="hljs-title class_">InterruptedException</span> e) {e.<span class="hljs-title function_">printStackTrace</span>(); }
<span class="hljs-comment">// 重试</span>
<span class="hljs-title function_">numLockTest</span>();
}
}
}
优化之添加 UUID 防止误删除
-
为什么会出现这种问题
- 线程 A 刚要执行解锁操作时,锁的过期时间到了,导致锁过期了。
- 这个时候如果线程 B 拿到锁进入了方法体,将会导致 A 释放了线程 B 的锁。
-
实现方案
@RestController
public class NumRedisLockController {
@Autowired
private RedisTemplate redisTemplate;
<span class="hljs-comment">// 1、压力测试前: 先在redis当中设置string类型的 num = 0</span>
<span class="hljs-meta">@RequestMapping</span>(<span class="hljs-string">"numLockTest"</span>)
<span class="hljs-keyword">public</span> <span class="hljs-built_in">void</span> <span class="hljs-title function_">numLockTest</span>(<span class="hljs-params"></span>) {
<span class="hljs-comment">// 获取UUID</span>
<span class="hljs-title class_">String</span> uuid = <span class="hljs-variable constant_">UUID</span>.<span class="hljs-title function_">randomUUID</span>().<span class="hljs-title function_">toString</span>();
<span class="hljs-comment">// 2、上锁 ,设置过期时间</span>
<span class="hljs-keyword">if</span> (redisTemplate.<span class="hljs-title function_">opsForValue</span>().<span class="hljs-title function_">setIfAbsent</span>(<span class="hljs-string">"numLock"</span>, uuid, <span class="hljs-number">1</span>, <span class="hljs-title class_">TimeUnit</span>.<span class="hljs-property">SECONDS</span>)){
<span class="hljs-comment">// 执行num + 1</span>
redisTemplate.<span class="hljs-title function_">opsForValue</span>().<span class="hljs-title function_">increment</span>(<span class="hljs-string">"num"</span>);
<span class="hljs-comment">// 3、解锁</span>
<span class="hljs-keyword">if</span> (uuid.<span class="hljs-title function_">equals</span>((<span class="hljs-title class_">String</span>)redisTemplate.<span class="hljs-title function_">opsForValue</span>().<span class="hljs-title function_">get</span>(<span class="hljs-string">"numLock"</span>))) {
redisTemplate.<span class="hljs-title function_">delete</span>(<span class="hljs-string">"numLock"</span>);
}
}<span class="hljs-keyword">else</span> {
<span class="hljs-keyword">try</span> { <span class="hljs-title class_">TimeUnit</span>.<span class="hljs-property">NANOSECONDS</span>.<span class="hljs-title function_">sleep</span>(<span class="hljs-number">20</span>);} <span class="hljs-keyword">catch</span> (<span class="hljs-title class_">InterruptedException</span> e) {e.<span class="hljs-title function_">printStackTrace</span>(); }
<span class="hljs-comment">// 重试</span>
<span class="hljs-title function_">numLockTest</span>();
}
}
}
优化之使用 lua 脚本保证的原子性
-
问题分析
- 线程 A 判断成功了当前锁对应的 value 值确实是自己的 uuid
- 刚准备执行删除操作的时候,锁过期时间到了
- 与此同时,线程 B 拿到了锁
- 此时线程 A 将会把线程 B 的锁给释放。
- 因此得出结论,需要让进行删除标识判断以及删除的操作保持原子性
-
创建一个 lua 脚本
if redis.call('get', KEYS[1]) == ARGV[1]
then
return redis.call('del', KEYS[1])
else
return 0
end
- 使用示例
@RestController
public class NumRedisLockController {
@Autowired
private RedisTemplate redisTemplate;
<span class="hljs-comment">// 1、压力测试前: 先在redis当中设置string类型的 num = 0</span>
<span class="hljs-meta">@RequestMapping("numLockTest")</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">numLockTest</span><span class="hljs-params">()</span> {
<span class="hljs-comment">// 获取UUID</span>
<span class="hljs-type">String</span> <span class="hljs-variable">uuid</span> <span class="hljs-operator">=</span> UUID.randomUUID().toString();
<span class="hljs-type">String</span> <span class="hljs-variable">lockKey</span> <span class="hljs-operator">=</span> <span class="hljs-string">"numLock"</span>;
<span class="hljs-comment">// 获取锁</span>
<span class="hljs-type">Boolean</span> <span class="hljs-variable">lock</span> <span class="hljs-operator">=</span> redisTemplate.opsForValue().setIfAbsent(lockKey, uuid, <span class="hljs-number">1</span>, TimeUnit.SECONDS);
<span class="hljs-keyword">if</span> (lock) {
<span class="hljs-comment">// 使num 每次+1 放入缓存</span>
redisTemplate.opsForValue().increment(<span class="hljs-string">"num"</span>);
<span class="hljs-comment">/*使用lua脚本来锁*/</span>
<span class="hljs-comment">// 定义lua 脚本</span>
<span class="hljs-type">String</span> <span class="hljs-variable">script</span> <span class="hljs-operator">=</span> <span class="hljs-string">"if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"</span>;
<span class="hljs-comment">// 创建一个Redis脚本文件</span>
DefaultRedisScript<Long> redisScript = <span class="hljs-keyword">new</span> <span class="hljs-title class_">DefaultRedisScript</span><>();
redisScript.setScriptText(script);
<span class="hljs-comment">// 设置一下返回值类型 为Long ,指的是redisTemplate.execute返回的实际类型</span>
redisScript.setResultType(Long.class);
<span class="hljs-comment">// 第一个要是script 脚本 ,第二个需要判断的key,第三个就是key所对应的值。</span>
<span class="hljs-type">Long</span> <span class="hljs-variable">execute</span> <span class="hljs-operator">=</span> (Long)redisTemplate.execute(redisScript, Arrays.asList(lockKey), uuid);
System.out.println(execute == <span class="hljs-number">1</span> ? <span class="hljs-string">"执行成功"</span>: <span class="hljs-string">"执行失败"</span>);
}<span class="hljs-keyword">else</span> {
<span class="hljs-keyword">try</span> { TimeUnit.NANOSECONDS.sleep(<span class="hljs-number">20</span>);} <span class="hljs-keyword">catch</span> (InterruptedException e) {e.printStackTrace(); }
<span class="hljs-comment">// 重试</span>
numLockTest();
}
}
}
内存淘汰策略
8 种淘汰策略(分为两类,一类为设置了过期时间的,一类为没有设置过期时间的)
-
1、noeviction
第一种淘汰策略是 noeviction,它是 Redis 的默认策略。在内存超过阀值后,Redis 不做任何清理工作,然后对所有写操作返回错误,但对读请求正常处理。noeviction 适合数据量不大的业务场景,将关键数据存入 Redis 中,将 Redis 当作 DB 来使用。 -
2、 volatile-lru
第二种淘汰策略是 volatile-lru,它对带过期时间的 key 采用最近最少访问算法来淘汰。使用这种策略,Redis 会从 redisDb 的 expire dict 过期字典中,首先随机选择 N 个 key,计算 key 的空闲时间,然后插入 evictionPool 中,最后选择空闲时间最久的 key 进行淘汰。这种策略适合的业务场景是,需要淘汰的 key 带有过期时间,且有冷热区分,从而可以淘汰最久没有访问的 key。 -
3、volatile-lfu
第三种策略是 volatile-lfu,它对带过期时间的 key 采用最近最不经常使用的算法来淘汰。使用这种策略时,Redis 会从 redisDb 中的 expire dict 过期字典中,首先随机选择 N 个 key,然后根据其 value 的 lru 值,计算 key 在一段时间内的使用频率相对值。对于 lfu,要选择使用频率最小的 key,为了沿用 evictionPool 的 idle 概念,Redis 在计算 lfu 的 Idle 时,采用 255 减去使用频率相对值,从而确保 Idle 最大的 key 是使用次数最小的 key,计算 N 个 key 的 Idle 值后,插入 evictionPool,最后选择 Idle 最大,即使用频率最小的 key,进行淘汰。这种策略也适合大多数 key 带过期时间且有冷热区分的业务场景。 -
4、volatile-ttl
第四种策略是 volatile-ttl,它是对带过期时间的 key 中选择最早要过期的 key 进行淘汰。使用这种策略时,Redis 也会从 redisDb 的 expire dict 过期字典中,首先随机选择 N 个 key,然后用最大无符号 long 值减去 key 的过期时间来作为 Idle 值,计算 N 个 key 的 Idle 值后,插入 evictionPool,最后选择 Idle 最大,即最快就要过期的 key,进行淘汰。这种策略适合,需要淘汰的 key 带过期时间,且有按时间冷热区分的业务场景。 -
5、volatile-random
第五种策略是 volatile-random,它是对带过期时间的 key 中随机选择 key 进行淘汰。使用这种策略时,Redis 从 redisDb 的 expire dict 过期字典中,随机选择一个 key,然后进行淘汰。如果需要淘汰的 key 有过期时间,没有明显热点,主要被随机访问,那就适合选择这种淘汰策略。 -
6、allkey-lru
第六种策略是 allkey-lru,它是对所有 key,而非仅仅带过期时间的 key,采用最近最久没有使用的算法来淘汰。这种策略与 volatile-lru 类似,都是从随机选择的 key 中,选择最长时间没有被访问的 key 进行淘汰。区别在于,volatile-lru 是从 redisDb 中的 expire dict 过期字典中选择 key,而 allkey-lru 是从所有的 key 中选择 key。这种策略适合,需要对所有 key 进行淘汰,且数据有冷热读写区分的业务场景。 -
7、allkeys-lfu
第七种策略是 allkeys-lfu,它也是针对所有 key 采用最近最不经常使用的算法来淘汰。这种策略与 volatile-lfu 类似,都是在随机选择的 key 中,选择访问频率最小的 key 进行淘汰。区别在于,volatile-flu 从 expire dict 过期字典中选择 key,而 allkeys-lfu 是从主 dict 中选择 key。这种策略适合的场景是,需要从所有的 key 中进行淘汰,但数据有冷热区分,且越热的数据访问频率越高。 -
8、allkeys-random
第八种策略是 allkeys-random,它是针对所有 key 进行随机算法进行淘汰。它也是从主 dict 中随机选择 key,然后进行删除回收。如果需要从所有的 key 中进行淘汰,并且 key 的访问没有明显热点,被随机访问,即可采用这种策略。
配置 redis 内存淘汰策略
-
设置 Redis 内存大小的限制,我们可以设置 maxmemory <bytes>,当数据达到限定大小后,会选择配置的策略淘汰数据
-
在配置文件中搜索 maxmemory-policy,设置 Redis 的淘汰策略
SpringBoot 整合 redis 并整合 Spring 提供的 CacheManager
引入依赖并修改依赖
<!--redis 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 修改 application.yml 配置
spring:
redis:
port: 6379
host: 192.168.50.10
添加一个 redis 配置类(使用的时候可以使用 @Resource 注入)
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
@Configuration
public class RedisConfig {
@Bean
public Jackson2JsonRedisSerializer<Object> jackson(){
Jackson2JsonRedisSerializer<Object> jackson2Serializer =
new Jackson2JsonRedisSerializer<>(Object.class);
// 序列化时添加对象信息:防止出现把 Object 转换成 LinkedHashMap
ObjectMapper om = new ObjectMapper();
/*
指定序列化的属性,
PropertyAccessor.ALL:表示属性、get 和 set
JsonAutoDetect.Visibility.ANY: 表示包括 private 和 public
/
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
/
指定序列化输入类型,也就是值类型
*/
om.activateDefaultTyping(
// 只序列化非 final 的属性(例如不会把 String,Integer 等类型的数据按照我们的序列化方式)
LaissezFaireSubTypeValidator.instance ,
/对于除了一些自然类型 (String、Double、Integer、Double) 类型外的非常量 (non-final) 类型,类型将会用在值的含义上。以便可以在 JSON 串中正确的推测出值所属的类型。/
ObjectMapper.DefaultTyping.NON_FINAL,
// 将多态信息作为数据的兄弟属性进行序列化
JsonTypeInfo.As.PROPERTY);
// 对象属性为空时,不进行序列化储存
om.setSerializationInclusion(JsonInclude.Include.NON_NULL);
// 把 long 类型的 id 已字符串形式格式化
SimpleModule simpleModule = new SimpleModule();
simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance);
om.registerModule(simpleModule);
<span class="hljs-comment">//格式化日期java.util.date</span>
<span class="hljs-comment">// om.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH🇲🇲ss"));</span>
<span class="hljs-comment">// 处理java8日期格式的模块</span>
<span class="hljs-type">JavaTimeModule</span> <span class="hljs-variable">javaTimeModule</span> <span class="hljs-operator">=</span> getTimeModule();
om.registerModule(javaTimeModule);
<span class="hljs-comment">// 如果有未知属性,则直接不反解析未知属性到实体类中即可,不抛出异常。</span>
om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, <span class="hljs-literal">false</span>);
jackson2Serializer.setObjectMapper(om);
<span class="hljs-keyword">return</span> jackson2Serializer;
}
<span class="hljs-comment">/**
* 处理java8中,时间日期格式的Module
* <span class="hljs-doctag">@return</span>
*/</span>
<span class="hljs-keyword">private</span> JavaTimeModule <span class="hljs-title function_">getTimeModule</span><span class="hljs-params">()</span> {
<span class="hljs-type">JavaTimeModule</span> <span class="hljs-variable">javaTimeModule</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">JavaTimeModule</span>();
<span class="hljs-type">String</span> <span class="hljs-variable">STANDARD_PATTERN</span> <span class="hljs-operator">=</span> <span class="hljs-string">"yyyy-MM-dd HH🇲🇲ss"</span>;
<span class="hljs-type">String</span> <span class="hljs-variable">DATE_PATTERN</span> <span class="hljs-operator">=</span> <span class="hljs-string">"yyyy-MM-dd"</span>;
<span class="hljs-type">String</span> <span class="hljs-variable">TIME_PATTERN</span> <span class="hljs-operator">=</span> <span class="hljs-string">"HH🇲🇲ss"</span>;
<span class="hljs-comment">//处理LocalDateTime</span>
<span class="hljs-type">DateTimeFormatter</span> <span class="hljs-variable">dateTimeFormatter</span> <span class="hljs-operator">=</span> DateTimeFormatter.ofPattern(STANDARD_PATTERN);
javaTimeModule.addSerializer(LocalDateTime.class, <span class="hljs-keyword">new</span> <span class="hljs-title class_">LocalDateTimeSerializer</span>(dateTimeFormatter));
javaTimeModule.addDeserializer(LocalDateTime.class, <span class="hljs-keyword">new</span> <span class="hljs-title class_">LocalDateTimeDeserializer</span>(dateTimeFormatter));
<span class="hljs-comment">//处理LocalDate</span>
<span class="hljs-type">DateTimeFormatter</span> <span class="hljs-variable">dateFormatter</span> <span class="hljs-operator">=</span> DateTimeFormatter.ofPattern(DATE_PATTERN);
javaTimeModule.addSerializer(LocalDate.class, <span class="hljs-keyword">new</span> <span class="hljs-title class_">LocalDateSerializer</span>(dateFormatter));
javaTimeModule.addDeserializer(LocalDate.class, <span class="hljs-keyword">new</span> <span class="hljs-title class_">LocalDateDeserializer</span>(dateFormatter));
<span class="hljs-comment">//处理LocalTime</span>
<span class="hljs-type">DateTimeFormatter</span> <span class="hljs-variable">timeFormatter</span> <span class="hljs-operator">=</span> DateTimeFormatter.ofPattern(TIME_PATTERN);
javaTimeModule.addSerializer(LocalTime.class, <span class="hljs-keyword">new</span> <span class="hljs-title class_">LocalTimeSerializer</span>(timeFormatter));
javaTimeModule.addDeserializer(LocalTime.class, <span class="hljs-keyword">new</span> <span class="hljs-title class_">LocalTimeDeserializer</span>(timeFormatter));
<span class="hljs-keyword">return</span> javaTimeModule;
}
<span class="hljs-comment">/**
* 主要配置了redis在序列化与反序列化时的一个序列化器
* <span class="hljs-doctag">@param</span> redisConnectionFactory
* <span class="hljs-doctag">@param</span> sedisSerializer
* <span class="hljs-doctag">@return</span>
*/</span>
<span class="hljs-meta">@Bean</span>
<span class="hljs-keyword">public</span> RedisTemplate<String,Object> <span class="hljs-title function_">redisTemplate</span><span class="hljs-params">(RedisConnectionFactory redisConnectionFactory,Jackson2JsonRedisSerializer<Object> sedisSerializer)</span>{
RedisTemplate<String, Object> redisTemplate = <span class="hljs-keyword">new</span> <span class="hljs-title class_">RedisTemplate</span>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
<span class="hljs-comment">// 创建key的序列化方式</span>
<span class="hljs-type">StringRedisSerializer</span> <span class="hljs-variable">stringRedisSerializer</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">StringRedisSerializer</span>();
<span class="hljs-comment">// 指定string类型的key的序列化方式: string</span>
redisTemplate.setKeySerializer(stringRedisSerializer);
<span class="hljs-comment">// hash类型key的序列化方式</span>
redisTemplate.setHashKeySerializer(stringRedisSerializer);
<span class="hljs-comment">// 创建value的序列化方式</span>
<span class="hljs-comment">// 指定string类型的value的序列化方式:json</span>
redisTemplate.setValueSerializer(sedisSerializer);
<span class="hljs-comment">// hash类型value的序列化方式</span>
redisTemplate.setHashValueSerializer(sedisSerializer);
<span class="hljs-keyword">return</span> redisTemplate;
}
<span class="hljs-comment">/**
* 指定使用redis进行缓存,
* 给容器注册一个Bean,返回缓存管理器,如果容器中有redisTemplate,会自动注入
* <span class="hljs-doctag">@return</span>
*/</span>
<span class="hljs-meta">@Bean</span>
<span class="hljs-keyword">public</span> CacheManager <span class="hljs-title function_">MyRedisCacheConfig</span><span class="hljs-params">(RedisConnectionFactory redisConnectionFactory,Jackson2JsonRedisSerializer<Object> sedisSerializer)</span> {
<span class="hljs-comment">//1.创建RedisCacheWriter</span>
<span class="hljs-comment">/**
* 非锁方式:nonLockingRedisCacheWriter(RedisConnectionFactory connectionFactory);
* 有锁方式:lockingRedisCacheWriter(RedisConnectionFactory connectionFactory);
*/</span>
<span class="hljs-type">RedisCacheWriter</span> <span class="hljs-variable">redisCacheWriter</span> <span class="hljs-operator">=</span> RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
<span class="hljs-comment">//3.传入 Jackson对象 并获取 RedisSerializationContext对象</span>
RedisSerializationContext<Object, Object> serializationContext = RedisSerializationContext.fromSerializer(sedisSerializer);
<span class="hljs-comment">//4.配置RedisCacheConfiguration</span>
<span class="hljs-comment">/**
* RedisCacheConfiguration.defaultCacheConfig()
* 设置 value 的序列化 serializeValuesWit(SerializationPari<?> valueSerializationPari)
* 设置 key 的序列化 serializeKeysWith(SerializationPari valueSerializationPari)
*/</span>
<span class="hljs-type">RedisCacheConfiguration</span> <span class="hljs-variable">redisCacheConfiguration</span> <span class="hljs-operator">=</span> RedisCacheConfiguration.defaultCacheConfig().serializeValuesWith(serializationContext.getValueSerializationPair());
<span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">RedisCacheManager</span>(redisCacheWriter, redisCacheConfiguration);
}
}
非常值得注意的一件事,Jackson 解析与反解析是根据实体类中的什么来决定的
-
需要被序列化的类必须实现 Serializable 接口
-
进行序列化时,不仅会根据 get 方法,还会根据任何有返回值的方法的返回结果进行序列化
-
而在反序列化时,只会根据 set 方法,如果没有指定的 set 方法完成反序列化,则会报错
-
因此需要添加如下配置
// 如果有未知属性,则直接不反解析未知属性到实体类中即可,不抛出异常。
om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
添加一个 Redis 的工具类
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.TimeUnit;
@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisCache
{
@Autowired
public RedisTemplate redisTemplate;
<span class="hljs-comment">/**
* 缓存基本的对象,Integer、String、实体类等
*
* <span class="hljs-doctag">@param</span> key 缓存的键值
* <span class="hljs-doctag">@param</span> value 缓存的值
*/</span>
<span class="hljs-keyword">public</span> <T> <span class="hljs-built_in">void</span> <span class="hljs-title function_">setCacheObject</span>(<span class="hljs-params">final <span class="hljs-built_in">String</span> key, final T value</span>)
{
redisTemplate.<span class="hljs-title function_">opsForValue</span>().<span class="hljs-title function_">set</span>(key, value);
}
<span class="hljs-comment">/**
* 缓存基本的对象,Integer、String、实体类等
*
* <span class="hljs-doctag">@param</span> key 缓存的键值
* <span class="hljs-doctag">@param</span> value 缓存的值
* <span class="hljs-doctag">@param</span> timeout 时间
* <span class="hljs-doctag">@param</span> timeUnit 时间颗粒度
*/</span>
<span class="hljs-keyword">public</span> <T> <span class="hljs-built_in">void</span> <span class="hljs-title function_">setCacheObject</span>(<span class="hljs-params">final <span class="hljs-built_in">String</span> key, final T value, final Integer timeout, final TimeUnit timeUnit</span>)
{
redisTemplate.<span class="hljs-title function_">opsForValue</span>().<span class="hljs-title function_">set</span>(key, value, timeout, timeUnit);
}
<span class="hljs-comment">/**
* 设置有效时间
*
* <span class="hljs-doctag">@param</span> key Redis键
* <span class="hljs-doctag">@param</span> timeout 超时时间
* <span class="hljs-doctag">@return</span> true=设置成功;false=设置失败
*/</span>
<span class="hljs-keyword">public</span> <span class="hljs-built_in">boolean</span> <span class="hljs-title function_">expire</span>(<span class="hljs-params">final <span class="hljs-built_in">String</span> key, final long timeout</span>)
{
<span class="hljs-keyword">return</span> <span class="hljs-title function_">expire</span>(key, timeout, <span class="hljs-title class_">TimeUnit</span>.<span class="hljs-property">SECONDS</span>);
}
<span class="hljs-comment">/**
* 设置有效时间
*
* <span class="hljs-doctag">@param</span> key Redis键
* <span class="hljs-doctag">@param</span> timeout 超时时间
* <span class="hljs-doctag">@param</span> unit 时间单位
* <span class="hljs-doctag">@return</span> true=设置成功;false=设置失败
*/</span>
<span class="hljs-keyword">public</span> <span class="hljs-built_in">boolean</span> <span class="hljs-title function_">expire</span>(<span class="hljs-params">final <span class="hljs-built_in">String</span> key, final long timeout, final TimeUnit unit</span>)
{
<span class="hljs-keyword">return</span> redisTemplate.<span class="hljs-title function_">expire</span>(key, timeout, unit);
}
<span class="hljs-comment">/**
* 获得缓存的基本对象。
*
* <span class="hljs-doctag">@param</span> key 缓存键值
* <span class="hljs-doctag">@return</span> 缓存键值对应的数据
*/</span>
<span class="hljs-keyword">public</span> <T> T <span class="hljs-title function_">getCacheObject</span>(<span class="hljs-params">final <span class="hljs-built_in">String</span> key</span>)
{
<span class="hljs-title class_">ValueOperations</span><<span class="hljs-title class_">String</span>, T> operation = redisTemplate.<span class="hljs-title function_">opsForValue</span>();
<span class="hljs-keyword">return</span> operation.<span class="hljs-title function_">get</span>(key);
}
<span class="hljs-comment">/**
* 删除单个对象
*
* <span class="hljs-doctag">@param</span> <span class="hljs-variable">key</span>
*/</span>
<span class="hljs-keyword">public</span> <span class="hljs-built_in">boolean</span> <span class="hljs-title function_">deleteObject</span>(<span class="hljs-params">final <span class="hljs-built_in">String</span> key</span>)
{
<span class="hljs-keyword">return</span> redisTemplate.<span class="hljs-title function_">delete</span>(key);
}
<span class="hljs-comment">/**
* 删除集合对象
*
* <span class="hljs-doctag">@param</span> collection 多个对象
* <span class="hljs-doctag">@return</span>
*/</span>
<span class="hljs-keyword">public</span> long <span class="hljs-title function_">deleteObject</span>(<span class="hljs-params">final Collection collection</span>)
{
<span class="hljs-keyword">return</span> redisTemplate.<span class="hljs-title function_">delete</span>(collection);
}
<span class="hljs-comment">/**
* 缓存List数据
*
* <span class="hljs-doctag">@param</span> key 缓存的键值
* <span class="hljs-doctag">@param</span> dataList 待缓存的List数据
* <span class="hljs-doctag">@return</span> 缓存的对象
*/</span>
<span class="hljs-keyword">public</span> <T> long <span class="hljs-title function_">setCacheList</span>(<span class="hljs-params">final <span class="hljs-built_in">String</span> key, final List<T> dataList</span>)
{
<span class="hljs-title class_">Long</span> count = redisTemplate.<span class="hljs-title function_">opsForList</span>().<span class="hljs-title function_">rightPushAll</span>(key, dataList);
<span class="hljs-keyword">return</span> count == <span class="hljs-literal">null</span> ? <span class="hljs-number">0</span> : count;
}
<span class="hljs-comment">/**
* 获得缓存的list对象
*
* <span class="hljs-doctag">@param</span> key 缓存的键值
* <span class="hljs-doctag">@return</span> 缓存键值对应的数据
*/</span>
<span class="hljs-keyword">public</span> <T> <span class="hljs-title class_">List</span><T> <span class="hljs-title function_">getCacheList</span>(<span class="hljs-params">final <span class="hljs-built_in">String</span> key</span>)
{
<span class="hljs-keyword">return</span> redisTemplate.<span class="hljs-title function_">opsForList</span>().<span class="hljs-title function_">range</span>(key, <span class="hljs-number">0</span>, -<span class="hljs-number">1</span>);
}
<span class="hljs-comment">/**
* 缓存Set
*
* <span class="hljs-doctag">@param</span> key 缓存键值
* <span class="hljs-doctag">@param</span> dataSet 缓存的数据
* <span class="hljs-doctag">@return</span> 缓存数据的对象
*/</span>
<span class="hljs-keyword">public</span> <T> <span class="hljs-title class_">BoundSetOperations</span><<span class="hljs-title class_">String</span>, T> <span class="hljs-title function_">setCacheSet</span>(<span class="hljs-params">final <span class="hljs-built_in">String</span> key, final <span class="hljs-built_in">Set</span><T> dataSet</span>)
{
<span class="hljs-title class_">BoundSetOperations</span><<span class="hljs-title class_">String</span>, T> setOperation = redisTemplate.<span class="hljs-title function_">boundSetOps</span>(key);
<span class="hljs-title class_">Iterator</span><T> it = dataSet.<span class="hljs-title function_">iterator</span>();
<span class="hljs-keyword">while</span> (it.<span class="hljs-title function_">hasNext</span>())
{
setOperation.<span class="hljs-title function_">add</span>(it.<span class="hljs-title function_">next</span>());
}
<span class="hljs-keyword">return</span> setOperation;
}
<span class="hljs-comment">/**
* 获得缓存的set
*
* <span class="hljs-doctag">@param</span> <span class="hljs-variable">key</span>
* <span class="hljs-doctag">@return</span>
*/</span>
<span class="hljs-keyword">public</span> <T> <span class="hljs-title class_">Set</span><T> <span class="hljs-title function_">getCacheSet</span>(<span class="hljs-params">final <span class="hljs-built_in">String</span> key</span>)
{
<span class="hljs-keyword">return</span> redisTemplate.<span class="hljs-title function_">opsForSet</span>().<span class="hljs-title function_">members</span>(key);
}
<span class="hljs-comment">/**
* 缓存Map
*
* <span class="hljs-doctag">@param</span> <span class="hljs-variable">key</span>
* <span class="hljs-doctag">@param</span> <span class="hljs-variable">dataMap</span>
*/</span>
<span class="hljs-keyword">public</span> <T> <span class="hljs-built_in">void</span> <span class="hljs-title function_">setCacheMap</span>(<span class="hljs-params">final <span class="hljs-built_in">String</span> key, final <span class="hljs-built_in">Map</span><<span class="hljs-built_in">String</span>, T> dataMap</span>)
{
<span class="hljs-keyword">if</span> (dataMap != <span class="hljs-literal">null</span>) {
redisTemplate.<span class="hljs-title function_">opsForHash</span>().<span class="hljs-title function_">putAll</span>(key, dataMap);
}
}
<span class="hljs-comment">/**
* 获得缓存的Map
*
* <span class="hljs-doctag">@param</span> <span class="hljs-variable">key</span>
* <span class="hljs-doctag">@return</span>
*/</span>
<span class="hljs-keyword">public</span> <T> <span class="hljs-title class_">Map</span><<span class="hljs-title class_">String</span>, T> <span class="hljs-title function_">getCacheMap</span>(<span class="hljs-params">final <span class="hljs-built_in">String</span> key</span>)
{
<span class="hljs-keyword">return</span> redisTemplate.<span class="hljs-title function_">opsForHash</span>().<span class="hljs-title function_">entries</span>(key);
}
<span class="hljs-comment">/**
* 往Hash中存入数据
*
* <span class="hljs-doctag">@param</span> key Redis键
* <span class="hljs-doctag">@param</span> hKey Hash键
* <span class="hljs-doctag">@param</span> value 值
*/</span>
<span class="hljs-keyword">public</span> <T> <span class="hljs-built_in">void</span> <span class="hljs-title function_">setCacheMapValue</span>(<span class="hljs-params">final <span class="hljs-built_in">String</span> key, final <span class="hljs-built_in">String</span> hKey, final T value</span>)
{
redisTemplate.<span class="hljs-title function_">opsForHash</span>().<span class="hljs-title function_">put</span>(key, hKey, value);
}
<span class="hljs-comment">/**
* 获取Hash中的数据
*
* <span class="hljs-doctag">@param</span> key Redis键
* <span class="hljs-doctag">@param</span> hKey Hash键
* <span class="hljs-doctag">@return</span> Hash中的对象
*/</span>
<span class="hljs-keyword">public</span> <T> T <span class="hljs-title function_">getCacheMapValue</span>(<span class="hljs-params">final <span class="hljs-built_in">String</span> key, final <span class="hljs-built_in">String</span> hKey</span>)
{
<span class="hljs-title class_">HashOperations</span><<span class="hljs-title class_">String</span>, <span class="hljs-title class_">String</span>, T> opsForHash = redisTemplate.<span class="hljs-title function_">opsForHash</span>();
<span class="hljs-keyword">return</span> opsForHash.<span class="hljs-title function_">get</span>(key, hKey);
}
<span class="hljs-comment">/**
* 删除Hash中的数据
*
* <span class="hljs-doctag">@param</span> <span class="hljs-variable">key</span>
* <span class="hljs-doctag">@param</span> <span class="hljs-variable">hkey</span>
*/</span>
<span class="hljs-keyword">public</span> <span class="hljs-built_in">void</span> <span class="hljs-title function_">delCacheMapValue</span>(<span class="hljs-params">final <span class="hljs-built_in">String</span> key, final <span class="hljs-built_in">String</span> hkey</span>)
{
<span class="hljs-title class_">HashOperations</span> hashOperations = redisTemplate.<span class="hljs-title function_">opsForHash</span>();
hashOperations.<span class="hljs-title function_">delete</span>(key, hkey);
}
<span class="hljs-comment">/**
* 获取多个Hash中的数据
*
* <span class="hljs-doctag">@param</span> key Redis键
* <span class="hljs-doctag">@param</span> hKeys Hash键集合
* <span class="hljs-doctag">@return</span> Hash对象集合
*/</span>
<span class="hljs-keyword">public</span> <T> <span class="hljs-title class_">List</span><T> <span class="hljs-title function_">getMultiCacheMapValue</span>(<span class="hljs-params">final <span class="hljs-built_in">String</span> key, final Collection<<span class="hljs-built_in">Object</span>> hKeys</span>)
{
<span class="hljs-keyword">return</span> redisTemplate.<span class="hljs-title function_">opsForHash</span>().<span class="hljs-title function_">multiGet</span>(key, hKeys);
}
<span class="hljs-comment">/**
* 获得缓存的基本对象列表
*
* <span class="hljs-doctag">@param</span> pattern 字符串前缀
* <span class="hljs-doctag">@return</span> 对象列表
*/</span>
<span class="hljs-keyword">public</span> <span class="hljs-title class_">Collection</span><<span class="hljs-title class_">String</span>> <span class="hljs-title function_">keys</span>(<span class="hljs-params">final <span class="hljs-built_in">String</span> pattern</span>)
{
<span class="hljs-keyword">return</span> redisTemplate.<span class="hljs-title function_">keys</span>(pattern);
}
}
通过 spring 配置开启 CacheManager,以及在需要进行缓存的 ServiceImpl 方法上,添加注解
- 修改启动类
// 开启缓存
@EnableCaching
- 在 service 上添加 @Cacheable 注解
@Cacheable 的常用属性以及 sqEL 编写 key 的元数据
-
Cacheable 的常用属性
-
cacheNames/value :用来指定缓存组件的名字
-
key :缓存数据时使用的 key,可以用它来指定。默认是使用方法参数的值。(这个 key 你可以使用 spEL 表达式来编写)
-
keyGenerator :key 的生成器。 key 和 keyGenerator 二选一使用
-
cacheManager :可以用来指定缓存管理器。从哪个缓存管理器里面获取缓存。
-
condition :可以用来指定符合条件的情况下才缓存
-
unless :否定缓存。当 unless 指定的条件为 true ,方法的返回值就不会被缓存。当然你也可以获取到结果进行判断。(通过 #result 获取方法结果)
-
sync :是否使用异步模式。
-
-
cacheNames
用来指定缓存组件的名字,将方法的返回结果放在哪个缓存中,可以是数组的方式,支持指定多个缓存。 -
key
缓存数据时使用的 key,默认使用的是方法参数的值。可以使用 spEL 表达式去编写。 -
keyGenerator(与 key 进行二选一)
key 的生成器,可以自己指定 key 的生成器,通过这个生成器来生成 key。 -
condition
符合条件的情况下才缓存 -
unless(刚好与 condition 相反)
否定缓存。当 unless 指定的条件为 true ,方法的返回值就不会被缓存。 -
sync
是否使用异步模式。默认是方法执行完,以同步的方式将方法返回的结果存在缓存中。 -
sqlEl 编写 key 时的元数据