Redis使用-与SpringBoot整合

一、JSR-107

Java Caching定义了5个核心接口,分别是CachingProvider(缓存提供者)、CacheManager(缓存管理器)、Cache(缓存)、Entry(缓存键值对)和Expiry(缓存时效)。
  CachingProvider定义了创建、配置、获取、管理和控制多个CacheManager。一个应用可以在运行期访问多个CachingProvider。
  CacheManager定义了创建、配置、获取、管理和控制多个唯一命名的Cache,这些Cache存在于CacheManager的上下文中。一个CacheManager仅被一个CachingProvider所拥有。
  Cache是一个类似Map的数据结构并临时存储以Key为索引的值。一个Cache仅被一个CacheManager所拥有。
  Entry是一个存储在Cache中的key-value对。
  Expiry 每一个存储在Cache中的条目有一个定义的有效期。一旦超过这个时间,条目为过期的状态。一旦过期,条目将不可访问、更新和删除。缓存有效期可以通过ExpiryPolicy设置。
JSR-107规范
 一个应用里面可以有多个缓存提供者(CachingProvider),一个缓存提供者可以获取到多个缓存管理器(CacheManager),一个缓存管理器管理着不同的缓存(Cache),缓存中是一个个的缓存键值对(Entry),每个entry都有一个有效期(Expiry)。缓存管理器和缓存之间的关系有点类似于数据库中连接池和连接的关系。一般不直接使用JSR-107开发

二、Spring缓存抽象

1、简介
  Spring从3.1开始定义了org.springframework.cache.Cache和
org.springframework.cache.CacheManager接口来统一不同的缓存技术;并支持使用JCache(JSR-107)注解简化我们开发。
  Cache接口为缓存的组件规范定义,包含缓存的各种操作(增删改查)集合。Cache接口下Spring提供了各种XxxCache的实现,如RedisCache、EhCacheCache、ConcurrentMapCache等。每次调用需要缓存功能的方法时,Spring会检查指定参数的指定的目标方法是否已经被调用过,如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户。下次调用直接从缓存中获取。
  使用Spring缓存抽象时我们需要关注以下两点:
   ①确定方法需要被缓存以及他们的缓存策略
   ②从缓存中读取之前缓存存储的数据

几个概念及注解:

注解参数 :
在这里插入图片描述
cache SpEL表达式
在这里插入图片描述

三、Redis

redis是Nosql数据库中使用较为广泛的非关系型内存数据库,redis内部是一个key-value存储系统。

它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set –有序集合)和hash(哈希类型,类似于Java中的map)。

Redis基于内存运行并支持持久化的NoSQL数据库,是当前最热门的NoSql数据库之一,也被人们称为数据结构存储服务务器。

在这里插入图片描述
FLUSHALL – 清空所有数据库的所有数据
FLUSHDB – 清空当前所在数据库的数据

Redis的基本命令

获取

在这里插入图片描述

判断

在这里插入图片描述

删除

在这里插入图片描述

获取键值对的数据类型

在这里插入图片描述

HELP

在这里插入图片描述

Redis的数据类型

字符串数据类型:String

在这里插入图片描述
incr和incrby:
使value(前提是value is an integer or out of range)递增1(incr)或者指定的值(incrby)
在这里插入图片描述
DECR、DECRBY:
递减,参考incr和incrby

APPEND:
在这里插入图片描述
STRLEN:
在这里插入图片描述
MSET、MGET
在这里插入图片描述
整理
SET key value 此命令设置指定键的值。
GET key 获取指定键的值。
GETRANGE key start end 获取存储在键上的字符串的子字符串。
GETSET key value 设置键的字符串值并返回其旧值。
GETBIT key offset 返回在键处存储的字符串值中偏移处的位值。
MGET key1 [key2…] 获取所有给定键的值
SETBIT key offset value 存储在键上的字符串值中设置或清除偏移处的位
SETEX key seconds value 使用键和到期时间来设置值
SETNX key value 设置键的值,仅当键不存在时
SETRANGE key offset value 在指定偏移处开始的键处覆盖字符串的一部分
STRLEN key 获取存储在键中的值的长度
MSET key value [key value …] 为多个键分别设置它们的值
MSETNX key value [key value …] 为多个键分别设置它们的值,仅当键不存在时
PSETEX key milliseconds value 设置键的值和到期时间(以毫秒为单位)
INCR key 将键的整数值增加1
INCRBY key increment 将键的整数值按给定的数值增加
INCRBYFLOAT key increment 将键的浮点值按给定的数值增加
DECR key 将键的整数值减1
DECRBY key decrement 按给定数值减少键的整数值
APPEND key value 将指定值附加到键

列表类型(List)

redis的另一个重要的数据结构叫做lists,翻译成中文叫做“列表”。

首先要明确一点,redis中的lists在底层实现上并不是数组,而是链表,也就是说对于一个具有上百万个元素的lists来说,在头部和尾部插入一个新元素,其时间复杂度是常数级别的,比如用LPUSH在10个元素的lists头部插入新元素,和在上千万元素的lists头部插入新元素的速度应该是相同的。

虽然lists有这样的优势,但同样有其弊端,那就是,链表型lists的元素定位会比较慢,而数组型lists的元素定位就会快得多。

lists的常用操作包括LPUSH、RPUSH、LRANGE等。我们可以用LPUSH在lists的左侧插入一个新元素,用RPUSH在lists的右侧插入一个新元素,用LRANGE命令从lists中指定一个范围来提取元素。

//新建一个list叫做mylist,并在列表头部插入元素"1"
127.0.0.1:6379> lpush mylist "1" 
//返回当前mylist中的元素个数
(integer) 1 
//在mylist右侧插入元素"2"
127.0.0.1:6379> rpush mylist "2" 
(integer) 2
//在mylist左侧插入元素"0"
127.0.0.1:6379> lpush mylist "0" 
(integer) 3
//列出mylist中从编号0到编号1的元素
127.0.0.1:6379> lrange mylist 0 1 
1) "0"
2) "1"
//列出mylist中从编号0到倒数第一个元素
127.0.0.1:6379> lrange mylist 0 -1 
1) "0"
2) "1"
3) "2"

lists的应用相当广泛,随便举几个例子:
1.我们可以利用lists来实现一个消息队列,而且可以确保先后顺序,不必像MySQL那样还需要通过ORDER BY来进行排序。
2.利用LRANGE还可以很方便的实现分页的功能。
3.在博客系统中,每片博文的评论也可以存入一个单独的list中。

集合SET

【redis数据结构 – 集合】

redis的集合,是一种无序的集合,集合中的元素没有先后顺序。

集合相关的操作也很丰富,如添加新元素、删除已有元素、取交集、取并集、取差集等。我们来看例子:

//向集合myset中加入一个新元素"one"
127.0.0.1:6379> sadd myset "one" 
(integer) 1
127.0.0.1:6379> sadd myset "two"
(integer) 1
//列出集合myset中的所有元素
127.0.0.1:6379> smembers myset 
1) "one"
2) "two"
//判断元素1是否在集合myset中,返回1表示存在
127.0.0.1:6379> sismember myset "one" 
(integer) 1
//判断元素3是否在集合myset中,返回0表示不存在
127.0.0.1:6379> sismember myset "three" 
(integer) 0
//新建一个新的集合yourset
127.0.0.1:6379> sadd yourset "1" 
(integer) 1
127.0.0.1:6379> sadd yourset "2"
(integer) 1
127.0.0.1:6379> smembers yourset
1) "1"
2) "2"
//对两个集合求并集
127.0.0.1:6379> sunion myset yourset 
1) "1"
2) "one"
3) "2"
4) "two"

对于集合的使用,也有一些常见的方式,比如,QQ有一个社交功能叫做“好友标签”,大家可以给你的好友贴标签,比如“大美女”、“土豪”、“欧巴”等等,这时就可以使用redis的集合来实现,把每一个用户的标签都存储在一个集合之中。

有序集合ZSET

redis不但提供了无需集合(sets),还很体贴的提供了有序集合(sorted sets)。有序集合中的每个元素都关联一个序号(score),这便是排序的依据。
很多时候,我们都将redis中的有序集合叫做zsets,这是因为在redis中,有序集合相关的操作指令都是以z开头的,比如zrange、zadd、zrevrange、zrangebyscore等等

//新增一个有序集合myzset,并加入一个元素baidu.com,给它赋予的序号是1:
127.0.0.1:6379> zadd myzset 1 baidu.com 
(integer) 1
//向myzset中新增一个元素360.com,赋予它的序号是3
127.0.0.1:6379> zadd myzset 3 360.com 
(integer) 1
//向myzset中新增一个元素google.com,赋予它的序号是2
127.0.0.1:6379> zadd myzset 2 google.com 
(integer) 1
//列出myzset的所有元素,同时列出其序号,可以看出myzset已经是有序的了。
127.0.0.1:6379> zrange myzset 0 -1 with scores 
1) "baidu.com"
2) "1"
3) "google.com"
4) "2"
5) "360.com"
6) "3"
//只列出myzset的元素
127.0.0.1:6379> zrange myzset 0 -1 
1) "baidu.com"
2) "google.com"
3) "360.com"

哈希

最后要给大家介绍的是hashes,即哈希。哈希是从redis-2.0.0版本之后才有的数据结构。
hashes存的是字符串和字符串值之间的映射,比如一个用户要存储其全名、姓氏、年龄等等,就很适合使用哈希。

//建立哈希,并赋值
127.0.0.1:6379> HMSET user:001 username antirez password P1pp0 age 34 
OK
//列出哈希的内容
127.0.0.1:6379> HGETALL user:001 
1) "username"
2) "antirez"
3) "password"
4) "P1pp0"
5) "age"
6) "34"
//更改哈希中的某一个值
127.0.0.1:6379> HSET user:001 password 12345 
(integer) 0
//再次列出哈希的内容
127.0.0.1:6379> HGETALL user:001 
1) "username"
2) "antirez"
3) "password"
4) "12345"
5) "age"
6) "34"

Redis持久化

redis提供了两种持久化的方式,分别是RDB(Redis DataBase)和AOF(Append Only File)。

RDB,简而言之,就是在不同的时间点,将redis存储的数据生成快照并存储到磁盘等介质上;

AOF,则是换了一个角度来实现持久化,那就是将redis执行过的所有写指令记录下来,在下次redis重新启动时,只要把这些写指令从前到后再重复执行一遍,就可以实现数据恢复了。

其实RDB和AOF两种方式也可以同时使用,在这种情况下,如果redis重启的话,则会优先采用AOF方式来进行数据恢复,这是因为AOF方式的数据恢复完整度更高。

如果你没有数据持久化的需求,也完全可以关闭RDB和AOF方式,这样的话,redis将变成一个纯内存数据库,就像memcache一样。

Redis事务处理

在聊redis事务处理之前,要先和大家介绍四个redis指令,即MULTI、EXEC、DISCARD、WATCH。这四个指令构成了redis事务处理的基础。
1.MULTI用来组装一个事务;
2.EXEC用来执行一个事务;
3.DISCARD用来取消一个事务;
4.WATCH用来监视一些key,一旦这些key在事务执行之前被改变,则取消事务的执行。

redis> MULTI //标记事务开始
OK
redis> INCR user_id //多条命令按顺序入队
QUEUED
redis> INCR user_id
QUEUED
redis> INCR user_id
QUEUED
redis> PING
QUEUED
redis> EXEC //执行
1) (integer) 1
2) (integer) 2
3) (integer) 3
4) PONG

在上面的例子中,我们看到了QUEUED的字样,这表示我们在用MULTI组装事务时,每一个命令都会进入到内存队列中缓存起来,如果出现QUEUED则表示我们这个命令成功插入了缓存队列,在将来执行EXEC时,这些被QUEUED的命令都会被组装成一个事务来执行。

对于事务的执行来说,如果redis开启了AOF持久化的话,那么一旦事务被成功执行,事务中的命令就会通过write命令一次性写到磁盘中去,如果在向磁盘中写的过程中恰好出现断电、硬件故障等问题,那么就可能出现只有部分命令进行了AOF持久化,这时AOF文件就会出现不完整的情况,这时,我们可以使用redis-check-aof工具来修复这一问题,这个工具会将AOF文件中不完整的信息移除,确保AOF文件完整可用。

有关事务,大家经常会遇到的是两类错误:

1.调用EXEC之前的错误
2.调用EXEC之后的错误

“调用EXEC之前的错误”,有可能是由于语法有误导致的,也可能时由于内存不足导致的。只要出现某个命令无法成功写入缓冲队列的情况,redis都会进行记录,在客户端调用EXEC时,redis会拒绝执行这一事务。(这时2.6.5版本之后的策略。在2.6.5之前的版本中,redis会忽略那些入队失败的命令,只执行那些入队成功的命令)。

127.0.0.1:6379> multi
OK
127.0.0.1:6379> haha //一个明显错误的指令
(error) ERR unknown command 'haha'
127.0.0.1:6379> ping
QUEUED
127.0.0.1:6379> exec
//redis无情的拒绝了事务的执行,原因是“之前出现了错误”
(error) EXECABORT Transaction discarded because of previous errors.
而对于“调用EXEC之后的错误”,redis则采取了完全不同的策略,即redis不会理睬这些错误,而是继续向下执行事务中的其他命令。这是因为,对于应用层面的错误,并不是redis自身需要考虑和处理的问题,所以一个事务中如果某一条命令执行失败,并不会影响接下来的其他命令的执行。我们也来看一个例子:
复制代码代码如下:
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set age 23
QUEUED
//age不是集合,所以如下是一条明显错误的指令
127.0.0.1:6379> sadd age 15 
QUEUED
127.0.0.1:6379> set age 29
QUEUED
127.0.0.1:6379> exec //执行事务时,redis不会理睬第2条指令执行错误
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
3) OK
127.0.0.1:6379> get age
"29" //可以看出第3条指令被成功执行了**
好了,我们来说说最后一个指令“WATCH”,这是一个很好用的指令,它可以帮我们实现类似于“乐观锁”的效果,即CAS(check and set)。

WATCH本身的作用是“监视key是否被改动过”,而且支持同时监视多个key,只要还没真正触发事务,WATCH都会尽职尽责的监视,一旦发现某个key被修改了,在执行EXEC时就会返回nil,表示事务无法触发。

127.0.0.1:6379> set age 23
OK
127.0.0.1:6379> watch age //开始监视age
OK
127.0.0.1:6379> set age 24 //在EXEC之前,age的值被修改了
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set age 25
QUEUED
127.0.0.1:6379> get age
QUEUED
127.0.0.1:6379> exec //触发EXEC
(nil) //事务无法被执行

四、SpringBoot整合Redis

依赖:

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

application.yml关于redis的配置:

redis:
      host: 127.0.0.1
      port: 6379
      timeout: 2000
      database: 0
      jedis:
           pool:
              max-active: 10
              max-idle: 10
              max-wait: 3

启动类添加注解

@EnableCaching

RedisConfig类

/**
 * StringRedisTemplate与RedisTemplate区别点
 * 两者的关系是StringRedisTemplate继承RedisTemplate。
 *
 * 两者的数据是不共通的;也就是说StringRedisTemplate只能管理StringRedisTemplate里面的数据,RedisTemplate只能管理RedisTemplate中的数据。
 *
 * 其实他们两者之间的区别主要在于他们使用的序列化类:
 *     RedisTemplate默认使用JdkSerializationRedisSerializer(jdk原生序列化方式,不建议)    存入数据会将数据先序列化成字节数组然后在存入Redis数据库。
 *
 *       StringRedisTemplate使用的是StringRedisSerializer
 *
 * 使用时注意事项:
 *    当你的redis数据库里面本来存的是字符串数据或者你要存取的数据就是字符串类型数据的时候,那么你就使用StringRedisTemplate即可。
 *    但是如果你的数据是复杂的对象类型,而取出的时候又不想做任何的数据转换,直接从Redis里面取出一个对象,那么使用RedisTemplate是更好的选择。
 */
@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<Object,Object> redisTemplate=new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(RedisTemplate默认使用JDK的序列化方式)
        Jackson2JsonRedisSerializer serializer=new Jackson2JsonRedisSerializer(Object.class);
        
        ObjectMapper mapper = new ObjectMapper();
        //指定要序列化的域为ALL(可选FIELD、get、set等)。ANY指定包括private和public
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        //设置后序列化时将对象全类名一起保存下来,那么存储到redis里的为有类型的json数据,反序列化为类对象。不设置时不保存类型信息,那么反序列后为Map。
        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance ,
                ObjectMapper.DefaultTyping.NON_FINAL);
                
        serializer.setObjectMapper(mapper);
        
        redisTemplate.setValueSerializer(serializer);
        //使用StringRedisSerializer来序列化和反序列化redis的key值
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

   // @Bean
   // public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory){
   // StringRedisTemplate stringRedisTemplate=new StringRedisTemplate();
   //   stringRedisTemplate.setConnectionFactory(redisConnectionFactory);
   //  return stringRedisTemplate;
   //  }
}

Service类


/**
 * 可以通过@CacheConfig的cacheNames 属性指定缓存的名字之后,该类中的其他缓存注解就不必再写value或者cacheName了,
 * 会使用该名字作为value或cacheName的值,当然也遵循就近原则
 */
@Slf4j
@CacheConfig (cacheNames = {"emp"})
@Service
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements IEmployeeService {
    @Autowired
    private EmployeeMapper employeeMapper;

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * @CachePut:即调用方法,又更新缓存数据
     * 修改了数据库中的数据,同时又更新了缓存
     *
     *运行时机:
     * 1.先调用目标方法
     * 2.将目标方法返回的结果缓存起来
     *
     */
    @CachePut(key = "#result.id",cacheNames = {"emp"})
    @Override
    public Employee addEmpService(Employee employee) {
        employeeMapper.insert(employee);
        return employee;
    }

    /**
     * @Cacheable的几个属性详解:
     *       cacheNames/value:指定缓存组件的名字
     *       key:缓存数据使用的key,可以用它来指定。默认使用方法参数的值,一般不需要指定
     *       keyGenerator:key生成策略
     *       cacheManager 和 cacheResolver 是互斥参数,同时指定两个可能会导致异常
     *       condition:调用前判断,指定符合条件才缓存,比如:condition="#id>3"
     *                   也就是说传入的参数id>3才缓存数据
     *       unless:执行后判断,否定缓存,当unless为true时不缓存,可以获取方法结果进行判断
     *       sync:是否使用异步模式
     * 执行流程:先执行@Cacheable注解中的getCache(String name)方法,根据name判断ConcurrentMap中是否有此缓存,
     * 如果没有缓存那么创建缓存并保存数据,另外service层的方法也会执行。如果有缓存不再创建缓存,另外service层的方法也不会执行。
     * @param id
     * @return
     */
    @Cacheable(key = "#root.args[0]",cacheNames = {"emp"})
    @Override
    public Employee getEmpById(Integer id) {
        System.out.println("22222222222222222222");
        log.info("33333333"+String.valueOf("    3   "+employeeMapper.selectById(id)));
        return employeeMapper.selectById(id);
    }

    /**
     * @CacheEvict:清除缓存
     * 1.key:指定要清除缓存中的某条数据
     * 2.allEntries=true:删除缓存中的所有数据
     * 3.beforeInvocation=false:默认是在方法之后执行清除缓存
     *   beforeInvocation=true:现在是在方法执行之前执行清除缓存.作用是:只清除缓存、不删除数据库数据
     * 注意这里不指定cacheNames是因为类上的CacheConfig注解。同理其他方法也可以省略cacheNames属性
     * @param id
     */
    @CacheEvict(key = "#id", beforeInvocation = true)
    @Override
    public void delEmp(Integer id) {
        employeeMapper.deleteById(id);
    }



    /**
     * @Caching是 @Cacheable、@CachePut、@CacheEvict注解的组合.其实就是可以指定多个缓存规则
     * 以下注解的含义:
     * 1.当使用指定名字查询数据库后,数据保存到缓存
     * 2.现在使用id、email就会直接查询缓存,而不是查询数据库
     * @param lastName
     * @return
     */
    @Caching(
            cacheable = {
                    @Cacheable(key = "#lastName")
            },
            put = {
                    @CachePut(key = "#result.id"),
                    @CachePut(key = "#result.email")
            }
    )
    @Override
    public Employee getEmpByLastName(String lastName) {
        return employeeMapper.selectOne(new QueryWrapper<Employee>().likeRight("lastName",lastName));
    }
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值