• Redis存储数据


    Redis是一款基于内存的NoSQL数据存储服务,是非关系型的,是使用K-V结构进行存储的

    • 基于内存:读写数据均在内存中直接操作
    • NoSQL:通常把能够存、取数据的服务都称之为数据库,所以,Redis也是数据库,但是,它没有SQL语句

    在基于Spring Boot的开发中,当需要在程序中访问Redis中的数据时,需要添加spring-boot-starter-data-redis依赖项。

    要操作Redis中的数据,需要使用RedisTemplate对象,则在csmall-product-webapi的根包下的config包中创建RedisConfiguration类,并在其中进行配置:

    1. @Configuration
    2. public class RedisConfiguration {
    3. @Bean
    4. public RedisTemplate<String, Serializable> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
    5. RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();
    6. redisTemplate.setConnectionFactory(redisConnectionFactory);
    7. redisTemplate.setKeySerializer(RedisSerializer.string());
    8. redisTemplate.setValueSerializer(RedisSerializer.json());
    9. return redisTemplate;
    10. }
    11. }

    接下来,在测试的根包下创建RedisTests来测试访问Redis中的数据:

    1. @SpringBootTest
    2. public class RedisTests {
    3. @Autowired
    4. RedisTemplate<String, Serializable> redisTemplate;
    5. @Test
    6. void testSetValue() {
    7. redisTemplate.opsForValue()
    8. .set("name", "liuguobin");
    9. }
    10. @Test
    11. void testSetValueTTL() {
    12. redisTemplate.opsForValue()
    13. .set("name", "fanchuanqi", 60, TimeUnit.SECONDS);
    14. }
    15. @Test
    16. void testSetObjectValue() {
    17. CategoryDetailsVO category = new CategoryDetailsVO();
    18. category.setId(65L);
    19. category.setIsParent(1);
    20. category.setDepth(1);
    21. category.setName("水果");
    22. redisTemplate.opsForValue()
    23. .set("category", category);
    24. }
    25. @Test
    26. void testGetValue() {
    27. // 当key存在时,可获取到有效值
    28. // 当key不存在时,获取到的结果将是null
    29. Serializable name = redisTemplate.opsForValue()
    30. .get("name");
    31. System.out.println("get value --> " + name);
    32. }
    33. @Test
    34. void testGetObjectValue() {
    35. // 当key存在时,可获取到有效值
    36. // 当key不存在时,获取到的结果将是null
    37. Serializable serializable = redisTemplate.opsForValue()
    38. .get("category");
    39. System.out.println("get value --> " + serializable);
    40. if (serializable != null) {
    41. CategoryDetailsVO category = (CategoryDetailsVO) serializable;
    42. System.out.println("get value --> " + category);
    43. }
    44. }
    45. @Test
    46. void testDeleteKey() {
    47. // 删除key时,将返回“是否成功删除”
    48. // 当key存在时,将返回true
    49. // 当key不存在时,将返回false
    50. Boolean result = redisTemplate.delete("name");
    51. System.out.println("result --> " + result);
    52. }
    53. @Test
    54. void testRightPushList() {
    55. // 存入List时,需要redisTemplate.opsForList()得到针对List的操作器
    56. // 通过rightPush()可以向Redis中的List追加数据
    57. // 每次调用rightPush()时使用的key必须是同一个,才能把多个数据放到同一个List中
    58. List<CategoryDetailsVO> list = new ArrayList<>();
    59. for (int i = 1; i <= 5; i++) {
    60. CategoryDetailsVO category = new CategoryDetailsVO();
    61. category.setName("类别00" + i);
    62. list.add(category);
    63. }
    64. String key = "categoryList";
    65. for (CategoryDetailsVO category : list) {
    66. redisTemplate.opsForList().rightPush(key, category);
    67. }
    68. }
    69. @Test
    70. void testListSize() {
    71. // 获取List的长度,即List中的元素数量
    72. String key = "categoryList";
    73. Long size = redisTemplate.opsForList().size(key);
    74. System.out.println("size --> " + size);
    75. }
    76. @Test
    77. void testRange() {
    78. // 调用opsForList()后再调用range(String key, long start, long end)方法取出List中的若干个数据,将得到List
    79. // long start:起始下标(结果中将包含)
    80. // long end:结束下标(结果中将包含),如果需要取至最后一个元素,可使用-1作为此参数值
    81. String key = "categoryList";
    82. List<Serializable> range = redisTemplate.opsForList().range(key, 0, -1);
    83. for (Serializable serializable : range) {
    84. System.out.println(serializable);
    85. }
    86. }
    87. @Test
    88. void testKeys() {
    89. // 调用keys()方法可以找出匹配模式的所有key
    90. // 在模式中,可以使用星号作为通配符
    91. Set<String> keys = redisTemplate.keys("*");
    92. for (String key : keys) {
    93. System.out.println(key);
    94. }
    95. }
    96. }

    最后,关于Key的使用,通常建议使用冒号区分多层次,类似URL的设计方式,例如:

    • 类别列表的Key:categories:listcategories
    • 某个id(9527)对应的类别的Key:categories:item:9527

     

    使用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中拿到必要的数据

    在接口中添加抽象方法:

    1. public interface ICategoryRedisRepository {
    2. String KEY_CATEGORY_ITEM_PREFIX = "categories:item:";
    3. // 将类别详情存入到Redis中
    4. void save(CategoryDetailsVO category);
    5. // 根据类别id获取类别详情
    6. CategoryDetailsVO getDetailsById(Long id);
    7. }

    创建(接口的实现类),实现以上接口:

    1. @Repository
    2. public class CategoryRedisRepositoryImpl implements ICategoryRedisRepository {
    3. @Autowired
    4. private RedisTemplate<String, Serilizalbe> redisTemplate;
    5. @Override
    6. public void save(CategoryDetailsVO category) {
    7. String key = KEY_CATEGORY_ITEM_PREFIX + category.getId();
    8. redisTemplate.opsForValue().set(key, category);
    9. }
    10. @Override
    11. public CategoryDetailsVO getDetailsById(Long id) {
    12. String key = KEY_CATEGORY_ITEM_PREFIX + id;
    13. Serializable result = redisTemplate.opsForValue().get(key);
    14. if (result == null) {
    15. return null;
    16. } else {
    17. CategoryDetailsVO category = (CategoryDetailsVO) result;
    18. return category;
    19. }
    20. }
    21. }

     

    为了避免缓存穿透,需要在ICategoryRedisRepository中添加2个抽象方法:

    1. /**
    2. * 判断是否存在id对应的缓存数据
    3. *
    4. * @param id 类别id
    5. * @return 存在则返回true,否则返回false
    6. */
    7. boolean exists(Long id);
    8. /**
    9. * 向缓存中写入某id对应的空数据(null),此方法主要用于解决缓存穿透问题
    10. *
    11. * @param id 类别id
    12. */
    13. void saveEmptyValue(Long id);

    并在CategoryRedisRepositoryImpl中补充实现:

    1. @Override
    2. public boolean exists(Long id) {
    3. String key = KEY_CATEGORY_ITEM_PREFIX + id;
    4. return redisTemplate.hasKey(key);
    5. }
    6. @Override
    7. public void saveEmptyValue(Long id) {
    8. String key = KEY_CATEGORY_ITEM_PREFIX + id;
    9. redisTemplate.opsForValue().set(key, null);
    10. }

    业务中的具体实现为:

    1. @Override
    2. public CategoryDetailsVO getDetailsById(Long id) {
    3. // ===== 以下是原有代码,只从数据库中获取数据 =====
    4. // CategoryDetailsVO category = categoryMapper.getDetailsById(id);
    5. // if (category == null) {
    6. // throw new ServiceException(State.ERR_CATEGORY_NOT_FOUND,
    7. // "获取类别详情失败,尝试访问的数据不存在!");
    8. // }
    9. // return category;
    10. // ===== 以下是新的业务,将从Redis中获取数据 =====
    11. log.debug("根据id({})获取类别详情……", id);
    12. // 从repository中调用方法,根据id获取缓存的数据
    13. // 判断缓存中是否存在与此id对应的key
    14. boolean exists = categoryRedisRepository.exists(id);
    15. if (exists) {
    16. // 有:表示明确的存入过某数据,此数据可能是有效数据,也可能是null
    17. // -- 判断此key对应的数据是否为null
    18. CategoryDetailsVO cacheResult = categoryRedisRepository.getDetailsById(id);
    19. if (cacheResult == null) {
    20. // -- 是:表示明确的存入了null值,则此id对应的数据确实不存在,则抛出异常
    21. log.warn("在缓存中存在此id()对应的Key,却是null值,则抛出异常", id);
    22. throw new ServiceException(State.ERR_CATEGORY_NOT_FOUND,
    23. "获取类别详情失败,尝试访问的数据不存在!");
    24. } else {
    25. // -- 否:表示明确的存入了有效数据,则返回此数据即可
    26. return cacheResult;
    27. }
    28. }
    29. // 缓存中没有此id匹配的数据
    30. // 从mapper中调用方法,根据id获取数据库的数据
    31. log.debug("没有命中缓存,则从数据库查询数据……");
    32. CategoryDetailsVO dbResult = categoryMapper.getDetailsById(id);
    33. // 判断从数据库中获取的结果是否为null
    34. if (dbResult == null) {
    35. // 是:数据库也没有此数据,先向缓存中写入错误数据,再抛出异常
    36. log.warn("数据库中也无此数据(id={}),先向缓存中写入错误数据", id);
    37. categoryRedisRepository.saveEmptyValue(id);
    38. log.warn("抛出异常");
    39. throw new ServiceException(State.ERR_CATEGORY_NOT_FOUND,
    40. "获取类别详情失败,尝试访问的数据不存在!");
    41. }
    42. // 将从数据库中查询到的结果存入到缓存中
    43. log.debug("已经从数据库查询到匹配的数据,将数据存入缓存……");
    44. categoryRedisRepository.save(dbResult);
    45. // 返回查询结果
    46. log.debug("返回查询到数据:{}", dbResult);
    47. return dbResult;
    48. }

    许多缓存数据应该是服务器刚刚启动就直接写入到Redis中的,当后续客户端访问时,缓存中已经存在的数据可以直接响应,避免获取数据时缓存中还没有对应的数据,还需要从数据库中查询。

    在服务器刚刚启动时就加载需要缓存的数据并写入到Redis中,这种做法称之为缓存预热。

    需要解决的问题有:

    • 需要实现开机启动时自动执行某个任务
    • 哪些数据需要写入到缓存中,例如全部“类别”数据

    在Spring Boot中,可以自定义某个组件类,实现ApplicationRunner即可,例如:

     

     

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

    • 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

    1. import cn.tedu.csmall.pojo.vo.CategoryDetailsVO;
    2. import java.util.List;
    3. public interface ICategoryRedisRepository {
    4. /**
    5. * 类别数据的KEY的前缀
    6. */
    7. String KEY_CATEGORY_ITEM_PREFIX = "categories:item:";
    8. /**
    9. * 类别列表的KEY
    10. */
    11. String KEY_CATEGORY_LIST = "categories:list";
    12. /**
    13. * 判断是否存在id对应的缓存数据
    14. *
    15. * @param id 类别id
    16. * @return 存在则返回true,否则返回false
    17. */
    18. Boolean exists(Long id);
    19. /**
    20. * 向缓存中写入某id对应的空数据(null),此方法主要用于解决缓存穿透问题
    21. *
    22. * @param id 类别id
    23. */
    24. void saveEmptyValue(Long id);
    25. /**
    26. * 将类别详情存入到Redis中
    27. *
    28. * @param category 类别详情
    29. */
    30. void save(CategoryDetailsVO category);
    31. /**
    32. * 将类别的列表存入到Redis中
    33. *
    34. * @param categories 类别列表
    35. */
    36. void save(List<CategoryDetailsVO> categories);
    37. /**
    38. * 删除Redis中各独立存储的类别数据
    39. */
    40. void deleteAllItem();
    41. /**
    42. * 删除Redis中的类别列表
    43. * @return 如果成功删除,则返回true,否则返回false
    44. */
    45. Boolean deleteList();
    46. /**
    47. * 根据类别id获取类别详情
    48. *
    49. * @param id 类别id
    50. * @return 匹配的类别详情,如果没有匹配的数据,则返回null
    51. */
    52. CategoryDetailsVO getDetailsById(Long id);
    53. }

    相关代码:CategoryRedisRepositoryImpl

    1. import cn.tedu.csmall.pojo.vo.CategoryDetailsVO;
    2. import cn.tedu.csmall.product.webapi.repository.ICategoryRedisRepository;
    3. import org.springframework.beans.factory.annotation.Autowired;
    4. import org.springframework.data.redis.core.RedisTemplate;
    5. import org.springframework.stereotype.Repository;
    6. import java.io.Serializable;
    7. import java.util.List;
    8. import java.util.Set;
    9. import java.util.concurrent.TimeUnit;
    10. @Repository
    11. public class CategoryRedisRepositoryImpl implements ICategoryRedisRepository {
    12. @Autowired
    13. private RedisTemplate<String, Serializable> redisTemplate;
    14. @Override
    15. public Boolean exists(Long id) {
    16. String key = KEY_CATEGORY_ITEM_PREFIX + id;
    17. return redisTemplate.hasKey(key);
    18. }
    19. @Override
    20. public void saveEmptyValue(Long id) {
    21. String key = KEY_CATEGORY_ITEM_PREFIX + id;
    22. redisTemplate.opsForValue().set(key, null, 30, TimeUnit.SECONDS);
    23. }
    24. @Override
    25. public void save(CategoryDetailsVO category) {
    26. String key = KEY_CATEGORY_ITEM_PREFIX + category.getId();
    27. redisTemplate.opsForValue().set(key, category);
    28. }
    29. @Override
    30. public void save(List<CategoryDetailsVO> categories) {
    31. for (CategoryDetailsVO category : categories) {
    32. redisTemplate.opsForList().rightPush(KEY_CATEGORY_LIST, category);
    33. }
    34. }
    35. @Override
    36. public void deleteAllItem() {
    37. Set<String> keys = redisTemplate.keys(KEY_CATEGORY_ITEM_PREFIX + "*");
    38. redisTemplate.delete(keys);
    39. }
    40. @Override
    41. public Boolean deleteList() {
    42. return redisTemplate.delete(KEY_CATEGORY_LIST);
    43. }
    44. @Override
    45. public CategoryDetailsVO getDetailsById(Long id) {
    46. String key = KEY_CATEGORY_ITEM_PREFIX + id;
    47. Serializable result = redisTemplate.opsForValue().get(key);
    48. if (result == null) {
    49. return null;
    50. } else {
    51. CategoryDetailsVO category = (CategoryDetailsVO) result;
    52. return category;
    53. }
    54. }
    55. }

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

    1. @Override
    2. public void preloadCache() {
    3. log.debug("删除缓存中的类别列表……");
    4. categoryRedisRepository.deleteList();
    5. log.debug("删除缓存中的各独立的类别数据……");
    6. categoryRedisRepository.deleteAllItem();
    7. log.debug("从数据库查询类别列表……");
    8. List<CategoryDetailsVO> list = categoryMapper.list();
    9. for (CategoryDetailsVO category : list) {
    10. log.debug("查询结果:{}", category);
    11. log.debug("将当前类别存入到Redis:{}", category);
    12. categoryRedisRepository.save(category);
    13. }
    14. log.debug("将类别列表写入到Redis……");
    15. categoryRedisRepository.save(list);
    16. log.debug("将类别列表写入到Redis完成!");
    17. }

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

     

  • 相关阅读:
    http 和 https
    babel-plugin-import 实现按需引入
    【IAP Kit】应用内支付订单参数相关问题解析
    80个在线小游戏源码
    h264编码算法流程
    SpringMVC-接收请求中的json数据及日期类型参数传递
    Feign源码解析:初始化过程(二)
    网络TCP/IP
    CuteOneP 一款php的OneDrive多网盘挂载程序 带会员 同步等功能
    Flink系列之Flink基础使用与核心概念
  • 原文地址:https://blog.csdn.net/ClearDream__/article/details/125459404