redis的使用

使用Redis缓存数据

使用Redis可以提高查询效率,一定程度上可以减轻数据库服务器的压力,从而保护了数据库。

通常,应用Redis的场景有:

  • 高频查询,例如:热搜列表、秒杀
  • 改变频率低的数据,例如:商品类别

一旦使用Redis,就会导致Redis和数据库中都存在同样的数据,当数据发生变化时,可能出现不一致的问题!

所以,还有某些数据在特定的场景中不能使用Redis:

  • 要求数据必须是准确的:下单购买时要求库存是准确的
    • 如果每次库存发生变化时都更新了Redis中的库存值,保证了Redis中的数据是准确的,也可以使用
  • 数据的修改频率很高,且对数据准确性有一定要求

需要学会评估是否要求数据一定保持一致!

要使用Redis缓存数据,至少需要:

  • 开发新的组件,实现对Redis中的数据访问
    • 此组件并不是必须的,因为访问Redis数据的API都非常简单,自定义组件时,组件中的每个方法可能也只有少量代码,甚至只有1行代码
    • 如果直接将访问Redis的代码写在Service中,首次开发时会更省事,但不利于维护
    • 【推荐】如果将访问Redis的代码写的新的组件中,首次开发时会更麻烦,但利于维护
  • 在Service中调用新的组件,在Service中决定何时访问MySQL,何时访问Redis

在使用Redis之前,还必须明确一些问题:

  • 哪些查询功能改为从Redis中获取数据
  • Redis中的数据从哪里来

暂定目标:

  • 根据类别的id查询类别详情,改为从Redis中获取数据
  • 优先从Redis中获取数据,如果Redis中没有,则从MySQL中获取,且获取到数据后,将数据存入到Redis中,所以,经过首次查询后,Redis中将存在此数据,后续每一次都可以直接从Redis中拿到必要的数据

cn.wlb.mall.product.webapi.repository创建ICategoryRedisRepository接口,并在接口中添加抽象方法:

public interface ICategoryRedisRepository {

    String KEY_CATEGORY_ITEM_PREFIX = "categories:item:";
    
    // 将类别详情存入到Redis中
    void save(CategoryDetailsVO category);
    
    // 根据类别id获取类别详情
    CategoryDetailsVO getDetailsById(Long id);
    
}

然后在cn.wlb.mall.product.webapi.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;
        }
    }
}

完成后,测试:

package cn.wlb.mall.product.webapi.repository;

import cn.wlb.mall.pojo.vo.CategoryDetailsVO;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class CategoryRedisRepositoryTests {

    @Autowired
    ICategoryRedisRepository repository;

    @Test
    void testGetDetailsByIdSuccessfully() {
        testSave();

        Long id = 10L;
        CategoryDetailsVO category = repository.getDetailsById(id);
        Assertions.assertNotNull(category);
    }

    @Test
    void testGetDetailsByIdReturnNull() {
        Long id = -1L;
        CategoryDetailsVO category = repository.getDetailsById(id);
        Assertions.assertNull(category);
    }

    private void testSave() {
        CategoryDetailsVO category = new CategoryDetailsVO();
        category.setId(10L);
        category.setName("家用电器");

        Assertions.assertDoesNotThrow(() -> {
            repository.save(category);
        });
    }
}

然后,需要修改CategoryServiceImpl中的实现:

@Autowired
private ICategoryRedisRepository categoryRedisRepository;

@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中获取数据 =====
    // 从repsotiroy中调用方法,根据id获取缓存的数据
    // 判断缓存中是否存在与此id对应的key
    // 有:表示明确的存入过某数据,此数据可能是有效数据,也可能是null
    // -- 判断此key对应的数据是否为null
    // -- 是:表示明确的存入了null值,则此id对应的数据确实不存在,则抛出异常
    // -- 否:表示明确的存入了有效数据,则返回此数据即可
    
    // 无:表示从未向缓存中写入此id对应的数据,在数据库中,此id可能存在数据,也可能不存在
    // 从mapper中调用方法,根据id获取数据库的数据
    // 判断从数据库中获取的结果是否为null
    // 是:数据库也没有此数据,先向缓存中写入错误数据(null),再抛出异常
    
    // 将从数据库中查询到的结果存入到缓存中
    // 返回查询结果
}

为了避免缓存穿透,需要在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即可,例如:

package cn.wlb.mall.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.wlb.mall.product.webapi.repository;

import cn.wlb.mall.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.wlb.mall.product.webapi.repository.impl;

import cn.wlb.mall.pojo.vo.CategoryDetailsVO;
import cn.wlb.mall.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):

package cn.wlb.mall.product.webapi.app;

import cn.wlb.mall.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("缓存预热完成!");
    }

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值