没弄懂深浅拷贝你也敢用缓存?

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 而其中,最简单的肯定就是第一种服务内部的缓存了。强哥前些天就是用了Guava做了缓存,结果因为代码写的有些乱,就出了个匪夷所思的问题。搞了半天最后才发现问题所在。在这里和大家分享一哈。

哈喽,大家好,我是强哥。


缓存对于我们大家来说并不陌生,在我们日常开发中,常用的缓存大概分以下几种类型:


  • 用Java Map或Guava的Cache做服务内部缓存;
  • H2、Derby、HSQLDB等内存数据库做缓存;
  • Redis、Memcache等类型的分布式缓存;


而其中,最简单的肯定就是第一种服务内部的缓存了。强哥前些天就是用了Guava做了缓存,结果因为代码写的有些乱,就出了个匪夷所思的问题。搞了半天最后才发现问题所在。在这里和大家分享一哈。


提出问题


先看工具类代码:


/**
 * Guava缓存工具类
 */
public class GuavaCacheUtils {
    /**
     * 有效时长(秒)
     */
    public static final Integer DURATION_SECOND = 12 * 60 * 60;
    private static Cache<String, Object> localCache = CacheBuilder.newBuilder().
            maximumSize(100). //key大小限制
            expireAfterWrite(DURATION_SECOND, TimeUnit.SECONDS). //缓存保留时长
            build();
    public static void setKey(String key, Object value) {
        localCache.put(key, value);
    }
    public static Object getKey(String key) {
        return localCache.getIfPresent(key);
    }
}


然后就是问题代码:


public void updateUser() {
  //获取用户缓存
  List<User> allUser = (List<User>) GuavaCacheUtils.getKey(ALL_USER_INFO);
  if (CollectionUtils.isEmpty(allUser)) {
      allUser = getAllUser();//数据库获取数据
      GuavaCacheUtils.setKey(ALL_USER_INFO, allUser);
  }
  List<User> users = new ArrayList<>();
  for (User item : allUser) {
      if (item.friends.contain(’小明‘)) {
          users.add(item);
      }
  }
   //加载配置文件
  ApplicationContext applicationContext=new ClassPathXmlApplicationContext("applicationContext.xml");
  //获取AccountDao实例
  AccountDao accountDao=(AccountDao) applicationContext.getBean("accountDao");
  for (User user : users) {
      //创建Account对象,并向Account对象中添加数据
      Account account=new Account();
      account.setSetAge(user.getAge());
      user.friends = new ArrayList<String>();
      account.setUsername("tom");
      //执行addAccount()方法,并获取返回结果
      int num=accountDao.updateAccount(account);
      if(num>0) {
          System.out.println("成功更新"+num+"条数据!");
      }
  }
}


代码有点长,不过也正是因为写得不清晰,导致了问题的出现。


使用上面的代码之后,在updateUser方法第一次调用时,输出结果为:“成功更新1000条数据!”。可是,在进行第二次方法请求调用时,在没有进行任何外部处理的情况下,却什么都没有输出。而断点调试后发现缓存获取回来的allUser的条数是没有问题的,可是下面这里的判断过后,users的内容却是空的:


List<User> users = new ArrayList<>();
for (User item : allUser) {
    if (item.friends.contain(’小明‘)) {
        users.add(item);
    }
}


这就奇怪了,难不成Guava的缓存有问题,存入缓存里的数据再拿出来,有类型转换或者是什么奇怪的事情发生导致数据没法用了?


可是作为一个广为流传的框架不应该会有这样的问题的,有问题肯定是自己的代码问题。


于是,在经过多次断点调试后,最后终于发现了问题所在。原来是上面的代码改动了缓存的数据,导致再次获取缓存时出了问题。


具体是哪里改了呢?没错,就是这句:


user.friends = new ArrayList<String>();


强哥在发现问题之后,还觉得这个问题有点意思,有种:“我不杀人,别人却因我而死”的味道。


分析问题


既然找到了问题点,那这里也就引入了我们今天要讲的主题深浅拷贝对缓存的影响。


假如我们上面缓存allUser使用的是Redis而不是Guava Cache。那结果肯定是可以正常运行的,为什么呢?


因为Redis是深拷贝,而Guava Cache是浅拷贝。Guava Cache其实内部存储原理类似ConcurrentMap


我们在把数据缓存到Guava Cache中之后,如果之后对存入它的数据引用进行二次操作,其结果是会影响到缓存中的数据的。


也正是因为这个,导致了我们第一次代码运行能正常更新到数据,而第二次却什么也更新不到。


这点确实是在我们使用服务内存缓存时要多注意。浅拷贝类型的内存缓存,在数据存入缓存之后,就最好不再使用该对象或对象引用进行写操作。否则,原本我们的用意是为了使用缓存做快照,存下当时的数据,却会因为后面的修改导致快照被破坏。而且这种问题在代码复杂后就很不容易发现。


当然,解决办法也很简单,就上面的代码而言,我们只要把存入users的数据做一个深拷贝操作,然后再对拷贝后的对象进行操作即可(当然这里只是给个例子,循环里面做深拷贝还是挺影响性能的):


List<User> users = new ArrayList<>();
for (User item : allUser) {
    if (item.friends.contain(’小明‘)) {
        User tmp = SerializationUtils.clone(item);
        users.add(item);
    }
}


Java实现深拷贝


上面我们说了Redis和Guava Cache做缓存在深浅拷贝上的区别。而用一个深拷贝操作解决了问题。那么,Java中有哪些常用的深拷贝方式呢?


强哥也给大家搜到了一个总结,觉得挺好的,具体内容见今天第二篇推文。


16.png

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
7月前
|
存储 Python
【数据结构】期中考试一把梭(通宵版上)
【数据结构】期中考试一把梭(通宵版上)
108 1
|
小程序 编译器 C语言
c++重中之重:“换个龟壳继续套娃“:运算符重载等的学习
c++重中之重:“换个龟壳继续套娃“:运算符重载等的学习
127 0
|
存储 算法 Java
揭开数组的真面目
揭开数组的真面目
105 0
|
存储 Java
揭开链表的真面目
揭开链表的真面目
151 0
|
存储 缓存 NoSQL
面试:第十一章:缓存
面试:第十一章:缓存
118 0
|
存储 缓存 前端开发
|
数据采集 算法 关系型数据库
透了,Mysql索引中最不容易记的三个知识点通透了
Mysql索引中最不容易记的三个知识点通透了
112 0
透了,Mysql索引中最不容易记的三个知识点通透了
|
设计模式 架构师 Java
为什么有些蛮厉害的人,后来都不咋样了
写这篇文章目的是之前在一篇文章中谈到,我实习那会有个老哥很牛皮,业务能力嘎嘎厉害,但是后面发展一般般,这引起我的思考,最近有个同事发了篇腾讯pcg的同学关于review 相关的文章,里面也谈到架构师的层次,也再次引起我关于架构师的相关思考,接下来我们展开聊聊吧~
161 0
|
Java 数据库连接 数据库
信不信十分钟让你彻底搞懂java反射
概念:反射是Java的一种机制,让我们可以在运行时获取类的信息 作用:通过反射,我们可以在程序运行时动态创建对象,还能获取到类的所有信息,比如它的属性、构造器、方法、注解等;
18151 2
信不信十分钟让你彻底搞懂java反射
|
编译器 C++
<C++>一篇文章搞懂类和对象中常函数和常对象的实质以及避免空指针访问的小妙招
<C++>一篇文章搞懂类和对象中常函数和常对象的实质以及避免空指针访问的小妙招
166 0