项目实战-redis

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                    # 最大连接数,默认 8
        max-wait: 10000ms                   # 最大连接阻塞等待时间,单位毫秒,默认 -1
        max-idle: 200                       # 最大空闲连接,默认 8
        min-idle: 5                          # 最小空闲连接,默认 0
   	 password: 123123

# 商品分类列表 Key
goods.category.list.key: goods:category:list:goodsCategoryList

为什么要配置redis-key?
提高代码的可维护性和可配置性,方便应用程序的管理和部署。
将Redis Key的名称配置在yml(或其他配置文件)中,有以下几个好处:

  1. 集中管理:将Redis Key的名称存储在一个配置文件中,可以方便地对Key进行统一的管理和维护。在需要修改Key名称时,只需要修改配置文件中的内容即可,而不需要在应用程序中修改多处代码。
  2. 易于维护:通过配置文件中的Redis Key名称,开发人员可以直观地了解每个Key的含义和作用,从而更容易地进行代码维护和开发。而且,通过配置文件,可以快速地了解应用程序中使用了哪些Redis Key。
  3. 可配置性:将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:创建一个RedisConfig类,在创建RedisTemplate Bean,这个方法主要是用来设置序列器的,防止乱码问题。

/**
 * Redis配置类
 *
 * @author zhoubin
 * @since 1.0.0
 */
@Configuration
public class RedisConfig {
	@Bean
	public 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数据操作时,可以保证数据的正确序列化和反序列化。这里使用了两种序列化器,StringRedisSerializerGenericJackson2JsonRedisSerializer,分别用于处理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自带的ObjectInputStreamObjectOutputStream实现的序列化器,适用于Java对象的序列化。3. Jackson2JsonRedisSerializer:使用Jackson库实现的JSON格式数据的序列化和反序列化。4. GenericJackson2JsonRedisSerializer:使用Jackson库实现的JSON格式数据的泛化型序列化和反序列化,支持类型信息。综上所述,数据序列化可以大幅减少数据大小,方便跨平台传输和存储,而使用不同的序列化器,可以适应不同的数据类型和业务需求。

JsonUtils

ObjectMapper是一个Java对象与JSON数据之间的转换工具,它可以将Java对象序列化为JSON格式的数据,也可以将JSON数据反序列化为Java对象。它是由FasterXML开发的一个开源库,常用于Java应用程序中进行JSON数据的处理。

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注入到您的服务类中

使用redis在服务类中,使用@Value从yml文件中取出来redis-key
首先要先查询Redis缓存是否有数据,有数据直接返回,没有数据去数据库查询,然后将查到的数据存到redis里面,如果没有查到数据,就缓存为空值并且设置过期时间。

@Service("GoodsCategoryService")
public class GoodsCategoryServiceImpl implements GoodsCategoryService {

	@Resource
	private GoodsCategoryMapper goodsCategoryMapper;
	@Autowired
	RedisTemplate<String,String> redisTemplate;
	@Value("${goods.category.list.key}")
	private String goodsCategoryListKey;
	/**
	 * 分类查询商品分类,顶级分类,二级分类,三级分类
	 * @return
	 */
	@Override
	public 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
	 */
	@Override
	public 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()+":";
		}
		//拼接完整的RedisKey
		String 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 {
			//如果没有数据,将空数据放入缓存,设置失效时间60s
			valueOperations.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);
第四步,返回分页对象到前端,渲染页面。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值