Redis面试不得不知的知识点

简介

背景

在传统的 Java Web 项目中,使用数据库进行存储数据,但是有一些致命的弊端,这些弊端主要来自于性能方面。由于数据库持久化数据主要是面向磁盘,而磁盘的读/写比较慢,很少存在高并发。

特点

  • 汇编语言,基于内存,单线程

    属于NoSQL,每秒十几万次的读/写操作,并提供一定的持久化

  • 支持高并发

    支持集群、分布式、主从同步,还能支持一定的事务能力

  • 数据结构简单

    6 种数据类型

作用:

  • 缓存

    读>写(9:1),内存空间有限

    在使用Redis 存储的时候,需要从 3 个方面进行考虑:

    业务数据常用吗?命中率如何?如果命中率很低,就没有必要写入缓存。

    该业务数据是读操作多,还是写操作多 ,如果写操作多 ,频繁需要写入数据库 ,也没有必要使用缓存。

    业务数据大小如何?如果要存储几百兆字节的文件,会给缓存带来很大的压力,有没有必要?

    redis缓存应用示意图
  • 高速读/写场合

    商品秒杀

    如果使用的是数据库, 一个瞬间数据库就需要执行成千上万的 SQL,很容易造成数据库的瓶颈,严重的会导致数据库瘫痪,造成 Java Web 系统服务崩溃。

    使用 Redis 去应对, 把这些需要高速读/写的数据 , 缓存到 Redis 中,在满足一定的条件下,触发这些缓存的数据异步写入数据库中。

  • 时效性控制(超时设置)

安装

Windows

常见问题:

  • windows下redis启动闪退:

    cmd到redis目录下,按以下方式启动:redis-server.exe redis.windows.conf

  • redis修改密码:redis 127.0.0.1:6379> config set requirepass dnc.2009

  • redis查看密码:config get requirepass

  • 若查看密码错误,需授权:auth dnc.2009

  • 局域网内不能访问的问题:

    • 1在配置文件redis.windows.conf中注释掉bind 127.0.0.1

    • 2.关闭防火墙,或者允许redis可以通过防火墙,重启redis

    • 3.完成以上步骤,若还不能访问,请通过强制启用配置文件的方式>redis-server.exe redis.windows.conf

Linux

下载 wget http://download.redis.io/releases/redis-3.2.8.tar.gz

解压 tar -xvf redis-3.2.4.tar.gz

安装 make (MALLOC=libc)

准备 允许redis可以通过防火墙

systemctl status firewalld

systemctl stop firewalld

systemctl start firewalld

修改配置

  • 1.注释掉bind 127.0.0.1(指定本机网卡对应的IP地址,即只有本机能访问;0.0.0.0,表示所有主机都可以连接到redis)

  • 2.protected mode yes(此项开启时需配置bind或设置密码)改成no(关闭保护模式外网可以访问)

  • 3.设置上redis的密码requirepass(推荐)requirepass dnc.2009

  • 4.2和3任选一

  • daemonize:yes 在该模式下,redis会在后台运行

启动 $redis/src/redis-server ../redis.conf --port 6379

$redis/src/redis-cli -p 6379

设为开机启动

1.编写 service 启动文件

vi /etc/init.d/redis,将下面代码写入文件,修改文件中相应的路径和配置


#!/bin/sh

# chkconfig: 2345 10 90 

# description: Start and Stop redis 



#端口根据自己 redis 需求配置

REDISPORT=6379

#server 路径根据自己 redis 实际路径配置

EXEC=/root/redis-4.0.9/src/redis-server

#cli 路径根据自己 redis 实际路径配置

REDIS_CLI=/root/redis-4.0.9/src/redis-cli

PIDFILE=/var/run/redis_${REDISPORT}.pid

#redis.conf 路径根据自己 redis 实际路径配置

CONF="/root/redis-4.0.9/redis.conf"

#如果redis配置了requirepass,auth 为 redis.conf 中配置的 requirepass

AUTH="dnc.2009"

case "$1" in 

        start) 

                if [ -f $PIDFILE ] 

                then 

                        echo "$PIDFILE exists, process is already running or crashed." 

                else 

                        echo "Starting Redis server..." 

                        $EXEC $CONF 

                fi 

                if [ "$?"="0" ] 

                then 

                        echo "Redis is running..." 

                fi 

                ;; 

        stop) 

                if [ ! -f $PIDFILE ] 

                then 

                        echo "$PIDFILE exists, process is not running." 

                else 

                        PID=$(cat $PIDFILE) 

                        echo "Stopping..." 

                        $REDIS_CLI -p $REDISPORT -a $AUTH SHUTDOWN   

                        sleep 2 

                        while [ -x $PIDFILE ] 

                        do 

                                echo "Waiting for Redis to shutdown..." 

                                sleep 1 

                        done 

                        echo "Redis stopped" 

                fi 

                ;; 

        restart|force-reload) 

                ${0} stop 

                ${0} start 

                ;; 

        *) 

                echo "Usage: /etc/init.d/redis {start|stop|restart|force-reload}" >&2 

                exit 1 

esac

上面的注释的意思是,redis服务必须在运行级2,3,4,5下被启动或关闭,启动的优先级是90,关闭的优先级是10。

给启动文件设置权限和用户组


chmod +x /etc/init.d/redis

chown root:root /etc/init.d/redis

添加redis服务:


chkconfig --add redis

设为开机启动 :


chkconfig redis on

打开/关闭redis命令:


service redis start/stop

6 种数据类型

常用命令

  • DEL key

  • EXISTS key

  • KEYS pattern

  • RANDOMKEY

  • TYPE key

  • RENAME key newkey

  • RENAMENX key newkey 不存在才改名

  • SORT 对list或set排序,仅排序不改变原数据

  • select db

  • move key db

  • FLUSHALL

字符串(String)

  • 特点

    • string 类型是 Redis 最基本的数据类型,一个 key 对应一个 value

    • string 类型是二进制安全的。意思是 redis 的 string 可以包含任何数据。比如jpg图片或者序列化的对象

    • string 类型的值最大能存储 512MB

  • 常用命令

    (m)set key value (multiple)

    setnx key value (not exist)

    (m)get key

    strlen key

    getset key value

    append key value

  • 简单运算命令(redis所有操作都是原子操作,采用单线程处理所有业务,因此无需考虑并发影响==》解决分布式id问题,最大范围java中的long范围)

    incr key

    incrby key increment

    decr key

    decrby key decrement

    incrbyfloat key increment

哈希(Hash)

hash 是一个 String 类型的 field 和 value(只能是字符串,不存在嵌套对象) 的映射表,如同 Java 的 map 一样 , 一个对象里面有许多键值对,它特别适合存储对象。

  • 常用命令

    hmset key field1 value1[field2 value2...] 覆盖

    hset key filed value

    hsetnx key field value 不存在才设置值,返回数量

    hmget key field1[field2.....] 当key 不存在时,返回 nil

    hdel key field1 [field2 ......]

    hexists key field

    hgetall key

    hkeys key

    hvals key

    hlen key

  • 简单运算命令

    hincrby key field increment

    hincrbyfloat key field increment

  • String存储对象(Json) VS Hash存储对象

    String存在对象讲究整体性,以读为主

    Hash存储对象讲究分散性,以写为主

列表(List)

  • 特点

    它可以存储多个字符串,而且它是有序的,双向的。有利于增删,不利于查找。

  • 常用命令

    lpush key value1 [value2].. 注意顺序

    lpushx key value 列表存在则插入,不然失败

    lpop key

    ......................分左右......................

    lrem key count value 删除列表key中count个(为0则是全部)值为value的节点

    lset key index value

    lrange key start stop 获取列表从start到stop的节点值,负数代表倒数第x个,-1即为最后一个

    lindex key index 读取下标为index的节点

    llen key

    linsert key before|after pivot value 将value插入到列表key中的pivot之前/后

    ...............................................是否进程安全...............................................

    blpop key timeout <-->lpop 可以设置等待时间,在等待时间内可以获取到客户端插入的值

集合(Set)

  • 特点

    • 随机,无序的==》抽奖

    • 不重复的(重复数据新加不了)==》黑名单

    • 每个元素是String

    • 底层是哈希表结构,仅存储键不存储值(nil),添加、删除、查找的复杂度都是 O(1)

    • 集合操作

  • 常用命令

    • sadd key member1 [member2...]

    • scard key

    • smembers key 返回所有元素

    • sismember key member 包含

    • smove src des member

    • srem key member1 [member2...]

    • spop key [count] 随机弹出集合的count个元素,3.2以上的版本可指定count

    • srandmember key [count] 随机获取count个元素

    • sdiff key1 [key2] 差集,rkey2为空,返回key1所有元素

    • sdiffstore des key1 [key2] 求出差集并存储到指定集合

    • sinter key1 [key2] 交集,rkey2为空,返回key1所有元素

    • sintestore des key1 [key2]

    • sunion key1 [key2] 并集,rkey2为空,返回key1所有元素

    • sunionstore key1 [key2]

有序集合(sorted set)

  • 特点

    有序集合和集合类似,只不过它会关联一个double类型的分数,依赖 key 标示它是属于哪个集合,依赖分数进行排序,所以值和分数是必须的。而实际上不仅可以对分数进行排序,在满足一定的条件下,也可以对值进行排序 。如果添加重复数据,value进不去,但分数会覆盖。定时任务执行顺序。

  • 常用方法

    • zadd key score1 value1 [score2 value2...]

    • zrange key start stop [withscores] 按分值大小从小到大获取全部数据【及相应score(奇数位是值,偶数位数score)】

    • zrevrange key start stop [withscores] 反向获取

    • zrangebyscore key min max [withscores] [limit] 按分数(bylex:按值)获取指定范围内数据

    • zrevrangebyscore key max min [withscores] [limit] (bylex:按值,byrank按索引)

    • zrem key member [member]

    • zremrange key start stop

    • zcard key

    • zcount key min max 根据分数返回对应的成员列表,包含min和max

    • zlexcount key min max

    • zcount key min max

    • zincrby key increment member

    • zinterstore desKey numkeys key1 [key2 key3 ....] [aggregate max\min\sum]其中numkeys 是一个整数,表示多少个有序集合,求交集后还可进行元素的max或sum操作,默认sum

    • z(rev)ank key member 按从小到大展示集合排行(第一为0,第二为1)

    • zscore key member 获取member的分数

基数(HyperLogLog)

  • 特点

    用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。基数的作用是评估大约需要准备多少个存储单元去存储数据(并不进行存储),但是基数的算法一般会存在一定的误差(一般是可控的)。

  • 常用命令

    • pfadd key element

    • pfcount key

    • pfmerge desKey key1 [key2 key3 ......]

超时与回收

https://www.jianshu.com/p/6a5eb0ddf57b

  • 常用命令

    • EXPIRE key seconds 单位秒

    • EXPIREAT key timstamp 时间戳

    • TTL key 以秒为单位,返回给定 key 的剩余生存时间(TTL, time to live)

    • PERSIST key 移除 key 的过期时间,key 将持久保持

    • del key

    • SETEX key seconds value 将键 key 的值设置为 value , 并将键 key 的生存时间设置为 seconds 秒钟。

    • PSETEX key milliseconds value

  • 超时机制

    • Redis 的 key 超时不会被其自动回收,它只会标识哪些键值对超时了。

    • 原因:如果一个很大的键值对超时,回收需要很长的时间 。 如果采用超时回收,则可能产生停顿。

    • 缺点:浪费空间。

  • 回收方式

    • 定时回收是指在确定的某个时间触发一段代码,回收超时的键值对。redis中有个时间事件,它会清理数据库中已经过期的键。

    • 惰性回收则是当一个超时的键,被再次用 get 命令访问时,将触发 Redis 将其从内存中清空。它的优势是可以指定回收超时的键值对;它的缺点是要执行一个莫名其妙的 get 操作,或者在某些时候,我们也难以判断哪些键值对已经超时。

    • redis采用的是两种方式的结合,轮询数据库随机抽查,重点抽查

  • 内存控制

    • maxmemory占用物理内存的比例,默认为0 即不限制

    • 每次选取待删除数据的个数

    • maxmemory-policy 内存溢出控制策略

      • 检查易失数据(可能会过期的数据集) volatile

      • 检查全库数据 allkeys

      • 放弃数据驱逐(no-enviction),会引发OOM

备份与恢复

分类

  • 快照(RDB)

    • save(单线程,会影响后面命令执行效率)

    • bgsave(调用fork函数生成子线程,日志中可以看到成功通知)

    • 配置文件内save second changes 自动执行,满足限定时间范围second内key的变化数量达到指定数量changes即进行持久化,只对增删改有影响

    • 特殊启动方式;

      • 全量复制(主从复制中)

      • 服务器运行过程中重启:debug reload

      • 关闭服务器时保存:shutdown save

  • AOF只追加文件(记录操作日志)

    • 写数据三种策略:

      • always

      • everysec(每秒)

      • no(系统控制)

    • 配置文件配置:

      • appendonly yes|no 是否开启AOF,默认no

      • appendfsync always|everysec|no

      • appendfilename

    • AOF重写

      • 规则

        • 同一数据若干条命令合并

        • 已超时数据

        • 只保留最后的有效数据

      • 方式

        • 手动重写 bgrewriteaof

        • 自动重写

          • 自动重写触发条件1:aof_current_size>auto_aof_rewrite_min_size

          • 自动重写触发条件2:(aof_current_size - aof_base_size)/aof_base_size > auto_aof_rewrite-percentage

          • 其中,auto_aof_rewrite_min_size和auto_aof_rewrite-percentage可在配置文件中设置,aof_current_size和aof_base_size可运行info Persitence获取具体信息

配置文件

定位:

SNAPSHOTTING

APPEND ONLY MODE

命令

SAVE(不能写)或BGSAVE(异步)命令在 redis 安装目录中创建dump.rdb文件

将备份文件 (dump.rdb) 移动到 redis 安装目录并启动服务即可

bgrewriteaof

事务及锁

redis是单线程的,但是不通客户端会争抢运行线程

  • 事务命令

    开启事务:multi

    执行事务:exec

    取消事务:discard

  • 事务中的错误

    • 语法错误(命令书写错误)--》事务中所有命令都不执行,包括正确的

    • 语法正确但不能执行--》运行正确的指令,跳过错误的,不会回滚,需要在代码中控制

  • 在开启事务前watch,监控key的值有没有改变,如果有其他客户端对事务内同一个对象进行操作则返回nil

    添加监控锁:watch xxx

    取消监控缩:unwatch

  • 分布式锁

    setnx lock-key value (最后一件商品被多人抢购,不能用watch,value随意)

    del lock-key

    expire lock-key second

    -->set key value [EX seconds] [PX milliseconds] nx 分布式锁改进,通过set进行原子性操作

主从同步

读写分离:master负责写,slave负责读

负载均衡

故障恢复

数据冗余(数据热备份)

高可用(集群)

enter image description here

从:

slaveof 192.168.1.107 6379

masterauth dnc.2009

主:

注释掉bind 127.0.0.1

从cli查看:

info replication

其它

哨兵

对主从结构中的每台服务器进行监控,当master主机宕机时通过投票机制选择新的master并通知。哨兵也是一台redis服务器,只是不提供数据服务;通常配置为单数

  • 配置

    参考sentinel.conf

  • 启动

    redis-sentinel sentinel-端口号

集群

  • 数据存储设计

    存储槽 可扩展性

  • 内部通讯设计

    各个redis保存其它redis存储槽顺序信息

  • 命令

    redis-cli --cluster create xxx

    redis-cli -c -p 6379

缓存预热

提前将相关的缓存数据放到redis

缓存雪崩

短时间内大量key集中过期

解决:缓存组件设计高可用(如集群,防止服务器故障)

请求限流与服务熔断降级机制

设置缓存过期时间一定的随机分布

缓存击穿

单个高热的key数据过期

解决:设置二级缓存,或者设置热点缓存永不过期

使用互斥锁,在执行过程中,如果缓存过期,那么先获取分布式锁,在执行从数据库中加载数据,如果找到数据就存入缓存,没有就继续该有的动作,在这个过程中能保证只有一个线程操作数据库,避免了对数据库的大量请求。

缓存穿透

大量非正常url请求redis,出现大量未命中(查询一个一定不存在的数据)

解决:缓存空值,需和真正缓存的数据区分开,另外将其过期时间设为较短时间。

使用布隆过滤器(能判断一个 key 一定不存在),在缓存的基础上,构建布隆过滤器数据结构,在布隆过滤器中存储对应的 key,如果存在,则说明 key 对应的值为空。

性能监控

redis-benchmark -c 100 -n 5000 压测,100个连接,5000次请求对应的性能

monitor 调试信息

slowlog 慢日志

SpringBoot2通过 RedisTemplate使用Redis

引入依赖


compile group: 'org.springframework.boot', name: 'spring-boot-starter-data-redis'

compile group: 'org.apache.commons', name: 'commons-pool2'

说明:

redis依赖commons-pool 这个依赖,用作 redis 连接池。

修改配置文件


spring:

  redis:

    # 数据库索引

    database: 0

    host: 39.106.99.124

    port: 6379

    password: dnc.2009

    # 连接超时时间(毫秒)

    timeout: 5000

    lettuce:

      # 关闭超时时间

      shutdown-timeout: 100

      pool:

        # 连接池最大连接数(使用负值表示没有限制)

        max-active: 8

        # 最大阻塞等待时间(使用负值表示没有限制)

        max-wait: -1

        # 连接池中的最大空闲连接

        max-idle: 8

        # 连接池中的最小空闲连接

        min-idle: 0

说明:

  1. 操作redis的客户端有jedis跟Lettuce。在springboot1.x系列中,其中使用的是jedis,但是到了springboot2.x其中使用的是Lettuce。 因为我们的版本是springboot2.x系列,所以今天使用的是Lettuce。

关于jedis跟lettuce的区别:

  • Lettuce 和 Jedis 的定位都是Redis的client,所以他们当然可以直接连接redis server。

  • Jedis在实现上是直接连接的redis server,如果在多线程环境下是非线程安全的,这个时候只有使用连接池,为每个Jedis实例增加物理连接

  • Lettuce的连接是基于Netty的,连接实例(StatefulRedisConnection)可以在多个线程间并发访问,应为StatefulRedisConnection是线程安全的,所以一个连接实例(StatefulRedisConnection)就可以满足多线程环境下的并发访问,当然这个也是可伸缩的设计,一个连接实例不够的情况也可以按需增加连接实例。

  1. redis的自动配置可以参照RedisAutoConfiguration.java,它提供两个 Bean 来操作 redis RedisTemplate ,StringRedisTemplate , StringRedisTemplate 是 RedisTemplate 的子类。
  • StringRedisTemplate默认采用的是String的序列化策略,保存的key和value都是采用此策略序列化保存的。

    RedisTemplate默认采用的是JDK的序列化策略,保存的key和value都是采用此策略序列化保存的。

  • RedisTemplate存入数据会将数据先序列化成字节数组然后在存入Redis数据库,这个时候打开Redis查看的时候,你会看到你的数据不是以可读的形式展现的,而是以字节数组显示,这样就会导致一个问题,当需要获取的数据不是以字节数组存在redis当中而是正常的可读的字符串的时候,RedisTemplate就无法获取导数据,这个时候获取到的值就是NULL,这个时候StringRedisTempate就派上了用场;StringRedisTemplate 默认做了 string 的 序列化,这样可以在redis客户端看到这些数据,StringRedisTemplate只能处理String-String的键值对数据。

  • (1)你的Redis数据库里面本来存的是字符串数据或者是你要存取的数据就是字符串类型数据的时候,那么你就使用StringRedisTemplate即可;

    (2)但是如果你的数据是复杂的对象类型,而取出的时候又不想做任何数据转换,直接从Redis里面取出一个对象,那么使用RedisTemplate是更好的选择;

    (3)RedisTemplate中存取数据都是字节数组。当Redis职工存入的数据是可读形式而非字节数组时,使用RedisTemplate取值的时候会无法获取导出数据,获得的值为null。可以使用StringRedisTemplate试试;

  • RedisTemplate默认是使用的JdkSerializationRedisSerializer序列化,序列化后的值包含了对象信息,版本号,类信息等,是一串字符串,所以无法进行数值自增操作。而Redis自身的机制是 如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作。所以当key不存在时,调用increment方法后,通过ValueOperations.get(key)或BoundValueOperations.get()获取value时会由于无法序列化而报错。而且JdkSerializationRedisSerializer处理对象时必须实现Serializable接口。

编写配置文件


@Configuration

public class RedisConfig extends CachingConfigurerSupport {

    /**

    * @description 缓存key前缀

    */

    private static final String KEY_PREFIX = "";

    /**

    * @description 默认超时时间,单位:小时

    */

    private static final long DEFAULT_OVERTIME = -1L;

    /*@Bean

    public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {

        return RedisCacheManager.create(connectionFactory);

    }*/

    @Bean

    @Primary

    public CacheManager cacheManager(RedisConnectionFactory factory){

        //缓存配置对象

        RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()

                //设置缓存的默认超时时间

                .entryTtl(Duration.ofHours(DEFAULT_OVERTIME))

                //如果是空值,不缓存

                .disableCachingNullValues()

                //设置key序列化器

                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))

                //设置value序列化器

                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer((new GenericJackson2JsonRedisSerializer())));

        //可以抽取的公共配置

        Map<String, RedisCacheConfiguration> map = new HashMap<>(RedisEnum.values().length);

        for(RedisEnum redisEnum:RedisEnum.values()){

            map.put(redisEnum.getDesc(), redisCacheConfiguration(cacheConfig,redisEnum.getValue()));

        }

        return RedisCacheManager

                .builder(factory)

                .transactionAware()

                .cacheDefaults(cacheConfig)

                .withInitialCacheConfigurations(map)

                .build();

    }

    public RedisCacheConfiguration redisCacheConfiguration(RedisCacheConfiguration configuration, Duration duration){

        configuration = configuration.entryTtl(duration);

        return configuration;

    }

    @Bean

    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {

        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();

        redisTemplate.setConnectionFactory(connectionFactory);

        redisTemplate.setEnableTransactionSupport(true);

        // 配置redis 的 jackson 序列化方案

        GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();

        /*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);*/

        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        // key序列化

        redisTemplate.setKeySerializer(stringRedisSerializer);

        // value序列化

        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);

        redisTemplate.setHashKeySerializer(stringRedisSerializer);

        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);

        redisTemplate.afterPropertiesSet();

        return redisTemplate;

    }

    /**

    * 缓存生成key的策略

    */

    @Bean

    @Override

    public KeyGenerator keyGenerator() {

        return (target, method, params) -> {

            //StringJoiner是 java8对String字符串拼接提供的工具类, 构造器第一个参数表示拼接以冒号分隔,参数二是前缀,参数三是后缀

            StringJoiner joiner = new StringJoiner(":",KEY_PREFIX,"");

            joiner.add(target.getClass().getSimpleName());

            joiner.add(method.getName());

            ObjectMapper objectMapper = new ObjectMapper();

            for (Object param : params) {

                try {

                    joiner.add(objectMapper.writeValueAsString(param));

                } catch (JsonProcessingException e) {

                    e.printStackTrace();

                }

            }

            return joiner.toString();

        };

    }

}

redis工具类


@Component

public class RedisUtil {

    @Autowired

    RedisTemplate<String, Object> redisTemplate;

    /**

    * 默认超时单位:分钟

    */

    public void setString(String key, Object value, int expireTime) {

        ValueOperations<String, Object> ops = redisTemplate.opsForValue();

        if (expireTime != 0) {

            ops.set(key, value, expireTime, TimeUnit.MINUTES);

        } else {

            ops.set(key, value);

        }

    }

    public void setString(String key, Object value, int expireTime, TimeUnit timeUnit) {

        ValueOperations<String, Object> ops = redisTemplate.opsForValue();

        if (expireTime != 0) {

            ops.set(key, value, expireTime, timeUnit);

        } else {

            ops.set(key, value);

        }

    }

    public Object getString(String key) {

        ValueOperations<String, Object> ops = this.redisTemplate.opsForValue();

        return ops.get(key);

    }

    /**

    * 删除多个key

    *

    * @param keys

    */

    public void deleteKey(String... keys) {

        Set<String> kSet = Stream.of(keys).collect(Collectors.toSet());

        redisTemplate.delete(kSet);

    }

    /**

    * 删除Key的集合

    *

    * @param keys

    */

    public void deleteKey(Collection<String> keys) {

        Set<String> kSet = keys.stream().collect(Collectors.toSet());

        redisTemplate.delete(kSet);

    }

    /**

    * 指定key在指定的日期过期

    *

    * @param key

    * @param date

    */

    public void expireKeyAt(String key, Date date) {

        redisTemplate.expireAt(key, date);

    }

    /**

    * 若未改变默认jdk序列化的方式, 也通过下面方法获取自增值

    *

    * @param key

    */

    public Long getIncrValue(final String key) {

        return redisTemplate.execute((RedisCallback<Long>) connection -> {

            RedisSerializer<String> serializer=redisTemplate.getStringSerializer();

            byte[] rowKey=serializer.serialize(key);

            byte[] rowVal=connection.get(rowKey);

            try {

                String val=serializer.deserialize(rowVal);

                return Long.parseLong(val);

            } catch (Exception e) {

                return 0L;

            }

        });

    }

测试类


@SpringBootTest

class SpringbootGradleApplicationTests {

    @Autowired

    private RedisTemplate redisTemplate;

    @Autowired

    private RedisUtil redisUtil;

    @Test

    public void redisTest() {

        /*opsForValue: 对应 String(字符串)

        opsForZSet: 对应 ZSet(有序集合)

        opsForHash: 对应 Hash(哈希)

        opsForList: 对应 List(列表)

        opsForSet: 对应 Set(集合)

        boundValueOps() Redis字符串(或值)键绑定操作*/

        /*opsForValue() VS boundValueOps(K key)

        ops开头的操作可以操作多个不同的key

        bound开头的操作只可以操作一个传入的key*/

        ValueOperations<String,Object> ops = redisTemplate.opsForValue();

        ops.set("k1", "v1");

        ops.increment("k2");

        UserInfo userInfo = new UserInfo();

        userInfo.setUserName("name1");

        userInfo.setId("id1");

        ops.set("k3", userInfo);

        System.out.println(ops.get("k1"));

        System.out.println(redisUtil.getIncrValue("k2"));

        System.out.println(ops.get("k2"));

        System.out.println(ops.get("k3"));

    }

}

SpringBoot通过Spring Cache 集成 Redis

application.yml


spring:

  cache:

    # 使用了Spring Cache后,能指定spring.cache.type就手动指定一下,虽然它会自动去适配已有Cache的依赖,但先后顺序会对Redis使用有影响(JCache -> EhCache -> Redis -> Guava)

    type: redis

使用举例


@Service

@CacheConfig(cacheNames = RedisEnum.DescConstant.USER_CACHE_10_MINUTES,keyGenerator = "keyGenerator")

public class UserService {

    @Autowired

    private UserDao userDao;

    @Autowired

    private RedisUtil redisUtil;

    @Transactional(rollbackFor = Exception.class)

    @CacheEvict(allEntries = true)

    public AppResponse insertUser(UserInfo userInfo) {

        //写业务逻辑

        String userName = userInfo.getUserName();

        if(null != userName && !"".equals(userName)){

            userName = userName + "aaa";

            userInfo.setUserName(userName);

        }

        //判断用户账号不能重复

        if(userDao.countUserAcct(userInfo)>0){

            return AppResponse.error("不能账号重复");

        }

        //上数据库查询姓名的count

        int num = userDao.saveUser(userInfo);

        if(num > 0){

            return AppResponse.success("新增成功");

        }

        return AppResponse.error("新增成功");

    }

    /**

    * 对无条件查询进行缓存

    */

    @Cacheable(condition = "T(com.neusoft.springboot_gradle.common.util.EntityUtil).checkObjAllFieldsIsNull(#userInfo) == true")

    public AppResponse listUsers(String flag, UserInfo userInfo) {

        List<UserInfo> userInfoList = userDao.listUsers(flag, userInfo);

        if(userInfoList != null && userInfoList.size() <= 0){

            return AppResponse.error("数据为空");

        }

        return AppResponse.success("查询成功", userInfoList);

    }

    /**

    * 修改时更新相应的用户查询信息,列表查询时效性不大暂不更新

    */

    @Transactional(rollbackFor = Exception.class)

    public AppResponse updateUser(UserInfo userInfo) {

        //判断用户账号不能重复

        if(userDao.countUserAcct(userInfo)>0){

            return AppResponse.error("不能账号重复");

        }

        int num  = userDao.updateUser(userInfo);

        if(num > 0){

            String key = RedisEnum.DescConstant.USER_CACHE_10_MINUTES+"::"+this.getClass().getSimpleName()+":"+userInfo.getId();

            redisUtil.setString(key,AppResponse.success("查询成功", userDao.getUser(userInfo.getId())),10);

            return AppResponse.success("修改成功");

        }

        return AppResponse.error("修改失败");

    }

    @Cacheable(key = "#root.targetClass.simpleName + ':' + #id")

    public AppResponse getUser(String id) {

        UserInfo userInfo = userDao.getUser(id);

        return AppResponse.success("查询成功", userInfo);

    }

    @CacheEvict(allEntries = true)

    @Transactional(rollbackFor = Exception.class)

    public AppResponse deleteUserBatch(UserInfo userInfo) {

        int num  = userDao.deleteUserBatch(userInfo);

        if(num > 0){

            return AppResponse.success("删除成功");

        }

        return AppResponse.error("删除失败");

    }

    /**

    * 删除时删除同组(value或cachenames)下的缓存

    */

    @CacheEvict(allEntries = true)

    @Transactional(rollbackFor = Exception.class)

    public AppResponse deleteUserBatch2(String lastModifiedBy,List<UserInfo> list) {

        Map map = new HashMap(2);

        map.put("lastModifiedBy", lastModifiedBy);

        map.put("myObjectList", list);

        int num  = userDao.deleteUserBatchByAcctAndName(map);

        if(num > 0){

            return AppResponse.success("删除成功");

        }

        return AppResponse.error("删除失败");

    }

说明:

  • @CacheEvict:缓存清除

    key:指定要清除的数据

    allEntries = true : 指定清除同组(value或CacheNames)缓存中的所有数据

    beforeInvocation=false: 缓存的清除是否在方法之前执行

    默认代表缓存清除操作是在方法执行之后执行;如果出现异常缓存就不会清除

    beforeInvocation=true 代表清除缓存操作是在方法运行之前执行,无论方法是否出现异常,缓存都清除

  • @CachePut: 既调用方法,又更新缓存数据;同步更新缓存

    修改了数据库的某个数据,同时更新缓存

    运行:

    1.先调用目标方法

    2.将目标方法的结果缓存起来

    我们更新id=1的员工 信息,返回了更新后的值,这个时候我再查询id=1的员工,可以看到只有之前更新时打印的数据,并没有从数据库查数据

@CachePut 注释,这个注释可以确保方法被执行,同时方法的返回值也被记录到缓存中。

本文首发于我的个人博客的技术模块,请戳==》 www.moomal.top ,转载请标明出处!
版权所有:papayadog

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容