Spring Boot 学习笔记 05——使用 Redis

247 阅读5分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

Redis 是一种运行在内存的数据库,支持 7 种数据类型的存储。Redis 是一个开源、使用 ANSIC 语言编写、遵守 BSD 协议、支持网络、可基于内存亦可持久化的日志型、键值数据库,并提供多种语言的 API。Redis 是基于内存的,所以运行速度很快,大约是关系数据库几倍到几十倍的速度。在测试中,Redis 可以在 1s 内完成 10 万次的读写,性能十分高效。如果我们将常用的数据存储在 Redis 中,用来代替关系数据库的查询访问,网站性能将可以得到大幅提高。

RedisTemplate

创建 RedisTemplate

@Bean(name = redisTemplate)
public RedisTemplate<Object, Object> initRedisTemplate() {
  RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
  // RedisTemplate 会自动初始化 StringRedisSerializer,所以这里直接获取
  RedisSerializer stringRedisSerializer = redisTemplate.getStringSerializer();
  // 设置字符串序列化器,这样 Spring 就会把 Redis 的 key 当作字符串处理了
  redisTemplate.setKeySerializer(stringRedisSerializer);
  redisTemplate.setHashKeySerializer(stringRedisSerializer);
  redisTemplate.setHashValueSerializer(stringRedisSerializer);
  redisTemplate.setConnectionFactory(initConnectionFactory());
  return redisTemplate;
}

使用 SessionCallBack 接口

// 让 RedisTemplate 回调,在同一连接下执行多个 Redis 命令
public void useSessionCallback(RedisTemplate redisTemplate) {
  redisTemplate.execute(new SessionCallback() {
    @Override
    public Object execute(RedisOperation ro) throws DataAccessException {
      ro.opsForValue().set("key1", "value1");
      ro.opsForHash().put("hash", "field", "hvalue");
      return null;
    }
  });
}
// 也可以使用 Lambda 表达式改写代码
public void useSessionCallback(RedisTemplate redisTemplate) {
  redisTemplate.execute((RedisOperation ro) -> {
    ro.opsForValue().set("key1", "value1");
    ro.opsForHash().put("hash", "field", "hvalue");
    return null;
  });
}

在 Spring Boot 中配置和使用 Redis

配置文件 application.yml

spring:
	redis:
		# 配置连接池属性
		jedis:
			pool:
				min-idle: 5
				max-active: 10
				max-idle: 10
				max-wait: 2000
		# 配置 Redis 服务器属性
		port: 6379
		host: 192.168.11.131
		password: 123456
    # Redis 连接超时时间,单位主运秒
		timeout: 1000

RedisTemplate 会默认使用 JdkSerializationRedisSerializer 进行序列化键值,这样便能够存储到 Redis 服务器中。Redis 服务器存入的便是一个经过序列化后的特殊字符串,有时候对于跟踪并不是很友好 。如果我们在 Redis 只是使用字符串 ,那么使用其自动生成的 StringRedisTemplate 即可,但是这样就只能支持字符串了,并不能支持 Java 对象的存储。为了克服这个问题,可以通过设置 RedisTemplate 的序列化器来处理。

自定义序列化(推荐使用 StringRedisTemplate)

为 Redis 客户端查看操作数据, redisTemplate 需要进行序列化设置, 默认配置的 jdk 序列化会导致在客户端查看不了数据(仍可使用内在函数存取修改, 只是查看不了), 为避免这种情况发生, 使用 StringRedisTemplate 或自行配置序列化, 自行配置可参考如下代码:

/**** imports ****/
@Configuration
public class MyRedisConfig {

  @Bean(name = "redisTemplate")
  public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){

    RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
    ////参照StringRedisTemplate内部实现指定序列化器
    redisTemplate.setConnectionFactory(redisConnectionFactory);
    redisTemplate.setKeySerializer(keySerializer());
    redisTemplate.setHashKeySerializer(keySerializer());
    redisTemplate.setValueSerializer(valueSerializer());
    redisTemplate.setHashValueSerializer(valueSerializer());
    return redisTemplate;
  }

  private RedisSerializer<String> keySerializer(){
    return new StringRedisSerializer();
  }

  //使用Jackson序列化器
  private RedisSerializer<Object> valueSerializer(){
    return new GenericJackson2JsonRedisSerializer();
  }
}

简单测试代码 test:

@RunWith(SpringRunner.class)
@SpringBootTest
public class MyConfigRedisTemplateTest {

  @Autowired
  // 在 MyRedisConfig 文件中配置了 redisTemplate 的序列化之后,客户端也能正确显示键值对了
  private RedisTemplate redisTemplate; 

  @Test
  public void test(){
    redisTemplate.opsForValue().set("wujinxing", "lige");
    System.out.println(redisTemplate.opsForValue().get("wujinxing"));
    Map<String, Object> map = new HashMap<>();
    for (int i=0; i<10; i++){
      User user = new User();
      user.setId(i);
      user.setName(String.format("测试%d", i));
      user.setAge(i+10);
      map.put(String.valueOf(i),user);
    }
    redisTemplate.opsForHash().putAll("测试", map);
    BoundHashOperations hashOps = redisTemplate.boundHashOps("测试");
    Map map1 = hashOps.entries();
    System.out.println(map1);
  }
  static class User implements Serializable {
    private int id;
    private String name;
    private long age;
    // 省略getter, setter, toString...
  }
}

操作 Redis 数据类型(String, hash, set 等)

常见操作(均在 Controller 上使用, 仅做测试, 实际项目应在 Service 层使用 Redis):

/**** imports ****/
@Controller
@RequestMapping("/redis")
public class RedisController {

  private static final Logger LOGGER = LoggerFactory.getLogger(RedisController.class);

  @Autowired
  private RedisTemplate redisTemplate = null ;

  @Autowired
  private StringRedisTemplate stringRedisTemplate = null ;

  @RequestMapping("/stringAndHash")
  @ResponseBody
  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<String, String>();
    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("filed4", "value5");
    Map<String, Object> map = new HashMap<String, Object>();
    map.put("success", true);
    return map;
  }

  @RequestMapping("/list")
  @ResponseBody
  public Map<String, Object> testList(){
    //链表从左到右的顺序为v10, v8, v6, v4, v2
    stringRedisTemplate.opsForList().leftPushAll("list1", "v2","v4","v6","v8","v10");
    //链表从左到右的顺序为v1, v3, v5, v7, v9
    stringRedisTemplate.opsForList().rightPushAll("list2", "v1","v3","v5","v7","v9");

    //绑定list2操作链表
    BoundListOperations listOps = stringRedisTemplate.boundListOps("list2");
    Object result1 = listOps.rightPop();//从右边弹出一个成员
    LOGGER.info("list2的最右边元素为: "+result1.toString());

    Object result2 = listOps.index(1); //获取定位元素, 下标从0开始
    LOGGER.info("list2下标为1的元素为"+result2.toString());

    listOps.leftPush("v0"); //从左边插入链表

    Long size = listOps.size();//求链表长
    LOGGER.info("list2的长度为: "+size);

    List element = listOps.range(0, size-2); //求链表区间成员
    LOGGER.info("list2从0到size-2的元素依次为: "+element.toString());

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

  @RequestMapping("/set")
  @ResponseBody
  public Map<String, Object> testSet(){
    //重复的元素不会被插入
    stringRedisTemplate.opsForSet().add("set1", "v1","v1","v3","v5","v7","v9");
    stringRedisTemplate.opsForSet().add("set2", "v2","v4","v6","v5","v10","v10");

    //绑定sert1集合操作
    BoundSetOperations setOps = stringRedisTemplate.boundSetOps("set1");
    setOps.add("v11", "v13");
    setOps.remove("v1", "v3");
    Set set = setOps.members();//返回所有元素
    LOGGER.info("集合中所有元素: "+set.toString());

    Long size = setOps.size();//求成员数
    LOGGER.info("集合长度: "+String.valueOf(size));

    Set inner = setOps.intersect("set2"); //求交集
    setOps.intersectAndStore("set2", "set1_set2");//求交集并用新的集合保存
    LOGGER.info("集合的交集: "+inner.toString());

    Set diff = setOps.diff("set2"); //求差集
    setOps.diffAndStore("set2","set1-set2"); //求差集并用新的集合保存
    LOGGER.info("集合的差集: "+diff.toString());

    Set union = setOps.union("set2"); //求并集
    setOps.unionAndStore("set2", "set1=set2"); //求并集并用新的集合保存
    LOGGER.info("集合的并集: "+union.toString());

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

  /**
     * redis操作有序集合
     * @return
     */
  @RequestMapping("/zset")
  @ResponseBody
  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 typedTuple = new DefaultTypedTuple<String>("value" + i, score);
      typedTupleSet.add(typedTuple);
    }
    LOGGER.info("新建的set: "+typedTupleSet.toString());
    //往有序集合插入元素
    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);
    LOGGER.info("下标下1-6的set: " + setRange.toString());

    //按分数排序获取有序集合
    Set<String> setScore = zSetOps.rangeByScore(0.2, 0.6);
    LOGGER.info("按分数排序获取有序集合: "+ setScore.toString());

    //定义值范围
    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);
    LOGGER.info("按值排序: "+setLex.toString());

    zSetOps.remove("value9", "value2");  //删除元素
    Double score = zSetOps.score("value8"); //求分数
    LOGGER.info("求value8的分数: "+score);

    //在下标区间 按分数排序, 同时返回value和score
    Set<ZSetOperations.TypedTuple<String>> rangeSet = zSetOps.rangeWithScores(1,6);
    LOGGER.info("在下标区间 按分数排序, 同时返回value和score:  "+rangeSet.toString());

    //在下标区间 按分数排序, 同时返回value和score
    Set<ZSetOperations.TypedTuple<String>> scoreSet = zSetOps.rangeByScoreWithScores(1,6);
    LOGGER.info("在下标区间 按分数排序, 同时返回value和score:  "+scoreSet.toString());

    //按从大到小排序
    Set<String> reverseSet = zSetOps.reverseRange(2, 8);
    LOGGER.info("按从大到小排序: "+reverseSet.toString());

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

  @RequestMapping("/multi")
  @ResponseBody
  public Map<String, Object> testMulti(){
    stringRedisTemplate.opsForValue().set("key1", "value1");

    /*List list = (List) stringRedisTemplate.execute((RedisOperations operations)->{
            operations.watch("key1");
            operations.multi();
            operations.opsForValue().set("key2", "value2");
            //operations.opsForValue().increment("key1", 1);
            //获取的值将为null, 因为redis知识把命令放入队列
            Object value2 = operations.opsForValue().get("key2");
            System.out.println("命令在队列, 所以value2为null [ " + value2 + " ] ");
            operations.opsForValue().set("key3", "value3");
            Object value3 = operations.opsForValue().get("key3");
            System.out.println("命令在队列, 所以value3为null [ " + value3 + " ] ");

            //执行exce()命令,将先判断key1是否在监控后被修改过, 如果是则不执行事务, 否则就执行事务
            return operations.exec();
        });
        System.out.println(list);*/
    Map<String, Object> map = new HashMap<>();
    map.put("success", true);
    return map;
  }
}

Service 层使用 Redis

@Service
public class CityServiceImpl implements CityService {

  private static final Logger LOGGER = LoggerFactory.getLogger(CityServiceImpl.class);

  @Autowired
  private CityMapper cityMapper;

  @Autowired
  private RedisTemplate redisTemplate;

  /**
     * 获取城市逻辑:
     * 如果缓存存在,从缓存中获取城市信息
     * 如果缓存不存在,从 DB 中获取城市信息,然后插入缓存
     */
  @Override
  public City findCityById(Long id){
    //从缓存中获取城市信息
    String key = "city_"+id;
    ValueOperations<String,City> operations = redisTemplate.opsForValue();

    //缓存存在
    boolean hasKey = redisTemplate.hasKey(key);
    if(hasKey){
      City city = operations.get(key);
      LOGGER.info("CityServiceImpl.findCityById() : 从缓存中获取了城市 >> " + city.toString());
      return city;
    }
    //从DB中获取城市
    City city = cityMapper.findById(id);

    //插入缓存
    operations.set(key,city,10,TimeUnit.SECONDS); //缓存的时间仅有十秒钟
    LOGGER.info("CityServiceImpl.findCityById() : 城市插入缓存 >> " + city.toString());
    LOGGER.info("刚才加入redis的数据是: "+operations.get(key));
    return city;
  }

  @Override
  public Long saveCity(City city) {
    return cityMapper.saveCity(city);
  }

  /**
     * 更新城市逻辑:
     * 如果缓存存在,删除
     * 如果缓存不存在,不操作
     */
  @Override
  public Long updateCity(City city) {
    Long ret = cityMapper.updateCity(city);

    //缓存存在,删除缓存
    String key = "city_" + city.getId();
    boolean hasKey = redisTemplate.hasKey(key);
    if (hasKey){
      redisTemplate.delete(key);
      LOGGER.info("CityServiceImpl.updateCity() : 从缓存中删除城市 >> " + city.toString());
    }
    return ret;
  }

  @Override
  public Long deleteCity(Long id) {
    Long ret = cityMapper.deleteCity(id);

    String key = "city_" + id;
    boolean hasKey = redisTemplate.hasKey(key);
    if(hasKey){
      redisTemplate.delete(key);
      LOGGER.info("CityServiceImpl.deleteCity() : 从缓存中删除城市 ID >> " + id);
    }
    return ret;
  }
}