Redis与SpringBoot整合
在 pom.xml 文件中引入 redis 相关依赖
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- spring2.X 集成 redis 所需 common-pool2-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
application.properties 配置 redis 配置
#Redis 服务器地址
spring.redis.host=192.168.111.100
#Redis 服务器连接端口
spring.redis.port=6379
#Redis 数据库索引(默认为 0)
spring.redis.database= 0
#连接超时时间(毫秒)
spring.redis.timeout=1800000
#连接池最大连接数(使用负值表示没有限制)
spring.redis.lettuce.pool.max-active=20
#最大阻塞等待时间(负数表示没限制)
spring.redis.lettuce.pool.max-wait=-1
#连接池中的最大空闲连接
spring.redis.lettuce.pool.max-idle=5
#连接池中的最小空闲连接
spring.redis.lettuce.pool.min-idle=0
添加 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;
@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;
@RestController
@RequestMapping("/redisTest")
public class RedisTestController {
@Autowired
private RedisTemplate redisTemplate;
@GetMapping
public String testRedis() {
//设置值到 redis
redisTemplate.opsForValue().set("name","lucy");
//从 redis 获取值
String name = (String)redisTemplate.opsForValue().get("name");
return name;
}
}
Redis事务
定义
Redis 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。
事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
Redis 事务的主要作用就是串联多个命令防止别的命令插队。
Multi、Exec、disacrd
从输入 Multi 命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入
Exec 后,Redis 会将之前的命令队列中的命令依次执行。
组队的过程中可以通过 discard 来放弃组队。
事务的错误处理
组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消
如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都 会执行,不会回滚。
事务冲突的问题
一个请求想给金额减 8000
一个请求想给金额减 5000
一个请求想给金额减 1000
悲观锁
悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人
会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会 block 直到它
拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读
锁,写锁等,都是在做操作之前先上锁。
乐观锁
乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人
不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新
这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞
吐量。Redis 就是利用这种 check-and-set 机制实现事务的。
WATCH key [key…]
在执行 multi 之前,先执行 watch key1 [key2],可以监视一个(或多个) key ,如果在事务 执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
unwatch
在执行 multi 之前,先执行 watch key1 [key2],可以监视一个(或多个) key ,如果在事务 执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
Redis事务三特性
单独的隔离操作
事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会 被其他客户端发送来的命令请求所打断。
没有隔离级别的概念
队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都 不会被实际执行
不保证原子性
事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚
Redis事务-秒杀案例
解决计数器和人员记录的事务操作
使用工具 ab 模拟测试
yum install httpd-tools
测试
vim postfile 模拟表单提交参数,以&符号结尾;存放当前目录。
内容:prodid=0101&
ab -n 1000 -c 100 -p ~/postfile -T application/x-www-form-urlencoded
http://192.168.111.100:8081/Seckill/doseckill
超卖
利用乐观锁淘汰用户,解决超卖问题
//增加乐观锁
jedis.watch(qtkey);
//3.判断库存
String qtkeystr = jedis.get(qtkey);
if(qtkeystr==null ||
"".equals(qtkeystr.trim())) {
System.out.println("未初始化库存");
jedis.close();
return false ;
}
int qt = Integer.parseInt(qtkeystr);
if(qt<=0) {
System.err.println("已经秒光");
jedis.close();
return false;
}
//增加乐观锁
jedis.watch(qtkey);
//3.判断库存
String qtkeystr = jedis.get(qtkey);
if(qtkeystr==null ||
"".equals(qtkeystr.trim())) {
System.out.println("未初始化库存");
jedis.close();
return false ;
}
int qt = Integer.parseInt(qtkeystr);
if(qt<=0) {
System.err.println("已经秒光");
jedis.close();
return false;
}
连接池
节省每次连接 redis 服务带来的消耗,把连接好的实例反复利用。 通过参数管理连接的行为
连接池参数
MaxTotal:控制一个 pool 可分配多少个 jedis 实例,通过 pool.getResource()来
获取;如果赋值为-1,则表示不限制;如果 pool 已经分配了 MaxTotal 个 jedis
实例,则此时 pool 的状态为 exhausted。
maxIdle:控制一个 pool 最多有多少个状态为 idle(空闲)的 jedis 实例;
MaxWaitMillis:表示当 borrow 一个 jedis 实例时,最大的等待毫秒数,如果超过等待
时间,则直接抛 JedisConnectionException;
testOnBorrow:获得一个 jedis 实例的时候是否检查连接可用性(ping());如
果为 true,则得到的 jedis 实例均是可用的;
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
jedisPool = new JedisPool(poolConfig, "192.168.44.168", 6379, 60000 );
}
}
}
return jedisPool;
}
public static void release(JedisPool jedisPool, Jedis jedis) {
if (null != jedis) {
jedisPool.returnResource(jedis);
}
}
}
解决库存遗留问题
LUA脚本
Lua 是一个小巧的脚本语言,Lua 脚本可以很容易的被 C/C++ 代码调用,也可以反过
来调用 C/C++的函数,Lua 并没有提供强大的库,一个完整的 Lua 解释器不过 200k,
所以 Lua 不适合作为开发独立应用程序的语言,而是作为嵌入式脚本语言。
很多应用程序、游戏使用 LUA 作为自己的嵌入式脚本语言,以此来实现可配置性、可
扩展性。
这其中包括魔兽争霸地图、魔兽世界、博德之门、愤怒的小鸟等众多游戏插件或外挂。
https://www.w3cschool.cn/lua/
LUA 脚本在 Redis 中的优势
- 将复杂的或者多步的 redis 操作,写为一个脚本,一次提交给 redis 执行,减少反复连 接 redis 的次数。提升性能。
- LUA 脚本是类似 redis 事务,有一定的原子性,不会被其他命令插队,可以完成一些 redis 事务性的操作。
- 但是注意 redis 的 lua 脚本功能,只有在 Redis 2.6 以上的版本才可以使用。 利用 lua 脚本淘汰用户,解决超卖问题。
- redis 2.6 版本以后,通过 lua 脚本解决争抢问题,实际上是 redis 利用其单线程的特性, 用任务队列的方式解决多任务并发问题
第一版:简单版
第二版:加事务-乐观锁(解决超卖),但出现遗留
库存和连接超时
第三版:连接池解决超时问题
第四版:解决库存依赖问题,LUA 脚本
Redis持久化
官网介绍 http://www.redis.io
Redis 提供了 2 个不同形式的持久化方式。
- RDB(Redis DataBase)
- AOF(Append Of File)
RDB
在指定的时间间隔内将内存中的数据集快照写入磁盘, 也就是行话讲的 Snapshot 快 照,它恢复时是将快照文件直接读到内存里
备份是如何执行的
Redis 会单独创建(fork)一个子进程来进行持久化,会先将数据写入到 一个临时文件
中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。 整个过程
中,主进程是不进行任何 IO 操作的,这就确保了极高的性能 如果需要进行大规模数
据的恢复,且对于数据恢复的完整性不是非常敏感,那 RDB 方式要比 AOF 方式更加
的高效。RDB 的缺点是最后一次持久化后的数据可能丢失。
Fork
- Fork 的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等)数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程
- 在 Linux 程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多 会 exec 系统调用,出于效率考虑,Linux 中引入了“写时复制技术”
- 一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容要 发生变化时,才会将父进程的内容复制一份给子进程。
RDB持久化流程
dump.rdb 文件
在 redis.conf 中配置文件名称,默认为 dump.rdb
配置位置
rdb 文件的保存路径,也可以修改。默认为 Redis 启动时命令行所在的目录下 dir “/myredis/”
如何触发 RDB 快照;保持策略
配置文件中默认的快照配置
命令 save VS bgsave
save :save 时只管保存,其它不管,全部阻塞。手动保存。不建议。
bgsave:Redis 会在后台异步进行快照操作, 快照同时还可以响应客户端请求。
可以通过 lastsave 命令获取最后一次成功执行快照的时间
flushall 命令
执行 flushall 命令,也会产生 dump.rdb 文件,但里面是空的,无意义
###SNAPSHOTTING 快照###
save
格式:save 秒钟 写操作次数
RDB 是整个内存的压缩过的 Snapshot,RDB 的数据结构,可以配置复合的快照触发条
件,
默认是 1 分钟内改了 1 万次,或 5 分钟内改了 10 次,或 15 分钟内改了 1 次。
禁用
不设置 save 指令,或者给 save 传入空字符串
stop-writes-on-bgsave-error
当 Redis 无法写入磁盘的话,直接关掉 Redis 的写操作。推荐 yes
rdbcompression 压缩文件
对于存储到磁盘中的快照,可以设置是否进行压缩存储。如果是的话,redis 会采用
LZF 算法进行压缩。
如果你不想消耗 CPU 来进行压缩的话,可以设置为关闭此功能。推荐 yes
rdbchecksum 检查完整性
在存储快照后,还可以让 redis 使用 CRC64 算法来进行数据校验, 但是这样做会增加大约 10%的性能消耗,如果希望获取到最大的性能提升,可以关 闭此功能 推荐 yes
rdb 的备份
先通过 config get dir 查询 rdb 文件的目录
将*.rdb 的文件拷贝到别的地方
rdb 的恢复
关闭 Redis
先把备份的文件拷贝到工作目录下 cp dump2.rdb dump.rdb
启动 Redis, 备份数据会直接加载
适合大规模的数据恢复
对数据完整性和一致性要求不高更适合使用
节省磁盘空间
恢复速度快
劣势
Fork 的时候,内存中的数据被克隆了一份,大致 2 倍的膨胀性需要考虑
虽然 Redis 在 fork 时使用了写时拷贝技术,但是如果数据庞大时还是比较消
耗性能。
在备份周期在一定间隔时间做一次备份,所以如果 Redis 意外 down 掉的话,
就会丢失最后一次快照后的所有修改。
如何停止
动态停止 RDB:redis-cli config set save ""#save 后给空值,表示禁用保存策略
总结
RDB是一个非常紧凑的文件RDB在保存RDB文件时父进程唯一需要做的就是fork出一个子进程,接下来的工作全部由子进程来做,父进程不需要再做其他IO操作,所以RDB持久化方式可以最大化redis的性能
与AOF相比,在恢复大的数据集的时候,RDB方式会更快一些
数据去失风险大
RDB需要经常fork子进程来保存数据集到硬盘上,当数据集比较大的时候,fork的过程是非常耗时的,可能会导致Redis在一些亳秒级不能相应客户端请求
AOF
以日志的形式来记录每个写操作(增量保存),将 Redis 执行过的所有写指令记录下
来(读操作不记录), 只许追加文件但不可以改写文件,redis 启动之初会读取该文件重
新构建数据,换言之,redis 重启的话就根据日志文件的内容将写指令从前到后执行一
次以完成数据的恢复工作
AOF 持久化流程
(1)客户端的请求写命令会被 append 追加到 AOF 缓冲区内;
(2)AOF 缓冲区根据 AOF 持久化策略[always,everysec,no]将操作 sync 同步到磁盘的
AOF 文件中;
(3)AOF 文件大小超过重写策略或手动重写时,会对 AOF 文件 rewrite 重写,压缩
AOF 文件容量;
(4)Redis 服务重启时,会重新 load 加载 AOF 文件中的写操作达到数据恢复的目的;
AOF 默认不开启
可以在 redis.conf 中配置文件名称,默认为 appendonly.aof
AOF 文件的保存路径,同 RDB 的路径一致。
AOF 和 RDB 同时开启,redis 听谁的?
AOF 和 RDB 同时开启,系统默认取 AOF 的数据(数据不会存在丢失)
AOF 启动/修复/恢复
AOF 的备份机制和性能虽然和 RDB 不同, 但是备份和恢复的操作同 RDB 一样,都
是拷贝备份文件,需要恢复时再拷贝到 Redis 工作目录下,启动系统即加载。
正常恢复
修改默认的 appendonly no,改为 yes
将有数据的 aof 文件复制一份保存到对应目录(查看目录:config get dir)
恢复:重启 redis 然后重新加载
异常恢复
修改默认的 appendonly no,改为 yes
如遇到 AOF 文件损坏,通过/usr/local/bin/redis-check-aof--fix
appendonly.aof 进行恢复
备份被写坏的 AOF 文件
恢复:重启 redis,然后重新加载
AOF 同步频率设置
appendfsync always
始终同步,每次 Redis 的写入都会立刻记入日志;性能较差但数据完整性比较好
appendfsync everysec
每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失。
appendfsync no
redis 不主动进行同步,把同步时机交给操作系统。
Rewrite 压缩
- 是什么:
AOF 采用文件追加方式,文件会越来越大为避免出现此种情况,新增了重写机制, 当
AOF 文件的大小超过所设定的阈值时,Redis 就会启动 AOF 文件的内容压缩, 只保留
可以恢复数据的最小指令集.可以使用命令 bgrewriteaof - 重写原理,如何实现重写
AOF 文件持续增长而过大时,会 fork 出一条新进程来将文件重写(也是先写临时文件最
后再 rename),redis4.0 版本后的重写,是指上就是把 rdb 的快照,以二级制的形式附
在新的 aof 头部,作为已有的历史数据,替换掉原来的流水账操作。
no-appendfsync-on-rewrite:
如果 no-appendfsync-on-rewrite=yes ,不写入 aof 文件只写入缓存,用户请求不会阻
塞,但是在这段时间如果宕机会丢失这段时间的缓存数据。(降低数据安全性,提高
性能)
如果 no-appendfsync-on-rewrite=no, 还是会把数据往磁盘里刷,但是遇到重
写操作,可能会发生阻塞。(数据安全,但是性能降低)
触发机制,何时重写
Redis 会记录上次重写时的 AOF 大小,默认配置是当 AOF 文件大小是上次 rewrite 后大
小的一倍且文件大于 64M 时触发
重写虽然可以节约大量磁盘空间,减少恢复时间。但是每次重写还是有一定的负担的,
因此设定 Redis 要满足一定条件才会进行重写。
auto-aof-rewrite-percentage:设置重写的基准值,文件达到 100%时开始重写(文件
是原来重写后文件的 2 倍时触发)
auto-aof-rewrite-min-size:设置重写的基准值,最小文件 64MB。达到这个值开始重
写。
例如:文件达到 70MB 开始重写,降到 50MB,下次什么时候开始重写?100MB
系统载入时或者上次重写完毕时,Redis 会记录此时 AOF 大小,设为 base_size,
如果 Redis 的 AOF 当前大小>= base_size +base_size*100% (默认)且当前大
小>=64mb(默认)的情况下,Redis 会对 AOF 进行重写。 - 重写流程
(1)bgrewriteaof 触发重写,判断是否当前有 bgsave 或 bgrewriteaof 在运行,如果
有,则等待该命令结束后再继续执行。
(2)主进程 fork 出子进程执行重写操作,保证主进程不会阻塞。
(3)子进程遍历 redis 内存中数据到临时文件,客户端的写请求同时写入 aof_buf 缓
冲区和 aof_rewrite_buf 重写缓冲区保证原 AOF 文件完整以及新 AOF 文件生成期间的
新的数据修改动作不会丢失。
(4)子进程写完新的 AOF 文件后,向主进程发信号,父进程更新统计信息。2).主
进程把 aof_rewrite_buf 中的数据写入到新的 AOF 文件。
(5)使用新的 AOF 文件覆盖旧的 AOF 文件,完成 AOF 重写。
优势
备份机制更稳健,丢失数据概率更低。
可读的日志文本,通过操作 AOF 稳健,可以处理误操作。
劣势
比起 RDB 占用更多的磁盘空间。
恢复备份速度要慢。
每次读写都同步的话,有一定的性能压力。
存在个别 Bug,造成恢复不能。
总结
AOF文件时一个只进行追加的日志文件
Redis可以在AOF文件体积变得过大时,自动地在后台对AOF进行重写
AOF文件有序地保存了对数据库执行的所有写入操作,这些写入操作以 Redis协议的格式保存,因此AOF文件的内容非常容易被人读懂,对文件进行分析也很轻松
对于相同的数据集来说,AOF文件的体积通常要大于RDB文件的体积
根据所使用的fsync策略,AOF的速度可能会慢于RDB
总结
官方推荐两个都启用。
- 如果对数据不敏感,可以选单独用 RDB。
- 不建议单独用 AOF,因为可能会出现 Bug。
- 如果只是做纯内存缓存,可以都不用。
RDB 持久化方式能够在指定的时间间隔能对你的数据进行快照存储
AOF 持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些
命令来恢复原始的数据,AOF 命令以 redis 协议追加保存每次写的操作到文件末尾.
Redis 还能对 AOF 文件进行后台重写,使得 AOF 文件的体积不至于过大
只做缓存:如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何
持久化方式.
同时开启两种持久化方式
在这种情况下,当 redis 重启的时候会优先载入 AOF 文件来恢复原始的数据, 因为在
通常情况下 AOF 文件保存的数据集要比 RDB 文件保存的数据集要完整.
RDB 的数据不实时,同时使用两者时服务器重启也只会找 AOF 文件。那要不要只
使用 AOF 呢?
建议不要,因为 RDB 更适合用于备份数据库(AOF 在不断变化不好备份), 快速重
启,而且不会有 AOF 可能潜在的 bug,留着作为一个万一的手段。
为 RDB 文件只用作后备用途,建议只在 Slave 上持久化 RDB 文件,而且只要 15
分钟备份一次就够了,只保留 save 900 1 这条规则。
如果使用 AOF,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单
只 load 自己的 AOF 文件就可以了。
代价,一是带来了持续的 IO,二是 AOF rewrite 的最后将 rewrite 过程中产生的新数据
写到新文件造成的阻塞几乎是不可避免的。
只要硬盘许可,应该尽量减少 AOF rewrite 的频率,AOF 重写的基础大小默认值 64M
太小了,可以设到 5G 以上。
默认超过原大小 100%大小时重写可以改到适当的数值。