Redis是一款基于内存的NoSQL数据存储服务,是非关系型的,是使用K-V结构进行存储的
在基于Spring Boot的开发中,当需要在程序中访问Redis中的数据时,需要添加spring-boot-starter-data-redis依赖项。
要操作Redis中的数据,需要使用RedisTemplate对象,则在csmall-product-webapi的根包下的config包中创建RedisConfiguration类,并在其中进行配置:
- @Configuration
- public class RedisConfiguration {
-
- @Bean
- public RedisTemplate<String, Serializable> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
- RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();
- redisTemplate.setConnectionFactory(redisConnectionFactory);
- redisTemplate.setKeySerializer(RedisSerializer.string());
- redisTemplate.setValueSerializer(RedisSerializer.json());
- return redisTemplate;
- }
-
- }
接下来,在测试的根包下创建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);
- }
- }
-
- }
最后,关于Key的使用,通常建议使用冒号区分多层次,类似URL的设计方式,例如:
categories:list或categoriescategories:item:9527
使用Redis可以提高查询效率,一定程度上可以减轻数据库服务器的压力,从而保护了数据库。
通常,应用Redis的场景有:
一旦使用Redis,就会导致Redis和数据库中都存在同样的数据,当数据发生变化时,可能出现不一致的问题!
所以,还有某些数据在特定的场景中不能使用Redis:
需要学会评估是否要求数据一定保持一致!
要使用Redis缓存数据,至少需要:
在使用Redis之前,还必须明确一些问题:
暂定目标:
在接口中添加抽象方法:
- public interface ICategoryRedisRepository {
-
- String KEY_CATEGORY_ITEM_PREFIX = "categories:item:";
-
- // 将类别详情存入到Redis中
- void save(CategoryDetailsVO category);
-
- // 根据类别id获取类别详情
- CategoryDetailsVO getDetailsById(Long id);
-
- }
创建(接口的实现类),实现以上接口:
- @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) {
- // ===== 以下是原有代码,只从数据库中获取数据 =====
- // CategoryDetailsVO category = categoryMapper.getDetailsById(id);
- // if (category == null) {
- // throw new ServiceException(State.ERR_CATEGORY_NOT_FOUND,
- // "获取类别详情失败,尝试访问的数据不存在!");
- // }
- // return category;
-
- // ===== 以下是新的业务,将从Redis中获取数据 =====
- 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即可,例如:

为了将全部“类别”写入到缓存中,首先,需要能够从数据库中查询到全部数据,则需要:
CategoryMapper接口中添加:List<CategoryDetailsVO> list();CategoryMapper.xml中配置以上抽象方法映射的SQL语句然后,还需要实现将查询到的List<CategoryDetailsVO>写入到Redis中,则需要:
ICategoryRedisRepository接口中添加:void save(List<CategoryDetailsVO> categories);CategoryRedisRepositoryImpl中实现以上方法
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:
-
-
- 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:
-
-
- 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):
