springboot集成redis
步骤
1、添加Redis依赖项:在项目的pom.xml文件中添加以下依赖项
<!-- spring data redis 依赖 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- commons-pool2 对象池依赖 -->
<dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId>
</dependency>
2、配置Redis连接属性:在application.yml中添加以下属性:# Redis配置
注意,如果你的redis有密码,还要配置密码!!!
spring:redis:timeout: 10000ms # 连接超时时间host: 192.168.10.100 # Redis服务器地址port: 6379 # Redis服务器端口database: 0 # 选择哪个库,默认0库lettuce:pool:max-active: 1024 # 最大连接数,默认 8max-wait: 10000ms # 最大连接阻塞等待时间,单位毫秒,默认 -1max-idle: 200 # 最大空闲连接,默认 8min-idle: 5 # 最小空闲连接,默认 0password: 123123# 商品分类列表 Key
goods.category.list.key: goods:category:list:goodsCategoryList
为什么要配置redis-key?
提高代码的可维护性和可配置性,方便应用程序的管理和部署。
将Redis Key的名称配置在yml(或其他配置文件)中,有以下几个好处:
- 集中管理:将Redis Key的名称存储在一个配置文件中,可以方便地对Key进行统一的管理和维护。在需要修改Key名称时,只需要修改配置文件中的内容即可,而不需要在应用程序中修改多处代码。
- 易于维护:通过配置文件中的Redis Key名称,开发人员可以直观地了解每个Key的含义和作用,从而更容易地进行代码维护和开发。而且,通过配置文件,可以快速地了解应用程序中使用了哪些Redis Key。
- 可配置性:将Redis Key的名称配置在一个文件中,可以方便地进行动态配置。这样,在应用程序启动时,可以根据不同的配置文件加载不同的Redis Key,从而实现灵活的应用程序部署和管理。
命名规则:
这个Redis Key是用于存储商品分类列表数据的,格式为goods:category:list:goodsCategoryList
。
命名空间:对象类型:数据类型:数据名称
Redis Key的设计通常遵循以下几个原则:
5. 命名空间:通过给Key添加一个命名空间,可以避免不同模块或不同功能使用相同的Key造成冲突。在这里,goods
就是一个命名空间,表示这个Key是与商品相关的数据。
6. 对象类型:通常,在Key中包含对象类型可以让开发人员快速地了解这个Key存储的是什么类型的数据。在这里,category
表示这个Key存储的是商品分类数据。
7. 层级结构:为了避免Key的名称过于冗长,可以考虑将Key分层。在这里,list
表示这个Key存储的是列表类型的数据,而goodsCategoryList
是实际的Key的名称。通过这种设计方式,可以使Key的名称更加清晰明了,易于开发人员的理解和维护。同时,也方便了对数据进行管理和查询。
3、 创建RedisTemplate Bean:在Spring配置类中创建RedisTemplate Bean,以便在应用程序中使用RedisTemplate。
/*** Redis配置类** @author zhoubin* @since 1.0.0*/
@Configuration
public class RedisConfig {@Beanpublic RedisTemplate<String,Object> redisTemplate(LettuceConnectionFactory redisConnectionFactory){RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();//为string类型key设置序列器redisTemplate.setKeySerializer(new StringRedisSerializer());//为string类型value设置序列器redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());//为hash类型key设置序列器redisTemplate.setHashKeySerializer(new StringRedisSerializer());//为hash类型value设置序列器redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());redisTemplate.setConnectionFactory(redisConnectionFactory);return redisTemplate;}
}
这个配置类的作用是为了创建一个RedisTemplate
对象并进行相关配置,具体的作用有以下几点:1. 序列化器配置:配置RedisTemplate
对象的序列化器,这样在进行Redis数据操作时,可以保证数据的正确序列化和反序列化。这里使用了两种序列化器,StringRedisSerializer
和GenericJackson2JsonRedisSerializer
,分别用于处理Redis Key和Value的序列化。2. 连接工厂配置:设置要使用的Redis客户端连接工厂。这里使用了LettuceConnectionFactory类型的对象,表示使用Lettuce客户端来进行Redis数据操作。3. Bean注解配置:通过@Bean
注解声明一个Bean,使该对象可以在应用程序中被使用。
这样编写的好处在于,RedisTemplate
是Spring提供的Redis客户端模板,能够方便地进行Redis数据操作。而该配置类提供了一种便捷的方式来创建RedisTemplate
实例并进行相关配置,可以使得在应用程序的其他地方直接使用该实例来进行Redis数据操作,简化了代码的编写。同时,通过注入redisConnectionFactory
,可以方便地在其他地方进行配置和替换该连接工厂,提高了代码的扩展性。
为什么要进行数据的序列化?
在使用Redis存储数据时,需要将数据序列化成二进制格式,以便于Redis服务器进行存储和读取。Redis只能存储二进制数据,不能直接存储Java对象
,因此需要将对象序列化成二进制格式,然后再存储到Redis中
。数据序列化的主要作用有:1. 压缩数据:将数据序列化成二进制格式可以大幅减少数据大小,从而节省内存和网络带宽。2. 跨平台传输:不同的平台和语言之间,二进制数据是一种通用的传输格式,比如Java对象序列化成二进制格式后,可以在Python、C++等其他语言中进行反序列化。3. 持久化存储:将数据序列化成二进制格式,可以方便地将数据存储到磁盘中,以实现数据持久化。在Redis中,可以使用不同的序列化器来序列化不同类型的数据。
其中,常用的序列化器有:1. StringRedisSerializer:序列化Redis Key的字符串类型。2. JdkSerializationRedisSerializer:使用JDK自带的ObjectInputStream
和ObjectOutputStream
实现的序列化器,适用于Java对象的序列化。3. Jackson2JsonRedisSerializer:使用Jackson库实现的JSON格式数据的序列化和反序列化。4. GenericJackson2JsonRedisSerializer:使用Jackson库实现的JSON格式数据的泛化型序列化和反序列化,支持类型信息。综上所述,数据序列化可以大幅减少数据大小,方便跨平台传输和存储,而使用不同的序列化器,可以适应不同的数据类型和业务需求。
JsonUtils
public class JsonUtil {private static ObjectMapper objectMapper = new ObjectMapper();/*** 将对象转换成json字符串** @param obj* @return*/public static String object2JsonStr(Object obj) {try {return objectMapper.writeValueAsString(obj);} catch (JsonProcessingException e) {//打印异常信息e.printStackTrace();}return null;}/*** 将字符串转换为对象** @param <T> 泛型*/public static <T> T jsonStr2Object(String jsonStr, Class<T> clazz) {try {return objectMapper.readValue(jsonStr.getBytes("UTF-8"), clazz);} catch (JsonParseException e) {e.printStackTrace();} catch (JsonMappingException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}return null;}/*** 将json数据转换成pojo对象list* <p>Title: jsonToList</p>* <p>Description: </p>** @param jsonStr* @param beanType* @return*/public static <T> List<T> jsonToList(String jsonStr, Class<T> beanType) {JavaType javaType = objectMapper.getTypeFactory().constructParametricType(List.class, beanType);try {List<T> list = objectMapper.readValue(jsonStr, javaType);return list;} catch (Exception e) {e.printStackTrace();}return null;}
}
4、使用RedisTemplate:使用@Autowired注释将RedisTemplate注入到您的服务类中
@Service("GoodsCategoryService")
public class GoodsCategoryServiceImpl implements GoodsCategoryService {@Resourceprivate GoodsCategoryMapper goodsCategoryMapper;@AutowiredRedisTemplate<String,String> redisTemplate;@Value("${goods.category.list.key}")private String goodsCategoryListKey;/*** 分类查询商品分类,顶级分类,二级分类,三级分类* @return*/@Overridepublic List<GoodsCategoryVo> selectCategoryListForView() {//opsForValue()获取redis中操作字符串的对象ValueOperations<String, String> valueOpreations = redisTemplate.opsForValue();//查询Redis缓存是否有数据,有数据直接返回,没有数据去数据库查询String gcvListJson = valueOpreations.get(goodsCategoryListKey);if (!StringUtils.isEmpty(gcvListJson)){return JsonUtil.jsonToList(gcvListJson,GoodsCategoryVo.class);}//========================JDK8新特性======================//创建查询对象GoodsCategoryExample example = new GoodsCategoryExample();//查询所有商品分类List<GoodsCategory> list = goodsCategoryMapper.selectByExample(example);//将GoodsCategory对象转成GoodsCategoryVo对象List<GoodsCategoryVo> gcvList = list.stream().map(e -> {GoodsCategoryVo gcv = new GoodsCategoryVo();BeanUtils.copyProperties(e, gcv);return gcv;}).collect(Collectors.toList());//将List<GoodsCategoryVo>转成Map<parentId,List<GoodsCategoryVo>>//按parentId分组,key就是parentId,值就是parentId对应的List<GoodsCategoryVo>Map<Short, List<GoodsCategoryVo>> map =gcvList.stream().collect(Collectors.groupingBy(GoodsCategoryVo::getParentId));//循环,给children赋值gcvList.forEach(e->e.setChildren(map.get(e.getId())));//拦截器,返回level为1的list,也就是顶级分类List<GoodsCategoryVo> gcvList01 = gcvList.stream().filter(e -> 1 == e.getLevel()).collect(Collectors.toList());//放入Redis缓存valueOpreations.set(goodsCategoryListKey, JsonUtil.object2JsonStr(gcvList01));//========================JDK8新特性=======================return gcvList01;}
补充知识点:java8新特性
在没有使用java8新特性的时候我们是这样写的
List<GoodsCategoryVo> gcvList1 = new ArrayList<>();GoodsCategoryExample example = new GoodsCategoryExample();example.createCriteria().andParentIdEqualTo(((short) 0)).andLevelEqualTo(((byte) 1));List<GoodsCategory> gcList1 = goodsCategoryMapper.selectByExample(example);List<GoodsCategoryVo> gcvList2 = new ArrayList<>();for(GoodsCategory gc1:gcList1){GoodsCategoryVo gcv1 = new GoodsCategoryVo();BeanUtils.copyProperties(gc1,gcv1);example.clear();example.createCriteria().andParentIdEqualTo(gc1.getId()).andLevelEqualTo(((byte) 2));List<GoodsCategory> gcList2 = goodsCategoryMapper.selectByExample(example);List<GoodsCategoryVo> gcvList3 =new ArrayList<>();for(GoodsCategory gc2:gcList2){GoodsCategoryVo gcv2 = new GoodsCategoryVo();BeanUtils.copyProperties(gc2,gcv2);example.clear();example.createCriteria().andParentIdEqualTo(gc2.getId()).andLevelEqualTo(((byte) 3));List<GoodsCategory> gcList3 = goodsCategoryMapper.selectByExample(example);for(GoodsCategory gc3:gcList3){GoodsCategoryVo gcv3 = new GoodsCategoryVo();BeanUtils.copyProperties(gc3,gcv3);gcv3.setChildren(null);gcvList3.add(gcv3);}gcv2.setChildren(gcvList3);gcvList2.add(gcv2);}gcv1.setChildren(gcvList2);gcvList1.add(gcv1);}
使用java8新特性
(1)//将GoodsCategory对象转成GoodsCategoryVo对象
可以选择获取List集合中的每一个GoodsCategory对象,然后将它映射成为GoodsCategoryVo对象
?为什么不直接使用list.stream()。而是使用list.stream().map呢?
因为list.stream()返回的是一个
Stream对象,它并没有对原有的元素类型进行转换。而
list.stream().map()方法可以对流中的元素进行转换,并返回一个新的
Stream` 对象,该对象包含转换后的元素。
list.stream()返回一个顺序流,并且它能让我们对这个序列进行一些操作,例如过滤,映射,排序等等。
list.stream().map()` 可以将顺序流中的每个元素通过传入的 Lambda 表达式进行转换,并返回一个新的 Stream 对象,该对象包含转换后的元素。相当于对流中的每个元素进行映射。
因此我们使用list.stream().map((参数)->{转换的语句,return 新元素类型})
?collect的作用是什么?
collect:将流转换为其他形式。接收一个Collector接口的实现。
综上,我们可以根据java8新特性来写转换。
首先获取gcList
List<GoodsCategory> list = goodsCategoryMapper.selectByExample(example);
//将GoodsCategory对象转成GoodsCategoryVo对象
List<GoodsCategoryVo> gcvList = list.stream().map(e -> {
GoodsCategoryVo gcv = new GoodsCategoryVo();BeanUtils.copyProperties(e, gcv);return gcv;}).collect(Collectors.toList());
(2)//根据parenId分组
已知我们分组分成三大类分别是顶级分类,二级分类,三级分类。他们是根据parentId,进行分类的,我们刚好也需要这些分类,因此我们可以根据parentId来讲大分类分成三组,因此我们最终会得到一个Map集合,他的key是parentId,他的value是List<GoodsCategoryVo>
分类用到的stream().collector(Collectors.groupingBy());
Map<Short, List<GoodsCategoryVo>> map =gcvList.stream().collect(Collectors.groupingBy(GoodsCategoryVo::getParentId));
GoodsCategoryVo::getParentId
使用的是 Java 8 中的方法引用(Method Reference)语法。方法引用是一种更简洁的 lambda 表达式的语法糖,用于直接引用已有方法或构造函数,从而在函数式编程中更容易地实现对象的方法传递。在这里,GoodsCategoryVo::getParentId
,表示将 GoodsCategoryVo
中的 getParentId
方法作为参数传递给 groupingBy()
方法,用于指定根据哪个属性进行分组。等价于 lambda 表达式 (gcv) -> gcv.getParentId()
。
**(3)**给每一个Vo对象的children赋值
根据他的父id和本身的id,进行对比,如果一个对象a的父id是对象b的id,那么a就是b的child
通过for循环,将每一个满足条件的元素都进行赋值
list.forEach
是 Java 8 中的新特性,用于在集合中遍历元素,对集合中的每个元素都执行指定操作。它使用了函数式编程中的 lambda 表达式,可以代替传统的 for 循环来遍历集合。
//循环,给children赋值gcvList.forEach(e->e.setChildren(map.get(e.getId())));//拦截器,返回level为1的list,也就是顶级分类List<GoodsCategoryVo> gcvList01 = gcvList.stream().filter(e -> 1 == e.getLevel()).collect(Collectors.toList());
有动态变化的属性在redis缓存中分页查询商品列表
/*** 商品列表-分页查询* @param goods* @param pageNum* @param pageSize* @return*/@Overridepublic BaseResult selectGoodsListByPage(Goods goods, Integer pageNum, Integer pageSize) {/*** 商品列表RedisKey* 1.无条件查询* goods:pageNum_:PageSize_:catId_:brandId_:goodsName_:* 2.条件查询* goods:pageNum_:PageSize_:catId_123:brandId_:goodsName_:* goods:pageNum_:PageSize_:catId_:brandId_123:goodsName_:* goods:pageNum_:PageSize_:catId_:brandId_:goodsName_华为:* goods:pageNum_:PageSize_:catId_123:brandId_123:goodsName_:* goods:pageNum_:PageSize_:catId_123:brandId_:goodsName_华为:* goods:pageNum_:PageSize_:catId_:brandId_123:goodsName_华为:* goods:pageNum_:PageSize_:catId_123:brandId_123:goodsName_华为:*///定义RedisKey数组String[] goodsKeyArr = new String[]{"goods:pageNum_"+pageNum+":PageSize_"+pageSize+":","catId_:","brandId_:","goodsName_:"};//构建分页对象PageHelper.startPage(pageNum,pageSize);//创建查询对象GoodsExample example = new GoodsExample();GoodsExample.Criteria criteria = example.createCriteria();//设置查询条件//分类参数if (null!=goods.getCatId()&&0!=goods.getCatId()){criteria.andCatIdEqualTo(goods.getCatId());goodsKeyArr[1] = "catId_"+goods.getCatId()+":";}//品牌参数if (null!=goods.getBrandId()&&0!=goods.getBrandId()){criteria.andBrandIdEqualTo(goods.getBrandId());goodsKeyArr[2] = "brandtId_"+goods.getBrandId()+":";}//关键词if (!StringUtils.isEmpty(goods.getGoodsName())){criteria.andGoodsNameLike("%"+goods.getGoodsName()+"%");goodsKeyArr[3] = "goodsName_"+goods.getGoodsName()+":";}//拼接完整的RedisKeyString goodsListKey = Arrays.stream(goodsKeyArr).collect(Collectors.joining());ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();//查询缓存,如果缓存中存在数据,直接返回String pageInfoGoodsJson = valueOperations.get(goodsListKey);if (!StringUtils.isEmpty(pageInfoGoodsJson)){return BaseResult.success(JsonUtil.jsonStr2Object(pageInfoGoodsJson,PageInfo.class));}//==============错误代码==================// String listGoodsJson = valueOperations.get(goodsListKey);// if (!StringUtils.isEmpty(listGoodsJson)){// List<Goods> goodsList = JsonUtil.jsonToList(listGoodsJson, Goods.class);// PageInfo<Goods> pageInfo = new PageInfo<>(goodsList);// return BaseResult.success(pageInfo);// }//==============错误代码==================//判断查询结果是否为空,不为空放入分页对象List<Goods> list = goodsMapper.selectByExample(example);if (!CollectionUtils.isEmpty(list)){PageInfo<Goods> pageInfo = new PageInfo<>(list);//放入缓存valueOperations.set(goodsListKey,JsonUtil.object2JsonStr(pageInfo));//==============错误代码==================// valueOperations.set(goodsListKey,JsonUtil.object2JsonStr(list));//==============错误代码==================return BaseResult.success(pageInfo);}else {//如果没有数据,将空数据放入缓存,设置失效时间60svalueOperations.set(goodsListKey,JsonUtil.object2JsonStr(new PageInfo<>(new ArrayList<Goods>())),60, TimeUnit.SECONDS);}return BaseResult.error();}
}
首先需要知道有哪些产品是动态的,哪些产品是会实时改变的。
已知商品分类id,品牌id,商品名称,分页页码,每页显示条数是固定的
因此我们可以使用一个数组来存储信息,当发生改变时,改变数组的内容。将本次请求中所有动态数据放入数据中,得到一个最终的goodsListKey,存入redis缓存中,如果以前查询过而且没有过期,那么从redis缓存中读取数据,如果没有查询过,则到数据库中查询。
如果查询出的数据为空,那么为了防止缓存雪崩的问题,那么我们采用返回一个空数据,并设置他的失效时间。
知识点:mybatis插件pageHelper
这里使用了分页对象PageInfo,pageInfo可以方便的封装分页信息,比如总记录,页码,每页显示条数等,并且他还封装了方法,可以让我们通过调用方法直接获取总记录条数等。
第一步,导入依赖
第二步,创建页对象 PageHelper.startPage(pageNum,pageSize);
第三步,创建分页对象 PageInfo<Good> pageInfo = new PageInfo(list);
第四步,返回分页对象到前端,渲染页面。