一、背景
高流量场景,为了防止数据穿透,减轻数据库压力,如何处理呢?
众所周知,使用缓存 !使用缓存 !使用缓存!
考虑到缓存,机智的我在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、小结
- 引用拷贝:不创建新对象,只是增加了一个指向已存在对象的引用。
- 浅拷贝:创建一个新对象,但是对象内的引用类型字段还是指向原来的对象。
- 深拷贝:创建一个完全独立的新对象,包括对象内所有的引用类型字段也各自拷贝了一份。