22-08-06 西安 尚医通(03)EasyExcel; Spring Cache 、Redis做缓存

EasyExcel

EasyExcel:一行一行读取到内存
EasyExcel是阿里巴巴开源的一个excel处理框架,以使用简单、节省内存著称

POI:java里操作excel,读取、创建excel

POI的缺点:耗内存。因为会把所有数据一起加载到内存中


EasyExcl读写演示

1.加依赖

        <!-- https://mvnrepository.com/artifact/com.alibaba/easyexcel -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
            <version>2.1.1</version>
        </dependency>

2.@ExcelProperty,创建实体类Anchor用来和表格做映射
index第几列,value列名

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Anchor {
    @ExcelProperty(index = 0,value = "主播姓名")
    private String name;

    @ExcelProperty(index = 1,value = "直播平台")
    private String platform;

    @ExcelProperty(index=2,value = "成名英雄")
    private String hero;
}
演示创建excel表格

一行代码,调用3个方法 EasyExcel.write().sheet().doWrite();

    public static void main(String[] args) {
        Anchor anchor1 = new Anchor("北慕", "虎牙", "露娜");
        Anchor anchor2 = new Anchor("骚白", "斗鱼", "花木兰");
        Anchor anchor3 = new Anchor("赖神", "虎牙", "老夫子");
        List<Anchor> anchorList = Arrays.asList(anchor1, anchor2, anchor3);
        EasyExcel.write("C:\\Users\\lenovo\\Desktop\\主播列表.xlsx",Anchor.class)
                .sheet("主播列表")
                .doWrite(anchorList);
    }

效果如下:
在这里插入图片描述
—————————————————————————————————————————

演示读取excel表格

EasyExcel采用一行一行的解析模式,
并将一行的解析结果以观察者的模式通知处理AnalysisEventListener

invoke方法用于处理每条数据,读者可以在这边进行业务逻辑处理
doAfterAllAnalysed方法是只处理完所有数据后进行的动作;

public class ExcelListener extends AnalysisEventListener<Anchor> {

    //一行一行去读取excle内容
    @Override
    public void invoke(Anchor anchor, AnalysisContext analysisContext) {
        System.out.println("***"+anchor);
    }

    //所有行读取完成后执行
    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
        System.out.println("读取完成后");
    }
}

主函数:正真读取是按照第二行开始的

    public static void main(String[] args) {
        String fileName = "C:\\Users\\lenovo\\Desktop\\主播列表.xlsx";
        // 这里 需要指定读用哪个class去读,然后读取第一个sheet 文件流会自动关闭
        EasyExcel.read(fileName, Anchor.class, new ExcelListener())
                .sheet("主播列表")
                .doRead();
    }

控制台打印如下:
在这里插入图片描述


数据字典导出

后端代码

可以理解为文件下载功能

DictEeVo,导出的数据字典格式

@Data
public class DictEeVo {

	@ExcelProperty(value = "id" ,index = 0)
	private Long id;

	@ExcelProperty(value = "上级id" ,index = 1)
	private Long parentId;

	@ExcelProperty(value = "名称" ,index = 2)
	private String name;

	@ExcelProperty(value = "值" ,index = 3)
	private String value;

	@ExcelProperty(value = "编码" ,index = 4)
	private String dictCode;

}

字典文件下载controller层,返回值为void就行,必须用response对象

@ApiOperation(value="导出")
@GetMapping(value = "/exportData")
public void exportData(HttpServletResponse response) {
    dictService.exportData(response);
} 

正真的实现是在service层。如下

    public void exportData(HttpServletResponse response) {
        try {
            //设置响应头
            response.setContentType("application/vnd.ms-excel"); //指示响应内容的格式
            response.setCharacterEncoding("utf-8");

            // 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
            String fileName = URLEncoder.encode("数据字典", "UTF-8");
            // 指示响应内容以附件形式下载
            response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");

            //封装数据集合
            List<Dict> dictList = baseMapper.selectList(null);
            ArrayList<DictEeVo> dictEeVos = new ArrayList<>();
            dictList.forEach(dict -> {
                DictEeVo dictEeVo = new DictEeVo();
                BeanUtils.copyProperties(dict, dictEeVo);
                dictEeVos.add(dictEeVo);
            });
            //用流的方式,浏览器文件下载
            EasyExcel.write(response.getOutputStream(), DictEeVo.class)
                    .sheet("数据字典")
                    .doWrite(dictEeVos);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

前端代码

在前台页面点击导出,

在这里插入图片描述
相关前端代码大致如下:

      <div class="el-toolbar-body" style="justify-content: flex-start">
        <el-button type="primary" @click="exportData"
          ><i class="el-icon-download el-icon--right" /> 导出</el-button
        >  ```

不需要写api部分,直接访问。也可以访问9001

 exportData() {
      window.open("http://localhost:8202/admin/cmn/dict/exportData");
    },

导出效果:
在这里插入图片描述


数据字典导入

后端接口

导入的excel文件格式,要满足格式DictEeVo
在这里插入图片描述
Controller方法想接收一个文件,这里文件名称必须是file,可以使用@RequestParam(“file”)

    @ApiOperation(value = "导入")
    @PostMapping("importData")
    public R importData(MultipartFile file) {
        dictService.importDictData(file);
        return R.ok();
    }
1、创建监听器读取

invoke,每读一行,这个方法就调用一次

@Component
public class DictListener extends AnalysisEventListener<DictEeVo> {

    @Autowired
    private DictMapper dictMapper;

    //一行一行读取
    @Override
    public void invoke(DictEeVo dictEeVo, AnalysisContext analysisContext) {
        //调用方法添加数据库
        Dict dict = new Dict();
        BeanUtils.copyProperties(dictEeVo,dict);
        dictMapper.insert(dict);
    }
    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {

    }
}
2.service层实现业务逻辑
    @Override
    public void importDictData(MultipartFile file) {
        try {
            EasyExcel.read(file.getInputStream(), DictEeVo.class, dictListener)
                    .sheet()
                    .doRead();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
3.使用postman测试文件上传接口在这里插入图片描述

查看数据库的字典表,导入成功
在这里插入图片描述


前端部分

在这里插入图片描述
点击弹窗框上选择导入的文件,visible.sync控制显示隐藏弹出框

    <el-dialog title="导入" :visible.sync="dialogImportVisible" width="480px">
      <el-form label-position="right" label-width="170px">
        <el-form-item label="文件">
          <el-upload
            :multiple="false"
            :on-success="onUploadSuccess"
            :action="'http://localhost:8202/admin/cmn/dict/importData'"
            class="upload-demo"
          >
            <el-button size="small" type="primary">点击上传</el-button>
            <div slot="tip" class="el-upload__tip">
              只能上传xls文件,且不超过500kb
            </div>
          </el-upload>
        </el-form-item>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button @click="dialogImportVisible = false">取消</el-button>
      </div>
    </el-dialog>

效果如下:
在这里插入图片描述
导入成功回调方法

    importData() {
      this.dialogImportVisible = true;
    },
    
    onUploadSuccess(response, file) {
      this.$message.info("导入成功");
      //关闭弹出框
      this.dialogImportVisible = false;
      //重新查询一级数据
      this.getDictList(1);
    },

Spring Cache

Spring Cache 是一个非常优秀的缓存组件;是Spring基于aop提供的自动缓存管理

只需要通过注解标注到查询的业务方法上 可以将查询方法返回的结果缓存起来,以后查询时有缓存不在执行业务代码

Spring Cache步骤:
1、为springcache提供一个缓存管理接口的实现
2、启动类/配置类 添加**@EnableCaching注解
3、在需要缓存管理的业务方法(查询)上使用
@Cachable(…key )** 标注

spring会自动管理业务方法的数据缓存 调用第一步实现的缓存管理对象的生命周期方法管理缓存


1、缓存管理接口的实现

redis缓存:缓存的目的避免(减少)客户端从mysql中读取数据

1.添加依赖
<!-- redis -->
<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- spring2.X集成redis所需common-pool2-->
<dependency>
     <groupId>org.apache.commons</groupId>
     <artifactId>commons-pool2</artifactId>
     <version>2.6.0</version>
</dependency>
2.创建配置类(拷贝)

在这里插入图片描述
@EnableCaching 必须要加,否则spring-data-cache相关注解不会生效…

@Configuration
@EnableCaching
public class RedisConfig {

    /**
     * 设置RedisTemplate规则
     * @param redisConnectionFactory
     * @return
     */
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

//解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
// 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);

//序列号key value
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);

        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    /**
     * 设置CacheManager缓存规则
     * @param factory
     * @return
     */
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

//解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);

// 配置序列化(解决乱码的问题),过期时间600秒
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(600))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                .disableCachingNullValues();

        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
        return cacheManager;
    }
}

当然还有第二版,这一版是之后加的(这版是老师讲过的,还算是能看懂。。。)

@Configuration
public class RedisCacheConfig {

    //1、RedisTemplate配置键和值的序列化器
    @Autowired
    RedisTemplate redisTemplate;
    @PostConstruct
    public void init(){
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
    }

    //2、缓存管理CacheManager接口的实现
    //LettuceConnectionFactory commons-pool2包中提供的Redis连接池工厂类
    @Bean
    public CacheManager cacheManager(LettuceConnectionFactory connectionFactory){
        RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(30)) //缓存过期时间
                .disableCachingNullValues() //不缓存空值
                .serializeKeysWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(new StringRedisSerializer())) //键序列化器
                .serializeValuesWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(new GenericJackson2JsonRedisSerializer()));//值序列化器
        RedisCacheManager cacheManager = RedisCacheManager.builder(connectionFactory)
                    .cacheDefaults(cacheConfig).build();
        return cacheManager;
    }
}
3.配置文件
spring.redis.host=192.168.2.108
spring.redis.port=6379
spring.redis.database= 0
spring.redis.timeout=1800000

#连接池最大连接数
spring.redis.lettuce.pool.max-active=20
#连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.lettuce.pool.max-wait=-1
#连接池中的最大空闲连接
spring.redis.lettuce.pool.max-idle=5
#连接池中的最小空闲连接
spring.redis.lettuce.pool.min-idle=0

2、使用@Cachable缓存管理

@Cacheable添加缓存

1、查询数据字典+@Cacheable

  • 如果缓存存在,则直接读取缓存数据返回;
  • 如果缓存不存在,则执行方法,并把返回的结果存入缓存中。

1.key 可选属性,可以使用 SpEL 标签自定义缓存的key, key中可以获取业务方法的形参值
2.#号代表这是一个 SpEL 表达式,此表达式可以遍历方法的参数对象
@Cacheable(value = "dictCache", key = "'dict_'+#id")
3. 缓存的key: 使用value和key的值拼接,如: dictCache::dict_id的值

如果参数是对象Dict dict,可以这么获取
> key="'dict_'+#dict.value"

    //value:命名空间
    @Cacheable(value = "dictCache", key = "'dict_'+#id")
    @Override
    public List<Dict> findChildData(Long id) {
        QueryWrapper<Dict> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("parent_id", id);
        //向list集合每个dict对象中设置hasChildren
        List<Dict> dicts = baseMapper.selectList(queryWrapper);
        System.out.println("从mysql中查询id=" + id + "的数据");
        dicts.forEach(dict -> {
            dict.setHasChildren(this.isChildren(dict.getId()));
        });
        return dicts;
    }

redis中效果:
在这里插入图片描述


@CacheEvict缓存驱逐

放在删除、更新方法上

使用该注解**@CacheEvict**标志的方法,会清空指定的缓存。一般用在更新或者删除方法上
allEntries 是否清除当前 value值空间下的所有缓存数据。默认false

导入数据的时候,把redis中数据都清除

    @CacheEvict(value = "dictCache", allEntries = true)
    @Override
    public void importDictData(MultipartFile file) {
        try {
            EasyExcel.read(file.getInputStream(), DictEeVo.class, dictListener)
                    .sheet()
                    .doRead();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

@CachePut更新缓存

用在修改方法或添加方法上
在这里插入图片描述

同步缓存的做法
当mysql中的数据发生改变,就会清空redis中对应命名空间下的缓存数据
在这里插入图片描述


3、手动缓存管理

由于缓存雪崩的问题,我们需要手动管理缓存。。使TTL设置的时间不那么集中。虽然这是另外一个方法,不要在意这些细节。。。

@Autowired
PmsClient pmsClient;//远程服务调用的feign接口

@Autowired
RedisTemplate redisTemplate;
//删除上面的Cacheable注解
@Override
public List<CategoryEntity> levelTwoAndSubsCates(String cid) {
    //使用缓存管理
    //1、从缓存中查询cid的二级三级分类集合  如果有直接返回
    String key = "idx:cache:cates:"+cid;
    Object obj = redisTemplate.opsForValue().get(key);
    if(obj!=null){
        return (List<CategoryEntity>) obj;
    }
    //2、如果缓存没有 再远程查询二三级分类集合
    ResponseVo<List<CategoryEntity>> responseVo = pmsClient.lv2AndSubsCates(cid);
    //将查询结果存到缓存中
    redisTemplate.opsForValue()
        .set(key,responseVo.getData() ,
             1800+new Random().nextInt(200), TimeUnit.SECONDS);
    return responseVo.getData();
}

4、缓存一致性

数据库数据更新后如何保证缓存的数据和数据库数据一致:

  • 双写模式:数据库更新同时更新redis缓存数据

    ​ 先写哪一个存在问题:redis和数据库之间的事务不容易保证

  • 失效模式: 更新数据库时 让缓存失效

    ​ 更新数据库业务执行时 缓存会失效,此时如果数据还未写成功 有请求查询数据查到了数据库中还未更新的数据到缓存中,此时才写成功 缓存的数据和数据库仍然不一致

  • 双删模式:更新数据库前删除一次缓存 更新成功后再删除一次缓存

  • 数据库同步中间件:基于mysql的binlog日志将数据库更新的数据同步到第三方的中间件(redis mq es)

    • canal:阿里开源的一个框架

5、缓存并发问题

1.缓存雪崩

缓存雪崩:首页数据访问量大,数据缓存过期时间接近 会导致多个缓存同时失效,大量的请求可能直接查询数据库

解决:随机因子 让多个热门key失效的时间分散


2.缓存击穿

缓存击穿:单个热点key突然失效,导致大量的请求同时访问数据库查询数据

​解决:控制只让一个线程查数据 其他的等待使用缓存 分布式锁


3.缓存穿透

缓存穿透 : 访问数据库一定不存在的数据时,请求每次都会先查询缓存,然后再查数据库

解决:

  1. 空数据也短暂的缓存:为了避免恶意攻击
  2. 布隆过滤器
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值