黄埔班第32节:分布式缓存Redis应用实战-2

2020/02/08

1、Redis的事务

场景描述:我们的数据操作是一个原子性的

# 看一下redis的场景
# client A           		client B
set name icoding(1)     set name icodingedu(2)
get name(3)             get name
# 这个时候数据就串了

事务需要边界

multi # 开启事务
set name icoding  
get name # 命令没执行先放到队列中
exec # 执行事务
discard # 回滚事务

事务执行过程中错误回退机制

  • 如果命令错误则自动回滚

  • 如果命令正确但语法错误,正确的语句都会被执行

    multi
    set name icoding
    lpush name 1 2 3 
    exec
    

    第一个命令会执行,第二个命令语法错误没被执行

2、Redis的乐观锁

业务场景举例

库存:store 1000–>100 / + 1000(三个业务人员incrby)

数据的状态:如果在你修改前已经被修改了,就不能再修改成功了?

# watch key1 key2 命令
# 只要在事务外watch了一个key或多个key,在事务还没有执行完毕的时候,watch其中的key被修改后,整个事务回滚
set store 10
watch store
multi
incrby store 100
get store
exec
  • watch在事务执行完毕后就释放了
  • watch的key如果在事务执行过程中失效了,事务也不受watch影响
  • watch只能在事务外执行
  • 客户端如果断开,watch也就失效了

3、内部事件订阅机制

发布与订阅

有点像消息,但这个订阅是阻塞式的,我要一直等,相当于同步

subscribe java php c #订阅,要早于发布之前阻塞执行
publish java springboot #发布消息
psubscribe java* #订阅统配频道

keyspace notifcations

业务场景:

订单超时2小时未支付,需要关闭,如何实现?

1、Quartz来做任务调度,定时执行巡检任务,5分钟巡检一次,会有4分59秒的巡检超时(被动)

2、Timer,java的定时器,秒为单位,数据量大的时候性能就成瓶颈了(被动)

3、Quartz+Timer ,Quartz拿出5分钟内将要充实的订单,然后在启用多线程以Timer每秒的方式去检查,但业务功能就比较复杂了(被动)

4、有没有一种功能,有个hook能通知我们我失效了?通知我执行业务(主动)

5、假设:expire 7200 秒过期后通知我,这就是实现了第4种方案了?

Redis在2.8版本后,推出keyspace notifcations特性,类似数据库的trigger触发器。

相当于key操作的事件通知,它是基于sub/pub发布订阅机制,可以接收对数据库中影响key操作的所有事件,如del、set、expire(过期时间)。

接受的事件类型

  • keyspace : 是key触发的事件的具体操作
  • keyevent : 是事件影响的键名
# pub这个动作是系统自动发布的,pop的原理机制
127.0.0.1:6379> del mykey
# 数据库0会发布以下两个信息
publish __keyspace@0__:mykey del
publish __keyevent@0__:del mykey

开启系统通知

redis.conf配置文件里

# 这都是配置项
#  K     Keyspace events, published with __keyspace@<db>__ prefix.
					keyspace事件,以__keyspace@<db>__为前缀发布
#  E     Keyevent events, published with __keyevent@<db>__ prefix.
					keyevent事件,以__keyevent@<db>__为前缀发布
#  g     Generic commands (non-type specific) like DEL, EXPIRE, RENAME, ...
					一般性的非特定类型的命令,如del,expire,rename
#  $     String commands 字符串特定命令
#  l     List commands	列表特定命令
#  s     Set commands	集合特定命令
#  h     Hash commands	哈希特定命令
#  z     Sorted set commands	zset有序集合特定命令
#  x     Expired events (events generated every time a key expires)
					过期事件,当某个键过期并删除时会产生该事件
#  e     Evicted events (events generated when a key is evicted for maxmemory)
					驱逐事件,当某个键因maxmemory策略被删除时,产生该事件
#  A     Alias for g$lshzxe, so that the "AKE" string means all the events.
					g$lshzxe的别名,“AKE”意味着所有事件
notify-keyspace-events "" #默认空字符串,是关闭状态
notify-keyspace-events "KEx" #配置文件里只配置了space和event的expried事件,就只自动发布这个事件

配置订阅

notify-keyspace-events "KEA"
set username gavin ex 10 #set事件,expire事件
psubscribe __key*@*__:*
#返回事件通知
4) "set"
1) "pmessage"
2) "__key*@*__:*"
3) "__keyevent@0__:set"
4) "username"
1) "pmessage"
2) "__key*@*__:*"
3) "__keyspace@0__:username"
4) "expire"
1) "pmessage"
2) "__key*@*__:*"
3) "__keyevent@0__:expire"
4) "username"

1) "pmessage"
2) "__key*@*__:*"
3) "__keyspace@0__:username"
4) "expired"
1) "pmessage"
2) "__key*@*__:*"
3) "__keyevent@0__:expired"
4) "username"

#只订阅过期事件
subscribe __keyevent@0__:expired

Springboot订阅通知

pom.xml导入依赖

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

业务场景:处理订单过期自动取消,比如下单30分钟未支付自动更改订单状态

  • 解决方案一

    1、开启redis的过期提醒,配置文件设置

    notify-keyspace-events "Ex"
    # 修改保存后redis需要重启
    

    2、定义配置类RedisListenerConfig

    @Configuration
    public class RedisListenerConfig {
        @Bean
        RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory{
         RedisMessageListenerContainer container = new RedisMessageListenerContainer();
         container.setConnectionFactory(connectionFactory);
            //container.addMessageListener(new RedisExpiredListener(), new PatternTopic("__keyevent@0__:expired"));
            return container;
        }
    }
    

    3、定义监听器RedisKeyExpirationListener 实现KeyExpirationEventMessageListener接口,查看源码实现,该接口监听所有db的过期事件keyevent@*:expired

    @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();
            if(expiredKey.startsWith("zeus:order")){
                //TODO
            }
        }
    }
    
  • 解决方案二

    使用springboot+quartz任务,任务信息写入mysql,quartz要集群分布式部署,多节点执行任务,下单成功后,生成一个30分钟后运行的任务:检查订单,如果未支付,进行处理。

4、Redis的持久化

  • RDB相当于MySQL的定时备份

  • AOF相当于MySQL的增量备份

RDB模式

1、什么是RDB

每隔一段时间,把内存中的数据写入磁盘,恢复的时候,他会自动从工作区拿出来进行恢复

2、RDB的优劣势

优势

  • 每隔一段时间,全量备份
  • 备份简单,可以直接传输文件到其他地方
  • 备份的过程中会fork一个新的进程来进行文件的存储

劣势

  • 发生故障时,会丢失上次备份到当前时间的数据(丢失最后一次备份和系统宕机之间的数据)
  • fork的进程会和父进程一摸一样,会导致内存随时膨胀两倍(可能会导致redis某个时间点会有点卡,当你去看的时候就好了)

配置文件redis.conf

save 900 1 #900秒内变更1次才触发bgsave
save 300 10 #300秒内变更10次触发bgsave
save 60 10000
# 如果不想开启RDB,就是配置成 save ""
dbfilename dump.rdb #rdb保存的文件名
dir ./ #就是存放我们RDB备份文件的目录
#yes:如果save过程出错了则停止Redis写操作
#no:没所谓save是否出错
stop-writes-on-bgsave-error yes
#开启RDB压缩
rdbcompression yes
#进行CRC64算法校验,有10%的性能损耗
rdbchecksum yes

手动备份RDB

手动触发有两个命令

  • save命令

    在执行的时候会阻塞Redis的服务,直到RDB文件完成才会释放我们Redis进程恢复读写操作

  • bgsave命令

    执行bgsave的时候会在后台fork一个进程进行RDB的生成,不影响主进程的业务操作

使用RDB恢复数据

只需要将dump.rdb移动到我们redis.conf配置的dir(dir ./)目录下,就会在Redis启动的时候自动加载数据到内存,但是Redis在加载RDB过程中是阻塞的,直到加载完毕才能恢复操作

redis-cli> config get dir # 获取dir的配置信息

AOF模式

RDB会丢失最后一次备份和系统宕机之间的数据,可能你觉得无所谓,所以就需要增量备份的过程了,什么Redis的增量备份,就是AOF,有点像MySQL的Binlog。

特点

  • 以日志形式来记录用户的请求和写操作,读操作不会记录
  • 文件是追加的形式而不是修改的形式
  • redis的aof恢复其实就是从头到尾执行一遍

优势

  • AOF更加耐用,可以以秒为单位进行备份,如果发生问题,只丢失一秒的数据
  • 以log日志的方式进行存放和追加数据,如果磁盘已经很满了,会执行redis-check-aof工具
  • 当aof文件太大的时候,redis在后台会自动重写aof,相当于把redis的aof文件进行压缩
  • AOF日志包含所有写操作,便于redis的恢复

劣势

  • 相同的数据,AOF比RDB大(因为AOF记录写操作,RDB记录数据本身,同一个可能被写多次)
  • AOF比RDB同步慢,恢复比RDB慢
  • 一旦AOF文件出现问题,数据就会不完整

redis.conf配置文件

#开启AOF
appendonly yes 
#AOF的文件名设置
appendfilename "appendonly.aof"
# aof同步备份的频率设置
# no : 写入aof文件,不等待磁盘同步
# everysec :每秒讲写操作备份,推荐使用
# always :每次操作都会备份,数据是安全和完成,但性能会比较差
appendfsync everysec
# 重写的时候是否要同步,yes则不同步,no同步阻塞可以保证数据安全(重写的时候redis不可以进行写操作,保证数据的不丢失)
no-appendfsync-on-rewrite no
# 重写机制(压缩机制):避免文件越来越大,将key的重复值合并(同一个key只保留最后一次写操作),重写的时候会触发fork一个新进程来操作,与RDB的fork进程区别是前者复制写操作,后者复制数据本身
# 重写触发条件就是下面两个都满足才触发
# 上面配置no,重写触发后会fork进程并阻塞主进程无法写入导致等待,所以两个值可以设置小点让重写快速阻塞完毕
# 1.现有的文件比上次多出100%:上次压缩完2G,现在已经4G了如果是50%就是3G
auto-aof-rewrite-percentage 100
# 2.现有的文件已经超过100mb了,避免设置过小,频繁触发重写,也不能过大,避免重写的时候阻塞时间过长
auto-aof-rewrite-min-size 100mb

Redis在RDB和AOF同时开启的过程中,重启后会优先加载AOF文件

  • AOF文件在开启后就会自动创建一个空文件
  • 重启后Redis会优先加载AOF:默认是以秒为单位进行保存的,在逻辑上比RDB的数据完整

热开启AOF

# 在redis客户端在线更改
config get appendonly
config set appendonly yes
# 开启aof后,dir目录就会有一个appendonly.aof日志文件
# 同时修改redis.conf配置文件的appendonly参数为yes,确保线上线下参数一致
appendonly yes

Post Directory