谁动了我的内存!

一、背景

高流量场景,为了防止数据穿透,减轻数据库压力,如何处理呢?

众所周知,使用缓存使用缓存使用缓存

考虑到缓存,机智的我在redis缓存的基础上再加了一级内存(本地缓存),变成如下结构:

查询逻辑:

先查询内存数据 ------> 无数据则再查询redis ------> 无数据则再查询兜底数据。

还加了兜底数据,简直绝绝子。

压测一波,单副本qps2500,简直完美。

二、问题

接口逻辑:第一条数据是kyc匹配(每个用户返回可能不一样),其他是剩余的列表中的内容,第一条数据会打上tag标签。

信心满满上线,结果竟然出问题了!!!返回的结果中竟然出现了多条数据都有tag标签,为什么?

代码如下:

ini 复制代码
// 从内存中获取数据
List<ScIndexAnalysisDTO> cacheList = analysisService.getScIndexAnaList();
 
List<ScIndexAnalysisDTO> analysisDTOList = new ArrayList<>(cacheList);
 
// 首条数据打上tag
analysisDTOList.get(0).setTag(analysisConfig.getTag());
 
return ScIndexAnalysisConverter.INSTANCE.toVOList(analysisDTOList);

看起来相当正常,甚至还new了一个list来接收,为什么会出问题呢?

原理:

new ArrayList<>(cacheList)这中方式采用的是引用拷贝, Java中的对象都是按引用传递,所以上述代码中的 cacheList 和 analysisDTOList 其实指向的是同一个对象。

当设置tag时,直接修改了原对象中的tag,由于不同用户请求时接口返回值中第一条数据会不一样,导致打上tag标签的数据不一样,最终造成返回结果中有多个tag标签。

三、解决方案

1、不修改原对象

将首条数据打上tag标签这一步放在数据转换之后

ini 复制代码
// 从内存中获取数据
List<ScIndexAnalysisDTO> cacheList = analysisService.getScIndexAnaList();
 
List<ScIndexAnalysisDTO> analysisDTOList = new ArrayList<>(cacheList);
 
 List<ScIndexAnalysisVO> finalAnaDtoList= ScIndexAnalysisConverter.INSTANCE.toVOList( analysisDTOList );
// 首条数据打上tag
finalAnaDtoList.get(0).setTag(analysisConfig.getTag());
 
return finalAnaDtoList;

converter转换的过程其实也是深拷贝。

2、一定要改变属性值,如何处理呢

采用深拷贝来实现!

有两种方法:

(1)ScIndexAnalysisDTO对象implement Cloneable 并实现一个clone方法

java 复制代码
public class ScIndexAnalysisDTOimplements Cloneable{
    private String name;
     
    //实现这个方法
    protected ScIndexAnalysisDTOimplements clone() throws CloneNotSupportedException {
        return (ScIndexAnalysisDTOimplements)super.clone();
    }
}

(2) 手动赋值

ini 复制代码
List<ScIndexAnalysisDTO> b = new ArrayList<>();
for (ScIndexAnalysisDTO element: a) {
   ScIndexAnalysisDTO dto = new ScIndexAnalysisDTO();
   dto.setId(element.getId());
    dto.setName(element.getName());
   b.add(dto);
}

四、总结

在Java中,对象的拷贝可以分为引用拷贝、浅拷贝和深拷贝三种方式,它们各自有不同的特点和应用场景。

**

1、引用拷贝(Reference Copy)

引用拷贝是最简单的一种拷贝方式,它并不创建一个新的对象,只是将新的引用指向已经存在的对象。因此,通过任何一个引用所做的改动都会反映到这个对象上。

示例 复制代码
Person original = new Person("Tom", 30);
Person copy = original;

copy和original都指向了同一个Person对象。如果我们通过copy修改了对象的属性,那么通过original也能看到这个改变。

2、浅拷贝(Shallow Copy)

浅拷贝创建了一个新的对象,但是对于对象内部的非基本类型的字段仍然采用引用拷贝。这意味着,如果原对象中的字段是引用类型,那么在浅拷贝后,原对象和拷贝对象会共享这些引用类型的字段。

示例 复制代码
class Person implements Cloneable {
    String name;
    int age;
    Address address; // 假设Address是另一个包含街道信息的类
 
    public Person(String name, int age, Address address) {
        this.name = name;
        this.age = age;
        this.address = address;
    }
 
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
 
// 使用浅拷贝
Person original = new Person("Tom", 30, new Address("Some Street"));
Person shallowCopy = (Person) original.clone();

在这个例子中,shallowCopy是original的浅拷贝。shallowCopy和original将共享相同的Address对象。

3、深拷贝(Deep Copy)

深拷贝不仅复制了对象本身,还复制了对象内部所有引用类型的成员。这意味着原对象和拷贝对象在内存中是完全独立的,对一个对象的修改不会影响到另一个。

示例 复制代码
class Person implements Cloneable {
    String name;
    int age;
    Address address;
 
    public Person(String name, int age, Address address) {
        this.name = name;
        this.age = age;
        this.address = address;
    }
 
    @Override
    protected Object clone() throws CloneNotSupportedException {
        Person cloned = (Person) super.clone();
        cloned.address = (Address) this.address.clone(); // 深拷贝
        return cloned;
    }
}
 
// 使用深拷贝
Person original = new Person("Tom", 30, new Address("Some Street"));
Person deepCopy = (Person) original.clone();

在进行深拷贝时,需要确保所有引用类型的字段也支持克隆(例如,这里的Address类也需要实现Cloneable接口并重写clone方法)。

4、小结

  • 引用拷贝:不创建新对象,只是增加了一个指向已存在对象的引用。
  • 浅拷贝:创建一个新对象,但是对象内的引用类型字段还是指向原来的对象。
  • 深拷贝:创建一个完全独立的新对象,包括对象内所有的引用类型字段也各自拷贝了一份。
相关推荐
向前看-22 分钟前
验证码机制
前端·后端
超爱吃士力架2 小时前
邀请逻辑
java·linux·后端
AskHarries4 小时前
Spring Cloud OpenFeign快速入门demo
spring boot·后端
isolusion5 小时前
Springboot的创建方式
java·spring boot·后端
zjw_rp5 小时前
Spring-AOP
java·后端·spring·spring-aop
TodoCoder6 小时前
【编程思想】CopyOnWrite是如何解决高并发场景中的读写瓶颈?
java·后端·面试
凌虚6 小时前
Kubernetes APF(API 优先级和公平调度)简介
后端·程序员·kubernetes
机器之心7 小时前
图学习新突破:一个统一框架连接空域和频域
人工智能·后端
.生产的驴8 小时前
SpringBoot 对接第三方登录 手机号登录 手机号验证 微信小程序登录 结合Redis SaToken
java·spring boot·redis·后端·缓存·微信小程序·maven
顽疲8 小时前
springboot vue 会员收银系统 含源码 开发流程
vue.js·spring boot·后端