Redis
之内存淘汰、过期机制和事务操作
一、内存淘汰策略
1.1. Redis
一共有六种淘汰机制:
-
noeviction:
当内存使用达到阈值时候,所有引用申请内存的命令都会报错 -
allkeys-lru:
在主键空间中,优先移除最近未使用的key(推荐) -
volatile-lru:
在设置了过期时间的键空间中,优先移除最近未使用的key
-
allkeys-random:
在主键空间中,随机移除某一个key
-
volatile-random:
在设置了过期时间的键空间中,随机移除一个key
-
volatile-ttl:
在设置了过期时间的键空间中,具有更早过期时间的key
优先移除
1.2. 如何设置淘汰策略
设置Redis
的内存大小限制, 当数据达到限定的大小之后,会选择配置的淘汰策略数据
# maxmemory <bytes>
maxmemory 100mb
配置Redis
淘汰策略
# The default is:
#
maxmemory-policy allkeys-lru
二、过期策略
2.1. Redis
过期的命令
expire <key> <TTL> # 将键的生存时间设置为ttl秒
pexpire <key> <TTL> # 将键的生存时间设置为ttl毫秒
expireat <key> <timestamp> # 将键的过期时间设为timestamp所指定的秒时间戳
pexpireat <key> <timestamp> # 将键的过期时间设为timestamp所指定的毫秒时间戳
ttl <key> # 返回剩余生存时间ttl秒
pttl <key> # 返回剩余生存时间pttl毫秒
persist <key> # 移除一个键的过期时间
Redis
是使用定期删除+惰性删除两者配合的过期策略。
2.2. 定期删除
定期删除指的是Redis
默认每隔100ms
就随机抽取一些设置了过期时间的key
,检测这些key
是否过期,如果过期了就将其删掉。
因为key
太多,如果全盘扫描所有的key会非常耗性能,所以是随机抽取一些key
来删除。这样就有可能删除不完,需要惰性删除配合。
2.3. 惰性删除
惰性删除不再是Redis
去主动删除,而是在客户端要获取某个key
的时候,Redis
会先去检测一下这个key
是否已经过期,如果没有过期则返回给客户端,如果已经过期了,那么Redis
会删除这个key
,不会返回给客户端。
所以惰性删除可以解决一些过期了,但没被定期删除随机抽取到的key
。但有些过期的key
既没有被随机抽取,也没有被客户端访问,就会一直保留在数据库,占用内存,长期下去可能会导致内存耗尽。所以Redis
提供了内存淘汰机制来解决这个问题。
2.4. Redis
过期通知机制
要开启Redis
过期通知需要修改配置文件:redis.conf
,当我们的key失效时,可以执行我们的客户端回调监听的方法。具体配置如下:
############################# EVENT NOTIFICATION ##############################
# Redis可以通知发布/订阅客户端有关密钥空间中发生的事件。
# This feature is documented at http://redis.io/topics/notifications
#
# 例如,如果启用了键空间事件通知,
# 并且客户端对存储在数据库0中的键“ foo”执行了DEL操作,则将通过Pub / Sub发布两条消息:
#
# PUBLISH __keyspace@0__:foo del
# PUBLISH __keyevent@0__:del foo
#
# 可以在一组类中选择Redis将通知的事件。每个类都由一个字符标识:
#
# K keyspace事件,事件以__keyspace@<db>__为前缀进行发布
# E keyevent事件,事件以__keyevent@<db>__为前缀进行发布
# g 一般性的,非特定类型的命令,比如del,expire,rename等
# $ 字符串特定命令
# l 列表特定命令
# s 集合特定命令
# h 哈希特定命令
# z 有序集合特定命令
# x 过期事件,当某个键过期并删除时会产生该事件
# e 驱逐事件,当某个键因maxmemore策略而被删除时,产生该事件
# A g$lshzxe的别名,因此”AKE”意味着所有事件
#
# “notify-keyspace-events”将由零个或多个字符组成的字符串作为参数。
# 空字符串表示已禁用通知
#
# Example: to enable list and generic events, from the point of view of the
# event name, use:
#
# notify-keyspace-events Elg
#
# Example 2: to get the stream of the expired keys subscribing to channel
# name __keyevent@0__:expired use:
#
# notify-keyspace-events Ex
#
# 默认情况下,所有通知都被禁用,因为大多数用户不需要此功能,并且该功能有一些开销。
# 请注意,如果您未指定K或E中的至少一个,则不会传递任何事件。
notify-keyspace-events Ex
重启Redis
之后,我们测试一下:
127.0.0.1:6379> psubscribe __keyevent@0__:expired # 开启失效监听
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "__keyevent@0__:expired"
3) (integer) 1
开启另一个Redis
客户端:
127.0.0.1:6379> setex age 5 19
OK
127.0.0.1:6379>
五秒之后开启监听的客户端就会出现我们刚才设置的key
127.0.0.1:6379> psubscribe __keyevent@0__:expired
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "__keyevent@0__:expired"
3) (integer) 1
1) "pmessage"
2) "__keyevent@0__:expired"
3) "__keyevent@0__:expired"
4) "age" # 刚才的age
2.5. Springboot
整合Redis
过期监听
需求: 处理订单过期自动取消,比如30分钟未支付自动更新订单状态。
引入依赖
<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.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
增加Redis
的监听配置类
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
/**
* @author 墨龙吟
* @version 1.0.0
* @ClassName RedisConfig.java
* @Description Redis失效监听器
* @createTime 2019年12月07日 - 12:51
*/
@Configuration
public class RedisListenerConfig {
@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer listenerContainer = new RedisMessageListenerContainer();
listenerContainer.setConnectionFactory(connectionFactory);
return listenerContainer;
}
}
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.listener.KeyExpirationEventMessageListener;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.stereotype.Component;
/**
* @author 墨龙吟
* @version 1.0.0
* @ClassName RedisKeyExpirationListener.java
* @Description 具体的监听类
* @createTime 2019年12月07日 - 12:58
*/
@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}
@Override
public void onMessage(Message message, byte[] pattern) {
// 里面就可处理自己的业务了, message.toString()可以获取失效的key
String expiredKey= message.toString();
System.out.println("该key :expiraKey:" + expiredKey + "失效啦~");
// 如果符合我们定义的前缀的话,就开始处理数据
if (expiredKey.startsWith("order:")) {
System.out.println("拿到key为:"+ expiredKey +" ==> 开始处理业务");
}
}
}
添加一个控制器使用:
@RestController
public class RedisController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
// 使用这个需要注意,redisTemplate,这个要是用@Resource注入
// @Resource
// private RedisTemplate<String, Object> redisTemplate;
@GetMapping("/set_key")
public String setKey() {
stringRedisTemplate.opsForValue().set("order:name", UUID.randomUUID().toString(), 5L, TimeUnit.SECONDS);
System.out.println("设置的key");
return "success";
}
}
结果:

注意 :针对这样的业务,我们也可以使用Spring
+ quartz
定时任务,下单成功后,生成一个30分钟后运行的任务,30分钟后检查订单状态,如果未支付,则进行处理。
2.6. 缺点与改进
2.6.1 缺点:
Redis key
的失效通知机制是基于其pub/sub
模式的;这个模式有个致命的缺陷是,消息通知不能持久化,假如监听服务宕机期间,有key
过期,那么这个失效通知就被忽略了。这样的场景,j就会出现丢失通知的情况,无法及时处理业务。
2.6.2 改进:
应当使用RabbitMq
,超时自动取消订单使用RabbitMq
的死信队列,接收死信队列更新订单状态即可。
三、事务操作
事务是必须满足4个条件(ACID
)::原子性(Atomicity
,或称不可分割性)、一致性(Consistency
)、隔离性(Isolation
,又称独立性)、持久性(Durability
)。
3.1. Redis
事务的基本操作
ping
:命令客户端向 Redis
服务器发送一个 PING
,如果服务器运作正常的话,会返回一个 PONG
,通常用于测试与服务器的连接是否仍然生效,或者用于测量延迟值。
127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379> incr user_id # 将user_id(默认为0)值加一
QUEUED
127.0.0.1:6379> incr user_id
QUEUED
127.0.0.1:6379> incr user_id
QUEUED
127.0.0.1:6379> ping
QUEUED
127.0.0.1:6379> exec # 提交事务
1) (integer) 1
2) (integer) 2
3) (integer) 3
4) PONG
127.0.0.1:6379>
注意,如果不加watch
,假如有另外客服端将user_id
改为100,那么最终exec
后,user_id
值为103。
使用watch
监视key
:
127.0.0.1:6379> watch name age # 监视 name, age 两个key
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set name tom
QUEUED
127.0.0.1:6379> incr age
QUEUED
127.0.0.1:6379> exec
1) OK
2) (integer) 1
127.0.0.1:6379>
watch
监视key
,且事务被打断:
# 第一个客户端
127.0.0.1:6379> watch java java_version # 第一个Redis客户端监视 这两个key
OK
127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379> set java web
QUEUED
# 第二个客户端
127.0.0.1:6379> set java_version 1.8
OK
127.0.0.1:6379>
# 返回第一个客户端提交事务
127.0.0.1:6379> exec
(nil) # 失败
127.0.0.1:6379>
unwatch
取消监视key
:
在执行 watch
命令之后, exec
命令或 discard
命令先被执行了的话,那么就不需要再执行 unwatch
了。
127.0.0.1:6379> watch java java_version
OK
127.0.0.1:6379> unwatch
OK
127.0.0.1:6379>
discard
取消事务:放弃执行事务块内的所有命令。
127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379> ping
QUEUED
127.0.0.1:6379> set name 123456
QUEUED
127.0.0.1:6379> discard # 取消事务
OK
127.0.0.1:6379> exec # 提交事务失败
(error) ERR EXEC without MULTI
127.0.0.1:6379>
事务内命令发生语法错误,整个事务命令都不执行:
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incr age
QUEUED
127.0.0.1:6379> set email # 错误命令
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:6379> exec # 提交事务失败
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379>
事务内,命令格式语法正确,但是执行出错,其他命令正常,不会回滚:
127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379> incr age # age加一
QUEUED
127.0.0.1:6379> get age # 获取age
QUEUED
127.0.0.1:6379> set name tom # 为name赋值为tom
QUEUED
127.0.0.1:6379> incr name # name 加一
QUEUED
127.0.0.1:6379> get name # 获取name值
QUEUED
127.0.0.1:6379> exec # 提交事务
1) (integer) 2
2) "2"
3) OK
4) (error) ERR value is not an integer or out of range
5) "tom"
127.0.0.1:6379>
3.2. Redis
事务和MySQL
事务的区别
第一个:
MySQL
用BEGIN
、ROLLBACK
、COMMIT
,显式开启并控制一个新的Transaction
。事务是默认开启的。MySQL
主要是通过乐观锁和悲观锁进行数据库事务的并发控制。
Redis
是用MULTI
、EXEC
、DISCARD
,显式开启并控制一个Transaction
。Redis
中是通过watch
加乐观锁对数据库进行并发控制。
第二个:
MySQL
实现事务,是基于UNDO/REDO
日志。UNDO
日志记录修改前的状态,ROLLBACK
基于UNDO
日志实现;ERDO
记录修改之后的状态,COMMIT
基于ERDO
日志实现。
Redis
实现事务,是基于COMMANDS
队列,如果没有开启事务,COMMAND
会立即返回执行结果,并直接写入磁盘;如果事务开启,COMMAND
不会被立即执行,而是排入队列并返回排队状态。调用EXEC
才会执行COMMANDS
队列。
3.3. Redis
如何保证事务安全
Redis
本身就是单线程的能够保证线程安全问题。
四、悲观锁、乐观锁和watch
解释
- 悲观锁:
悲观锁(Pessimistic Lock
),每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block
直到它拿到锁。
传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。 - 乐观锁:
乐观锁(Optimistic Lock
),就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。
乐观锁适用于多读的应用类型,这样可以提高吞吐量。
乐观锁策略:提交版本必须大于记录当前版本才能执行更新。 -
watch
:
watch
指令,类似乐观锁,事务提交时,如果Key
的值已被别的客户端改变。比如某个list
已被别的客户端push/pop
过了,整个事务队列都不会被执行。
通过watch
命令在事务执行之前监控了多个keys
,倘若在watch
之后有任何key
的值发生了变化,exec
命令执行的事务都将被放弃,同时返回Nullmulti-bulk
应答以通知调用者事务执行失败。
一旦执行了exec/unwatch/discard
之前加的监控锁都会被取消掉了。
五、参考文章