​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";
}

}

结果:

Redis之淘汰策略、过期机制和事务控制_Redis事务

注意 :针对这样的业务,我们也可以使用​​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​​之前加的监控锁都会被取消掉了。

五、参考文章