2025年Redis的使用

Redis的使用1 简介 Redis 是一款基于内存的 NoSQL 数据存储服务 是非关系型的 是使用 K V 结构进行存储的 基于内存 读写数据均在内存中直接操作 NoSQL 通常把能够存 取数据的服务都称之为数据库 所以 Redis 也是数据库 但是 它没有 SQL 语句 2 基本使用 1

大家好,我是讯享网,很高兴认识大家。

1.简介

  • Redis是一款基于内存的NoSQL数据存储服务,是非关系型的,是使用K-V结构进行存储的
    • 基于内存:读写数据均在内存中直接操作
    • NoSQL:通常把能够存、取数据的服务都称之为数据库,所以,Redis也是数据库,但是,它没有SQL语句

2.基本使用

1.依赖项

  • 在基于Spring Boot的开发中,当需要在程序中访问Redis中的数据时,需要添加spring-boot-starter-data-redis依赖项。
<!--非关系型数据库 spring-boot-starter-data-redis 支持对rids编程--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>2.5.9</version> </dependency> 

讯享网
  • 注: 版本需要与spring-boot的版本一致

2.配置类

  • 要操作Redis中的数据,需要使用RedisTemplate对象,则在根包下的config包中创建RedisConfiguration类,并在其中进行配置:
讯享网/ * redis配置类 */ @Configuration public class RedisConfiguration { 
    / * redisTemplate 序列化使用的jdk序列化方式 * @param redisConnectionFactory * @return */ @Bean public RedisTemplate<String, Serializable> redisTemplate(RedisConnectionFactory redisConnectionFactory) { 
    RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>(); // 创建redisTemplate redisTemplate.setConnectionFactory(redisConnectionFactory); // 设置redis连接工厂 redisTemplate.setKeySerializer(RedisSerializer.string()); // 设置key序列化方式 redisTemplate.setValueSerializer(RedisSerializer.json()); // 设置value序列化方式 return redisTemplate; } } 

3.基本使用

  • 接下来,在测试的根包下创建RedisTests来测试访问Redis中的数据:
 @SpringBootTest public class RedisTests { 
    @Autowired RedisTemplate<String, Serializable> redisTemplate; @Test void testSetValue() { 
    redisTemplate.opsForValue().set("name", "liuguobin"); } @Test void testSetValueTTL() { 
    redisTemplate.opsForValue() .set("name", "fanchuanqi", 60, TimeUnit.SECONDS); } @Test void testSetObjectValue() { 
    CategoryDetailsVO category = new CategoryDetailsVO(); category.setId(65L); category.setIsParent(1); category.setDepth(1); category.setName("水果"); redisTemplate.opsForValue() .set("category", category); } @Test void testGetValue() { 
    // 当key存在时,可获取到有效值 // 当key不存在时,获取到的结果将是null Serializable name = redisTemplate.opsForValue() .get("name"); System.out.println("get value --> " + name); } @Test void testGetObjectValue() { 
    // 当key存在时,可获取到有效值 // 当key不存在时,获取到的结果将是null Serializable serializable = redisTemplate.opsForValue() .get("category"); System.out.println("get value --> " + serializable); if (serializable != null) { 
    CategoryDetailsVO category = (CategoryDetailsVO) serializable; System.out.println("get value --> " + category); } } @Test void testDeleteKey() { 
    // 删除key时,将返回“是否成功删除” // 当key存在时,将返回true // 当key不存在时,将返回false Boolean result = redisTemplate.delete("name"); System.out.println("result --> " + result); } @Test void testRightPushList() { 
    // 存入List时,需要redisTemplate.opsForList()得到针对List的操作器 // 通过rightPush()可以向Redis中的List追加数据 // 每次调用rightPush()时使用的key必须是同一个,才能把多个数据放到同一个List中 List<CategoryDetailsVO> list = new ArrayList<>(); for (int i = 1; i <= 5; i++) { 
    CategoryDetailsVO category = new CategoryDetailsVO(); category.setName("类别00" + i); list.add(category); } String key = "categoryList"; for (CategoryDetailsVO category : list) { 
    redisTemplate.opsForList().rightPush(key, category); } } @Test void testListSize() { 
    // 获取List的长度,即List中的元素数量 String key = "categoryList"; Long size = redisTemplate.opsForList().size(key); System.out.println("size --> " + size); } @Test void testRange() { 
    // 调用opsForList()后再调用range(String key, long start, long end)方法取出List中的若干个数据,将得到List // long start:起始下标(结果中将包含) // long end:结束下标(结果中将包含),如果需要取至最后一个元素,可使用-1作为此参数值 String key = "categoryList"; List<Serializable> range = redisTemplate.opsForList().range(key, 0, -1); for (Serializable serializable : range) { 
    System.out.println(serializable); } } @Test void testKeys() { 
    // 调用keys()方法可以找出匹配模式的所有key // 在模式中,可以使用星号作为通配符 Set<String> keys = redisTemplate.keys("*"); for (String key : keys) { 
    System.out.println(key); } } } 

4.使用场景

  • 使用Redis可以提高查询效率,一定程度上可以减轻数据库服务器的压力,从而保护了数据库。
  • 通常,应用Redis的场景有:
    • 高频查询,例如:热搜列表、秒杀
    • 改变频率低的数据,例如:商品类别
  • 一旦使用Redis,就会导致Redis和数据库中都存在同样的数据,当数据发生变化时,可能出现不一致的问题!
  • 所以,还有某些数据在特定的场景中不能使用Redis:
    • 要求数据必须是准确的:下单购买时要求库存是准确的
      • 如果每次库存发生变化时都更新了Redis中的库存值,保证了Redis中的数据是准确的,也可以使用
    • 数据的修改频率很高,且对数据准确性有一定要求
      需要学会评估是否要求数据一定保持一致!

3.示例:

  • 要使用Redis缓存数据,至少需要:
    • 开发新的组件,实现对Redis中的数据访问
      • 此组件并不是必须的,因为访问Redis数据的API都非常简单,自定义组件时,组件中的每个方法可能也只有少量代码,甚至只有1行代码
      • 如果直接将访问Redis的代码写在Service中,首次开发时会更省事,但不利于维护
      • 【推荐】如果将访问Redis的代码写的新的组件中,首次开发时会更麻烦,但利于维护
    • 在Service中调用新的组件,在Service中决定何时访问MySQL,何时访问Redis
  • 在使用Redis之前,还必须明确一些问题:
    • 哪些查询功能改为从Redis中获取数据
    • Redis中的数据从哪里来
  • 暂定目标:
    • 根据类别的id查询类别详情,改为从Redis中获取数据
    • 优先从Redis中获取数据,如果Redis中没有,则从MySQL中获取,且获取到数据后,将数据存入到Redis中,所以,经过首次查询后,Redis中将存在此数据,后续每一次都可以直接从Redis中拿到必要的数据
  • repository创建ICategoryRedisRepository接口,并在接口中添加抽象方法:
讯享网public interface ICategoryRedisRepository { 
    String KEY_CATEGORY_ITEM_PREFIX = "categories:item:"; // 将类别详情存入到Redis中 void save(CategoryDetailsVO category); // 根据类别id获取类别详情 CategoryDetailsVO getDetailsById(Long id); } 
  • 然后在repository.impl创建CategoryRedisRepositoryImpl(接口的实现类),实现以上接口:
@Repository public class CategoryRedisRepositoryImpl implements ICategoryRedisRepository { 
    @Autowired private RedisTemplate<String, Serilizalbe> redisTemplate; @Override public void save(CategoryDetailsVO category) { 
    String key = KEY_CATEGORY_ITEM_PREFIX + category.getId(); redisTemplate.opsForValue().set(key, category); } @Override public CategoryDetailsVO getDetailsById(Long id) { 
    String key = KEY_CATEGORY_ITEM_PREFIX + id; Serializable result = redisTemplate.opsForValue().get(key); if (result == null) { 
    return null; } else { 
    CategoryDetailsVO category = (CategoryDetailsVO) result; return category; } } } 
  • 为了避免缓存穿透,需要在ICategoryRedisRepository中添加2个抽象方法:
讯享网/ * 判断是否存在id对应的缓存数据 * * @param id 类别id * @return 存在则返回true,否则返回false */ boolean exists(Long id); / * 向缓存中写入某id对应的空数据(null),此方法主要用于解决缓存穿透问题 * * @param id 类别id */ void saveEmptyValue(Long id); 
  • 并在CategoryRedisRepositoryImpl中补充实现:
@Override public boolean exists(Long id) { 
    String key = KEY_CATEGORY_ITEM_PREFIX + id; return redisTemplate.hasKey(key); } @Override public void saveEmptyValue(Long id) { 
    String key = KEY_CATEGORY_ITEM_PREFIX + id; redisTemplate.opsForValue().set(key, null); } 
  • 业务中实现
讯享网@Override public CategoryDetailsVO getDetailsById(Long id) { 
    log.debug("根据id({})获取类别详情……", id); // 从repository中调用方法,根据id获取缓存的数据 // 判断缓存中是否存在与此id对应的key boolean exists = categoryRedisRepository.exists(id); if (exists) { 
    // 有:表示明确的存入过某数据,此数据可能是有效数据,也可能是null // -- 判断此key对应的数据是否为null CategoryDetailsVO cacheResult = categoryRedisRepository.getDetailsById(id); if (cacheResult == null) { 
    // -- 是:表示明确的存入了null值,则此id对应的数据确实不存在,则抛出异常 log.warn("在缓存中存在此id()对应的Key,却是null值,则抛出异常", id); throw new ServiceException(State.ERR_CATEGORY_NOT_FOUND, "获取类别详情失败,尝试访问的数据不存在!"); } else { 
    // -- 否:表示明确的存入了有效数据,则返回此数据即可 return cacheResult; } } // 缓存中没有此id匹配的数据 // 从mapper中调用方法,根据id获取数据库的数据 log.debug("没有命中缓存,则从数据库查询数据……"); CategoryDetailsVO dbResult = categoryMapper.getDetailsById(id); // 判断从数据库中获取的结果是否为null if (dbResult == null) { 
    // 是:数据库也没有此数据,先向缓存中写入错误数据,再抛出异常 log.warn("数据库中也无此数据(id={}),先向缓存中写入错误数据", id); categoryRedisRepository.saveEmptyValue(id); log.warn("抛出异常"); throw new ServiceException(State.ERR_CATEGORY_NOT_FOUND, "获取类别详情失败,尝试访问的数据不存在!"); } // 将从数据库中查询到的结果存入到缓存中 log.debug("已经从数据库查询到匹配的数据,将数据存入缓存……"); categoryRedisRepository.save(dbResult); // 返回查询结果 log.debug("返回查询到数据:{}", dbResult); return dbResult; } 
  • 许多缓存数据应该是服务器刚刚启动就直接写入到Redis中的,当后续客户端访问时,缓存中已经存在的数据可以直接响应,避免获取数据时缓存中还没有对应的数据,还需要从数据库中查询。
  • 在服务器刚刚启动时就加载需要缓存的数据并写入到Redis中,这种做法称之为缓存预热。
  • 需要解决的问题有:
    • 需要实现开机启动时自动执行某个任务
    • 哪些数据需要写入到缓存中,例如全部“类别”数据
  • 在Spring Boot中,可以自定义某个组件类,实现ApplicationRunner即可,例如:
package cn.tedu.csmall.product.webapi.app; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.stereotype.Component; @Component public class CachePreLoad implements ApplicationRunner { 
    @Override public void run(ApplicationArguments args) throws Exception { 
    System.out.println("CachePreLoad.run()"); } } 

为了将全部“类别”写入到缓存中,首先,需要能够从数据库中查询到全部数据,则需要:

  • CategoryMapper接口中添加:List<CategoryDetailsVO> list();
  • CategoryMapper.xml中配置以上抽象方法映射的SQL语句
    然后,还需要实现将查询到的List<CategoryDetailsVO>写入到Redis中,则需要:
  • ICategoryRedisRepository接口中添加:void save(List<CategoryDetailsVO> categories);
  • CategoryRedisRepositoryImpl中实现以上方法
    • 存入时,Key值可以是:categories:list
      由于向Redis中存入列表数据始终是“追加”的,且Redis中的数据并不会因为项目重启而消失,所以,如果反复启动项目,会在Redis的列表中反复追加重复的数据!为了避免此问题,应该在每次缓存预热之间先删除现有数据,所以,还需要:
  • ICategoryRedisRepository接口中添加:Boolean deleteList();
  • CategoryRedisRepositoryImpl中实现以上方法
    从设计的角度,Service是可以调用数据访问层的组件的,即可以调用Mapper或其它Repository组件,换言之,Mapper和其它Repository组件应该只被Service调用!


    讯享网

所以,应该在ICategoryService中定义“预热类别数据的缓存”的抽象方法:

讯享网void preloadCache(); 

另外,在Redis中存入了整个“类别”的列表后,也只能一次性拿到整个列表,不便于根据“类别”的id获取指定的数据,反之,如果每个“类别”数据都独立的存入到Redis中,当需要获取整个列表时,也只能把每个数据都找出来,然后再在Java程序中存入到List集合中,操作也是不方便的,所以,当需要更加关注效率时,应该将类别数据存2份到Redis中,一份是整个列表,另一份是若干个独立的类别数据。
目前,在缓存中存入独立的各个类别数据,在预热时并没有清除这些数据,如果在数据库中删除了数据,但缓存中的数据仍存在,为了避免这样的错误,应该在预热时,补充“删除所有类别”的功能!

则在ICategoryRedisRepository中添加void deleteAllItem();方法,用于删除所有独立的类别数据。

相关代码:ICategoryRedisRepository

package cn.tedu.csmall.product.webapi.repository; import cn.tedu.csmall.pojo.vo.CategoryDetailsVO; import java.util.List; public interface ICategoryRedisRepository { 
    / * 类别数据的KEY的前缀 */ String KEY_CATEGORY_ITEM_PREFIX = "categories:item:"; / * 类别列表的KEY */ String KEY_CATEGORY_LIST = "categories:list"; / * 判断是否存在id对应的缓存数据 * * @param id 类别id * @return 存在则返回true,否则返回false */ Boolean exists(Long id); / * 向缓存中写入某id对应的空数据(null),此方法主要用于解决缓存穿透问题 * * @param id 类别id */ void saveEmptyValue(Long id); / * 将类别详情存入到Redis中 * * @param category 类别详情 */ void save(CategoryDetailsVO category); / * 将类别的列表存入到Redis中 * * @param categories 类别列表 */ void save(List<CategoryDetailsVO> categories); / * 删除Redis中各独立存储的类别数据 */ void deleteAllItem(); / * 删除Redis中的类别列表 * @return 如果成功删除,则返回true,否则返回false */ Boolean deleteList(); / * 根据类别id获取类别详情 * * @param id 类别id * @return 匹配的类别详情,如果没有匹配的数据,则返回null */ CategoryDetailsVO getDetailsById(Long id); } 

相关代码:CategoryRedisRepositoryImpl

讯享网package cn.tedu.csmall.product.webapi.repository.impl; import cn.tedu.csmall.pojo.vo.CategoryDetailsVO; import cn.tedu.csmall.product.webapi.repository.ICategoryRedisRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Repository; import java.io.Serializable; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; @Repository public class CategoryRedisRepositoryImpl implements ICategoryRedisRepository { 
    @Autowired private RedisTemplate<String, Serializable> redisTemplate; @Override public Boolean exists(Long id) { 
    String key = KEY_CATEGORY_ITEM_PREFIX + id; return redisTemplate.hasKey(key); } @Override public void saveEmptyValue(Long id) { 
    String key = KEY_CATEGORY_ITEM_PREFIX + id; redisTemplate.opsForValue().set(key, null, 30, TimeUnit.SECONDS); } @Override public void save(CategoryDetailsVO category) { 
    String key = KEY_CATEGORY_ITEM_PREFIX + category.getId(); redisTemplate.opsForValue().set(key, category); } @Override public void save(List<CategoryDetailsVO> categories) { 
    for (CategoryDetailsVO category : categories) { 
    redisTemplate.opsForList().rightPush(KEY_CATEGORY_LIST, category); } } @Override public void deleteAllItem() { 
    Set<String> keys = redisTemplate.keys(KEY_CATEGORY_ITEM_PREFIX + "*"); redisTemplate.delete(keys); } @Override public Boolean deleteList() { 
    return redisTemplate.delete(KEY_CATEGORY_LIST); } @Override public CategoryDetailsVO getDetailsById(Long id) { 
    String key = KEY_CATEGORY_ITEM_PREFIX + id; Serializable result = redisTemplate.opsForValue().get(key); if (result == null) { 
    return null; } else { 
    CategoryDetailsVO category = (CategoryDetailsVO) result; return category; } } } 

相关代码:缓存预热的业务代码(以下方法的声明在ICategoryService接口中,以下代码是CategoryServiceImpl中重写的方法):

@Override public void preloadCache() { 
    log.debug("删除缓存中的类别列表……"); categoryRedisRepository.deleteList(); log.debug("删除缓存中的各独立的类别数据……"); categoryRedisRepository.deleteAllItem(); log.debug("从数据库查询类别列表……"); List<CategoryDetailsVO> list = categoryMapper.list(); for (CategoryDetailsVO category : list) { 
    log.debug("查询结果:{}", category); log.debug("将当前类别存入到Redis:{}", category); categoryRedisRepository.save(category); } log.debug("将类别列表写入到Redis……"); categoryRedisRepository.save(list); log.debug("将类别列表写入到Redis完成!"); } 

相关代码:缓存预热类(CachePreLoad):

讯享网 import cn.tedu.csmall.product.service.ICategoryService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.stereotype.Component; @Component @Slf4j public class CachePreLoad implements ApplicationRunner { 
    @Autowired private ICategoryService categoryService; @Override public void run(ApplicationArguments args) throws Exception { 
    System.out.println("CachePreLoad.run()"); log.debug("准备执行缓存预热……"); categoryService.preloadCache(); log.debug("缓存预热完成!"); } } 
小讯
上一篇 2025-04-07 18:58
下一篇 2025-03-19 14:54

相关推荐

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/37458.html