Spring缓存-redis应用

redis是什么

redis是一种NoSQL数据库,其具备如下特性:
1. 基于内存运行,性能高效;
2. 支持分布式,理论上可无限扩展;
3. key-value存储系统;
4. 开源的使用ANSI C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、key-value数据库,并提供多种语言的API

Spring为什么要使用redis

缓存热点数据,对热点数据进行排序等

redis在Spring中的应用

  • Spring Boot中配置和使用Redis
    application.yml
# 配置redis连接池属性
spring:
  redis:
    jedis:
      pool:
        min-idle: 5
        max-active: 10
        max-idle: 10
        max-wait: 2000ms
    port: 6379
    host: 10.xx.xx.xx
    timeout: 1000ms
# 缓存管理器配置
  cache:
    cache-names: redisCache
    type: REDIS
#    redis:
#      cache-null-values: true #是否允许Redis缓存空值
#      key-prefix: # Redis的键前缀
#      time-to-live: 0ms # 缓存超时时间戳,配置为0则不设置超时时间
#      use-key-prefix: true # 是否启用Redis的键前缀
#

spring:
  datasource:
    name: ceph_manager
    # 坚炼测试机
    url: jdbc:mysql://10.xx.xx.xx:3306/ys?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&rewriteBatchedStatements=true&allowMultiQueries=true&useSSL=false&serverTimezone=GMT%2B8
    username: root
    password: 123456
    #druid相关配置
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    filters: stat
    maxActive: 20
    initialSize: 1
    maxWait: 60000
    minIdle: 1
    timeBetweenEvictionRunsMillis: 60000
    minEvictableIdleTimeMillis: 300000
    validationQuery: select 'x'
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    poolPreparedStatements: true
    maxOpenPreparedStatements: 20

## mybatis配置
mybatis:
  mapper-locations: classpath:mapping/*.xml  #注意:一定要对应mapper映射xml文件的所在路径
  type-aliases-package: com.example.springbootlocal.model  # 注意:对应实体类的路径

## 日志配置为DEBUG级别,这样日志最为详细
logging:
  level:
    root: DEBUG
    org:
      springframework: DEBUG
      org:
        mybatis: DEBUG

Application.java

package com.example.springbootlocal;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;

import javax.annotation.PostConstruct;

@EnableCaching
@SpringBootApplication(scanBasePackages = "com.example.springbootlocal")
public class SpringbootlocalApplication {

  @Autowired
  private RedisTemplate redisTemplate;

  /**
   * 利用Spring Bean声明周期的特性使用注解@PostConstruct自定义后初始化方法。
   */
  @PostConstruct
  public void init() {
    initRedisTemplate();
  }

  /**
   * 在RedisTemplate中它会默认地定义一个StringRedisSerializer对象,所以这里并没有自己创建一个新的StringRedisSerializer对象,
   * 而是从RedisTemplate中获取。然后把RedisTemplate关于键和其散列数据类型的filed都修改为了使用StringRedisSerializer进行
   * 序列化,这样在Redis服务器上得到的键和散列的field就都以字符串存储了。
   */
  public void initRedisTemplate() {
    RedisSerializer stringSerializer = redisTemplate.getStringSerializer();
    redisTemplate.setKeySerializer(stringSerializer);
    redisTemplate.setHashKeySerializer(stringSerializer);
  }

  public static void main(String[] args) {
    SpringApplication.run(SpringbootlocalApplication.class, args);
  }

}
  • redis基本使用
package com.example.springbootlocal.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisZSetCommands;
import org.springframework.data.redis.core.*;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import redis.clients.jedis.Jedis;

import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

@RestController
@RequestMapping("/redis")
public class RedisController {

  @Autowired
  private RedisTemplate redisTemplate;

  @Autowired
  private StringRedisTemplate stringRedisTemplate;

  @RequestMapping("/stringAndHash")
  public Map<String, Object> testStringAndHash() {
    redisTemplate.opsForValue().set("key1", "value1");
    // 注意这里使用了JDK的序列化器,所以Redis保存时不是整数,不能运算
    redisTemplate.opsForValue().set("int_key", "1");
    stringRedisTemplate.opsForValue().set("int", "1");
    // 使用运算
    stringRedisTemplate.opsForValue().increment("int", 1);

    // 获取底层Jedis连接
    Jedis jedis = (Jedis) stringRedisTemplate.getConnectionFactory().getConnection().getNativeConnection();
    // 减1操作,这个命令RedisTemplate不支持,所以先获取底层的连接操作
    jedis.decr("int");
    Map<String, String> hash = new HashMap<>();
    hash.put("field1", "value1");
    hash.put("field2", "value2");
    // 存入一个散列数据类型
    stringRedisTemplate.opsForHash().putAll("hash", hash);
    // 新增一个字段
    stringRedisTemplate.opsForHash().put("hash", "field3", "value3");
    // 绑定散列操作的key,这样可以连续对同一个散列数据类型进行操作
    BoundHashOperations hashOps = stringRedisTemplate.boundHashOps("hash");
    // 删除两个字段
    hashOps.delete("field1", "field2");
    // 新增一个字段
    hashOps.put("field4", "value5");
    Map<String, Object> map = new HashMap<>();
    map.put("success", true);

    return map;
  }

  @RequestMapping("/list")
  public Map<String, Object> testList() {
    // 插入两个列表,注意它们在链表的顺序
    // 链表从左到右顺序为v10, v8, v6, v4, v2
    stringRedisTemplate.opsForList().leftPushAll(
        "list", "v2", "v4", "v6", "v8", "v10"
    );
    // 链表从左到右顺序为v1, v2, v3, v4, v5, v6
    stringRedisTemplate.opsForList().rightPushAll(
        "list2", "v1", "v2", "v3", "v4", "v5", "v6"
    );
    // 绑定list2链表操作
    BoundListOperations<String, String> listOps = stringRedisTemplate.boundListOps("list2");
    // 从右边弹出一个成员
    Object result1 = listOps.rightPop();
    // 获取定位元素,Redis从0开始计算,这里值为v2
    Object result2 = listOps.index(1);
    // 从左边插入链表
    listOps.leftPush("v0");
    // 求链表长度
    Long size = listOps.size();
    // 求链表下标区间成员,整个链表下标范围为0到size-1,这里不取最后一个元素
    List elements = listOps.range(0, size - 2);
    Map<String, Object> map = new HashMap<>();
    map.put("success", true);

    return map;
  }

  @RequestMapping("/set")
  public Map<String, Object> testSet() {
    // 注意:这里v1重复两次,因为集合不允许重复,所以只是插入5个成员到集合中
    stringRedisTemplate.opsForSet().add("set1", "v1", "v1", "v2", "v3", "v4", "v5");
    stringRedisTemplate.opsForSet().add("set2", "v2", "v4", "v6", "v8");
    // 绑定set1集合操作
    BoundSetOperations setOps = stringRedisTemplate.boundSetOps("set1");
    // 增加两个元素
    setOps.add("v6", "v7");
    //删除两个元素
    setOps.remove("v1", "v7");
    // 返回所有元素
    Set set1 = setOps.members();
    // 求成员数
    Long size = setOps.size();
    // 求交集
    Set inter = setOps.intersect("set2");
    // 求交集,并且用新集合inter保存
    setOps.intersectAndStore("set2", "inter");
    // 求差集
    Set diff = setOps.diff("set2");
    // 求差集,并且用新集合diff保存
    setOps.diffAndStore("set2", "diff");
    // 求并集
    Set union = setOps.union("set2");
    // 求并集,并且用新集合union保存
    setOps.unionAndStore("set2", "union");
    Map<String, Object> map = new HashMap<>();
    map.put("success", true);

    return map;
  }

  /**
   * 一些网站中,经常会有排名,如最热门的商品或者最大的购买买家,都是常常见到的场景
   * 对于这类排名,刷新往往需要及时,也涉及较大的统计,如果使用数据库会太慢。
   * 为了支持集合的排序,Redis还提供了有序集合(zset)。有序集合与集合的差异并不大,它也是
   * 一种散列表存储方式,同时它的有序性只是靠它在数据结构中增加一个属性——score(分数)得以支持
   * @return
   */
  @RequestMapping("/zset")
  public Map<String, Object> testZset() {
    Set<ZSetOperations.TypedTuple<String>> typedTupleSet = new HashSet<>();
    for (int i = 1; i <= 9; i++) {
      // 分数
      double score = i*0.1;
      // 创建一个TypedTuple对象,存入值和分数
      ZSetOperations.TypedTuple<String> typedTuple = new DefaultTypedTuple<>("value" + i, score);
      typedTupleSet.add(typedTuple);
    }
    // 往有序集合插入元素
    stringRedisTemplate.opsForZSet().add("zset1", typedTupleSet);
    // 绑定zset1有序集合操作
    BoundZSetOperations<String, String> zSetOps = stringRedisTemplate.boundZSetOps("zset1");
    // 增加一个元素
    zSetOps.add("value10", 0.26);
    Set<String> setRange = zSetOps.range(1, 6);
    // 按分数排序获取有序集合
    Set<String> setScore = zSetOps.rangeByScore(0.2, 0.6);
    // 定义值范围
    RedisZSetCommands.Range range = new RedisZSetCommands.Range();
    range.gt("value3"); // 大于value3
    // range.gte("value3"); // 大于等于value3
    // range.lt("value8"); // 小于value8
    range.lte("value8"); // 小于等于value8
    // 按值排序,注意这个排序是按字符串排序
    Set<String> setLex = zSetOps.rangeByLex(range);
    // 删除排序
    zSetOps.remove("value9", "value2");
    // 求分数
    Double score = zSetOps.score("value8");
    // 在下标区间下,按分数排序,同时返回value和score
    Set<ZSetOperations.TypedTuple<String>> rangeSet = zSetOps.rangeWithScores(1, 6);
    // 在分数区间下,按分数排序,同时返回value和score
    Set<ZSetOperations.TypedTuple<String>> scoreSet = zSetOps.rangeByScoreWithScores(1, 6);
    // 按从大到小排序
    Set<String> reverseSet = zSetOps.reverseRange(2, 8);
    Map<String, Object> map = new HashMap<>();
    map.put("success", true);

    return map;
  }
}
  • 使用Redis事务
    在这里插入图片描述

在Redis中使用事务,通常的命令组合是watch…multi…exec,即要在一个Redis连接中执行多个命令,这时就可以考虑用SessionCallback接口来达到这个目的。

watch命令是可以监控Redis的一些键;
multi命令是开始事务,开始事务后,该客户端的命令不会马上被执行,而是存放在一个队列里,所以此时调用Redis命令,结果都是返回null;
exe命令的意义在于执行事务,只是它在队列命令执行前会判断被watch监控的Redis的键的数据是否发生过变化(即使赋予与之前相同的值也会被认为是变化过),如果它认为发生了变化,则Redis就会取消事务,否则就会执行事务

  /**
   * 使用Redis事务
   * @return
   */
  @RequestMapping("/multi")
  public Map<String, Object> testMulti() {
    redisTemplate.opsForValue().set("key1", "value1");

    List list = (List)redisTemplate.execute(new SessionCallback() {

      @Override
      public Object execute(RedisOperations redisOperations) throws DataAccessException {
        // 设置要监控key1
        redisOperations.watch("key1");
        // 开启事务,在exec命令执行前,全部都只是进入队列
        redisOperations.multi();
        redisOperations.opsForValue().set("key2", "value2");
        //redisOperations.opsForValue().increment("key1", 1);
        // 获取值将为null,因为redis只是把命令放入队列
        Object value2 = redisOperations.opsForValue().get("key2");
        System.out.println("命令在队列,所以value为null【" + value2 + "】");

        redisOperations.opsForValue().set("key3", "value3");
        Object value3 = redisOperations.opsForValue().get("key3");
        System.out.println("命令在队列,所以value为null【" + value3 + "】");
        // 执行exec命令,将先判别key1是否在监控后被修改过,如果是则不执行事务,否则就执行事务

        return redisOperations.exec();
      }
    });

    Map<String, Object> map = new HashMap<>();
    map.put("success", true);

    return map;
  }
  • 使用Redis流水线
  /**
   * 使用Redis流水线:
   * 默认情况下,Redis客户端是一条条命令发送给Redis服务器的,这样显然性能不高,
   * Redis也可以像关系数据库中批量执行命令,即流水线(pipeline)技术
   * @return
   */
  @RequestMapping("/pipeline")
  public Map<String, Object> testPipeline() {
    long start = System.currentTimeMillis();
    List list = redisTemplate.executePipelined(new SessionCallback<Object>() {
      @Override
      public Object execute(RedisOperations redisOperations) throws DataAccessException {
        for (int i = 1; i <= 100000; i++) {
          redisOperations.opsForValue().set("pipeline_" + i, "value_" + i);
          String value = (String) redisOperations.opsForValue().get("pipeline_" + i);
          if (i == 100000) {
            System.out.println("命令只是进入队列,所以值为空【" + value + "】");
          }
        }
        return null;
      }
    });

    long end = System.currentTimeMillis();
    System.out.println("耗时:" + (end - start) + "毫秒。");
    Map<String, Object> map = new HashMap<>();
    map.put("success", true);

    return map;
  }
  • 使用Redis发布订阅

  • Spring缓存实践——使用Spring缓存注解操作Redis

POJO类——User.java 对应数据库的表

package com.example.springbootlocal.model;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import javax.validation.constraints.NotNull;
import java.io.Serializable;

@Data
@ApiModel(description = "用户实体")
public class User implements Serializable {
  private static final long serialVersionUID = 77732723737327233L;

  @ApiModelProperty("用户编号")
  private Long id;

  @NotNull
  @ApiModelProperty("用户姓名")
  private String userName;

  @ApiModelProperty("note")
  private String note;

  @NotNull
  @ApiModelProperty("用户年龄")
  private int age;

}

MyBatis用户操作接口: UserDao.java

package com.example.springbootlocal.dao;

import com.example.springbootlocal.model.User;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.data.repository.query.Param;

import java.util.List;

@Mapper
public interface UserDao {

  User getUser(Long id);

  int insertUser(User user);

  int updateUser(User user);

  List<User> findUsers(@Param("userName") String userName,
                       @Param("note") String note);

  int deleteUser(Long id);

}

定义用户SQL和映射关系: UserDaoMapping.xml 这里需要注意通过将属性useGeneratedKeys设置为true,代表将通过数据库生成主键,而将keyProperty设置为POJO的id属性,MyBatis就会将数据库生成的主键回填到POJO的id属性中

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.example.springbootlocal.dao.UserDao" >

  <resultMap id="user" type="com.example.springbootlocal.model.User">
    <id column="id" property="id" jdbcType="INTEGER" />
    <result column="userName" property="userName" jdbcType="VARCHAR" />
    <result column="age" property="age" jdbcType="INTEGER" />
    <result column="note" property="note" jdbcType="VARCHAR" />
  </resultMap>

  <sql id="Base_Column_List" >
    id, userName, age, note
  </sql>

  <select id="getUser" parameterType="long" resultType="com.example.springbootlocal.model.User">
    SELECT id, userName, note age
    FROM ys_user
    WHERE id = #{id}
  </select>

  <insert id="insertUser" useGeneratedKeys="true" keyProperty="id" parameterType="com.example.springbootlocal.model.User">
    INSERT INTO ys_user(userName, age, note)
    VALUES (#{userName}, #{age}, #{note})
  </insert>

  <update id="updateUser">
    UPDATE ys_user
    <set>
      <if test="userName != NULL">
        userName = #{userName},
      </if>
      <if test="age != NULL">
        age = #{age},
      </if>
      <if test="note != NULL">
        note = #{note}
      </if>
    </set>
    WHERE id = #{id}
  </update>

  <select id="findUsers" resultType="com.example.springbootlocal.model.User">
    SELECT <include refid="Base_Column_List" />
    FROM ys_user
    <where>
      <if test="userName != NULL">
        userName = #{userName},
      </if>
      <if test="note != NULL">
        note = #{note}
      </if>
    </where>
  </select>

  <delete id="deleteUser" parameterType="long">
    DELETE FROM ys_user
    WHERE id = #{id}
  </delete>

</mapper>

Spring MVC中的servicer层的业务处理接口:UserService.java

package com.example.springbootlocal.service;

import com.example.springbootlocal.model.User;
import java.util.List;

public interface UserService {

  User getUser(Long id);
  User insertUser(User user);
  User updateUserName(Long id, String userName);
  List<User> findUsers(String userName, String note);
  int deleteUser(Long id);
}

service层实现——UserServiceImpl.java

package com.example.springbootlocal.serviceImpl;

import com.example.springbootlocal.dao.UserDao;
import com.example.springbootlocal.model.User;
import com.example.springbootlocal.service.UserService;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.util.List;

@Service
public class UserServiceImpl implements UserService {

  @Resource
  private UserDao userDao;

  /**
   * 插入用户,最后MyBatis会回填id,取结果id缓存用户
   * @param user
   * @return
   */
  @Override
  @Transactional
  @CachePut(value = "redisCache", key = "'redis_user_'+#result.id")
  public User insertUser(User user) {
    userDao.insertUser(user);
    return user;
  }

  /**
   * 获取id,取参数id缓存用户
   * @param id
   * @return
   */
  @Override
  @Transactional
  @Cacheable(value = "redisCache", key = "'redis_user_'+#id")
  public User getUser(Long id) {
    return userDao.getUser(id);
  }

  /**
   * 更新数据后,更新缓存,如果condition配置项使结果返回为null,不缓存
   * @param id
   * @param userName
   * @return
   */
  @Override
  @Transactional
  @CachePut(value = "redisCache", condition = "#result != 'null'", key = "'redis_user_'+#id")
  public User updateUserName(Long id, String userName) {
    User user = this.getUser(id);
    if (user == null) {
      return null;
    }
    user.setUserName(userName);
    userDao.updateUser(user);

    return user;
  }

  /**
   * 命中率低,所以不采用缓存机制
   * @param userName
   * @param note
   * @return
   */
  @Override
  @Transactional
  public List<User> findUsers(String userName, String note) {
    return userDao.findUsers(userName, note);
  }

  /**
   * 移除缓存
   * @param id
   * @return
   */
  @Override
  @Transactional
  @CacheEvict(value = "redisCache", key = "'redis_user_'+#id", beforeInvocation = false)
  public int deleteUser(Long id) {
    return userDao.deleteUser(id);
  }
}

下面通过以接口的形式来测试缓存注解:UserController.java

package com.example.springbootlocal.controller;

import com.example.springbootlocal.model.User;
import com.example.springbootlocal.service.UserService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/users")
public class UserController {

  @Resource
  private UserService userService;

  @RequestMapping("/getUser")
  public User getUser(Long id) {
    return userService.getUser(id);
  }

  @RequestMapping("/insertUser")
  public User insertUser(String userName, String note) {
    User user = new User();
    user.setUserName(userName);
    user.setNote(note);
    userService.insertUser(user);

    return user;
  }

  @RequestMapping("/findUsers")
  public List<User> findUsers(String userName, String note) {
    return userService.findUsers(userName, note);
  }

  @RequestMapping("/updateUserName")
  public Map<String, Object> updateUserName(Long id, String userName) {
    User user = userService.updateUserName(id, userName);
    boolean flag = user != null;
    String message = flag ? "更新成功" : "更新失败";

    return resultMap(flag, message);
  }

  @RequestMapping("/deleteUser")
  public Map<String, Object> deleteUser(Long id) {
    int result = userService.deleteUser(id);
    boolean flag = result == 1;
    String message = flag ? "删除成功" : "删除失败";

    return resultMap(flag, message);
  }

  private Map<String, Object> resultMap(boolean success, String message) {
    Map<String, Object> result = new HashMap<String, Object>();

    result.put("success", success);
    result.put("message", message);

    return result;
  }
}

@CachePut表示将方法结果返回存放到缓存中;

@Cacheable表示先从缓存中通过定义的键查询,如果可以查询到数据,则返回,否则执行该方法,返回数据,并且将返回结果保存到缓存中;

@CacheEvict通过定义的键移除缓存,它有一个Boolean类型的配置项beforeInvocation,表示在方法之前或者之后移除缓存。因为其默认值为false,所以默认为方法之后将缓存移除。

  • 自定义缓存管理器
    缓存注解自调用失效问题:

正如上面描述的问题所说,我们并不希望采用Spring Boot机制带来的键命名方式,也不希望缓存永不超时,这时我们可以自定义缓存管理器。
有两种方法定制缓存管理器:

  1. 通过配置消除缓存键的前缀和自定义超时时间的属性来定制生成RedisCacheManager;
  2. 另一种是不采用Spring Boot为我们生成的方式,而是完全通过自己的代码创建缓存管理器,尤其是当需要比较多自定义的时候,更加推荐你采用自定义的代码;

附录

  • 参考文章
    《深入浅出Spring Boot2.x》
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值