缓存和它的那些淘汰算法们
为什么我们需要缓存?
很久很久以前,在还没有缓存的时候……用户经常是去请求一个对象,而这个对象是从数据库去取,然后,这个对象变得越来越大,这个用户每次的请求时间也越来越长了,这也把数据库弄得很痛苦,他无时不刻不在工作。所以,这个事情就把用户和数据库弄得很生气,接着就有可能发生下面两件事情:
1.用户很烦,在抱怨,甚至不去用这个应用了(这是大多数情况下都会发生的)
2.数据库为打包回家,离开这个应用,然后,就出现了大麻烦(没地方去存储数据了)(发生在极少数情况下)
上帝派来了缓存
在几年之后,IBM(60年代)的研究人员引进了一个新概念,它叫“缓存”。
什么是缓存?
正如开篇所讲,缓存是“存贮数据(使用频繁的数据)的临时地方,因为取原始数据的代价太大了,所以我可以取得快一些。”
缓存可以认为是数据的池,这些数据是从数据库里的真实数据复制出来的,并且为了能正确取回,被标上了标签(键 ID)。太棒了
programmer one 已经知道这点了,但是他还不知道下面的缓存术语。
命中:
当客户发起一个请求(我们说他想要查看一个产品信息),我们的应用接受这个请求,并且如果是在第一次检查缓存的时候,需要去数据库读取产品信息。
如果在缓存中,一个条目通过一个标记被找到了,这个条目就会被使用、我们就叫它缓存命中。所以,命中率也就不难理解了。
Cache Miss:
但是这里需要注意两点:
1. 如果还有缓存的空间,那么,没有命中的对象会被存储到缓存中来。
2. 如果缓存满了,而又没有命中缓存,那么就会按照某一种策略,把缓存中的旧对象踢出,而把新的对象加入缓存池。而这些策略统称为替代策略(缓存算法),这些策略会决定到底应该提出哪些对象。
存储成本:
当没有命中时,我们会从数据库取出数据,然后放入缓存。而把这个数据放入缓存所需要的时间和空间,就是存储成本。
索引成本:
和存储成本相仿。
失效:
当存在缓存中的数据需要更新时,就意味着缓存中的这个数据失效了。
替代策略:
当缓存没有命中时,并且缓存容量已经满了,就需要在缓存中踢出一个老的条目,加入一条新的条目,而到底应该踢出什么条目,就由替代策略决定。
最优替代策略:
最优的替代策略就是想把缓存中最没用的条目给踢出去,但是未来是不能够被预知的,所以这种策略是不可能实现的。但是有很多策略,都是朝着这个目前去努力。
缓存算法
没有人能说清哪种缓存算法优于其他的缓存算法
Least Frequently Used(LFU):
大家好,我是 LFU,我会计算为每个缓存对象计算他们被使用的频率。我会把最不常用的缓存对象踢走。
Least Recently User(LRU):
我是 LRU 缓存算法,我把最近最少使用的缓存对象给踢走。
我总是需要去了解在什么时候,用了哪个缓存对象。如果有人想要了解我为什么总能把最近最少使用的对象踢掉,是非常困难的。
浏览器就是使用了我(LRU)作为缓存算法。新的对象会被放在缓存的顶部,当缓存达到了容量极限,我会把底部的对象踢走,而技巧就是:我会把最新被访问的缓存对象,放到缓存池的顶部。
所以,经常被读取的缓存对象就会一直呆在缓存池中。有两种方法可以实现我,array 或者是 linked list。
我的速度很快,我也可以被数据访问模式适配。我有一个大家庭,他们都可以完善我,甚至做的比我更好(我确实有时会嫉妒,但是没关系)。我家庭的一些成员包括 LRU2 和 2Q,他们就是为了完善 LRU 而存在的。
First in First out(FIFO):
我是先进先出,我是一个低负载的算法,并且对缓存对象的管理要求不高。我通过一个队列去跟踪所有的缓存对象,最近最常用的缓存对象放在后面,而更早的缓存对象放在前面,当缓存容量满时,排在前面的缓存对象会被踢走,然后把新的缓存对象加进去。我很快,但是我并不适用。
Second Chance:
大家好,我是 second chance,我是通过 FIFO 修改而来的,被大家叫做 second chance 缓存算法,我比 FIFO 好的地方是我改善了 FIFO 的成本。我是 FIFO 一样也是在观察队列的前端,但是很FIFO的立刻踢出不同,我会检查即将要被踢出的对象有没有之前被使用过的标志(1一个 bit 表示),没有被使用过,我就把他踢出;否则,我会把这个标志位清除,然后把这个缓存对象当做新增缓存对象加入队列。你可以想象就这就像一个环队列。当我再一次在队头碰到这个对象时,由于他已经没有这个标志位了,所以我立刻就把他踢开了。我在速度上比 FIFO 快。
其他的缓存算法还考虑到了下面几点:
成本 :如果缓存对象有不同的成本,应该把那些难以获得的对象保存下来。
容量 :如果缓存对象有不同的大小,应该把那些大的缓存对象清除,这样就可以让更多的小缓存对象进来了。
时间 :一些缓存还保存着缓存的过期时间。电脑会失效他们,因为他们已经过期了。
根据缓存对象的大小而不管其他的缓存算法可能是有必要的。
看看缓存元素(缓存实体)
public class CacheElement
{
private Object objectValue;
private Object objectKey;
private int index;
private int hitCount; // getters and setters
}
这个缓存实体拥有缓存的key和value,这个实体的数据结构会被以下所有缓存算法用到。
缓存算法的公用代码
public final synchronized void addElement(Object key, Object value)
{
int index;
Object obj;
// get the entry from the table
obj = table.get(key);
// If we have the entry already in our table
// then get it and replace only its value.
obj = table.get(key);
if (obj != null)
{
CacheElement element;
element = (CacheElement) obj;
element.setObjectValue(value);
element.setObjectKey(key);
return;
}
}
上面的代码会被所有的缓存算法实现用到。这段代码是用来检查缓存元素是否在缓存中了,如果是,我们就替换它,但是如果我们找不到这个 key 对应的缓存,我们会怎么做呢?那我们就来深入的看看会发生什么吧!
现场访问
今天的专题很特殊,因为我们有特殊的客人,事实上他们是我们想要听的与会者,但是首先,先介绍一下我们的客人:Random Cache,FIFO Cache。让我们从 Random Cache开始。
看看随机缓存的实现
public final synchronized void addElement(Object key, Object value)
{
int index;
Object obj;
obj = table.get(key);
if (obj != null)
{
CacheElement element;// Just replace the value.
element = (CacheElement) obj;
element.setObjectValue(value);
element.setObjectKey(key);
return;
}// If we haven't filled the cache yet, put it at the end.
if (!isFull())
{
index = numEntries;
++numEntries;
}
else { // Otherwise, replace a random entry.
index = (int) (cache.length * random.nextFloat());
table.remove(cache[index].getObjectKey());
}
cache[index].setObjectValue(value);
cache[index].setObjectKey(key);
table.put(key, cache[index]);
}
看看FIFO缓算法的实现
public final synchronized void addElement(Objectkey, Object value)
{
int index;
Object obj;
obj = table.get(key);
if (obj != null)
{
CacheElement element; // Just replace the value.
element = (CacheElement) obj;
element.setObjectValue(value);
element.setObjectKey(key);
return;
}
// If we haven't filled the cache yet, put it at the end.
if (!isFull())
{
index = numEntries;
++numEntries;
}
else { // Otherwise, replace the current pointer,
// entry with the new one.
index = current;
// in order to make Circular FIFO
if (++current >= cache.length)
current = 0;
table.remove(cache[index].getObjectKey());
}
cache[index].setObjectValue(value);
cache[index].setObjectKey(key);
table.put(key, cache[index]);
}
看看LFU缓存算法的实现
public synchronized Object getElement(Object key)
{
Object obj;
obj = table.get(key);
if (obj != null)
{
CacheElement element = (CacheElement) obj;
element.setHitCount(element.getHitCount() + 1);
return element.getObjectValue();
}
return null;
}
public final synchronized void addElement(Object key, Object value)
{
Object obj;
obj = table.get(key);
if (obj != null)
{
CacheElement element; // Just replace the value.
element = (CacheElement) obj;
element.setObjectValue(value);
element.setObjectKey(key);
return;
}
if (!isFull())
{
index = numEntries;
++numEntries;
}
else
{
CacheElement element = removeLfuElement();
index = element.getIndex();
table.remove(element.getObjectKey());
}
cache[index].setObjectValue(value);
cache[index].setObjectKey(key);
cache[index].setIndex(index);
table.put(key, cache[index]);
}
public CacheElement removeLfuElement()
{
CacheElement[] elements = getElementsFromTable();
CacheElement leastElement = leastHit(elements);
return leastElement;
}
public static CacheElement leastHit(CacheElement[] elements)
{
CacheElement lowestElement = null;
for (int i = 0; i < elements.length; i++)
{
CacheElement element = elements[i];
if (lowestElement == null)
{
lowestElement = element;
}
else {
if (element.getHitCount() < lowestElement.getHitCount())
{
lowestElement = element;
}
}
}
return lowestElement;
}
最重点的代码,就应该是 leastHit 这个方法,这段代码就是把hitCount 最低的元素找出来,然后删除,给新进的缓存元素留位置
看看LRU缓存算法实现
private void moveToFront(int index)
{
int nextIndex, prevIndex;
if(head != index)
{
nextIndex = next[index];
prevIndex = prev[index];
// Only the head has a prev entry that is an invalid index
// so we don't check.
next[prevIndex] = nextIndex;
// Make sure index is valid. If it isn't, we're at the tail
// and don't set prev[next].
if(nextIndex >= 0)
prev[nextIndex] = prevIndex;
else
tail = prevIndex;
prev[index] = -1;
next[index] = head;
prev[head] = index;
head = index;
}
}
public final synchronized void addElement(Object key, Object value)
{
int index;Object obj;
obj = table.get(key);
if(obj != null)
{
CacheElement entry;
// Just replace the value, but move it to the front.
entry = (CacheElement)obj;
entry.setObjectValue(value);
entry.setObjectKey(key);
moveToFront(entry.getIndex());
return;
}
// If we haven't filled the cache yet, place in next available
// spot and move to front.
if(!isFull())
{
if(_numEntries > 0)
{
prev[_numEntries] = tail;
next[_numEntries] = -1;
moveToFront(numEntries);
}
++numEntries;
}
else { // We replace the tail of the list.
table.remove(cache[tail].getObjectKey());
moveToFront(tail);
}
cache[head].setObjectValue(value);
cache[head].setObjectKey(key);
table.put(key, cache[head]);
}
这段代码的逻辑如 LRU算法 的描述一样,把再次用到的缓存提取到最前面,而每次删除的都是最后面的元素。
缓存技术杂谈
一般而言,现在互联网应用(网站或App)的整体流程,可以概括如图1所示,用户请求从界面(浏览器或App界面)到网络转发、应用服务再到存储(数据库或文件系统),然后返回到界面呈现内容。
随着互联网的普及,内容信息越来越复杂,用户数和访问量越来越大,我们的应用需要支撑更多的并发量,同时我们的应用服务器和数据库服务器所做的计算也越来越多。但是往往我们的应用服务器资源是有限的,且技术变革是缓慢的,数据库每秒能接受的请求次数也是有限的(或者文件的读写也是有限的),如何能够有效利用有限的资源来提供尽可能大的吞吐量?一个有效的办法就是引入缓存,打破标准流程,每个环节中请求可以从缓存中直接获取目标数据并返回,从而减少计算量,有效提升响应速度,让有限的资源服务更多的用户。
如图1所示,缓存的使用可以出现在1~4的各个环节中,每个环节的缓存方案与使用各有特点。
图1 互联网应用一般流程
缓存特征
缓存也是一个数据模型对象,那么必然有它的一些特征:
命中率
命中率=返回正确结果数/请求缓存次数,命中率问题是缓存中的一个非常重要的问题,它是衡量缓存有效性的重要指标。命中率越高,表明缓存的使用率越高。
最大元素(或最大空间)
缓存中可以存放的最大元素的数量,一旦缓存中元素数量超过这个值(或者缓存数据所占空间超过其最大支持空间),那么将会触发缓存启动清空策略根据不同的场景合理的设置最大元素值往往可以一定程度上提高缓存的命中率,从而更有效的时候缓存。
清空策略
如上描述,缓存的存储空间有限制,当缓存空间被用满时,如何保证在稳定服务的同时有效提升命中率?这就由缓存清空策略来处理,设计适合自身数据特征的清空策略能有效提升命中率。常见的一般策略有:
FIFO(first in first out)
先进先出策略,最先进入缓存的数据在缓存空间不够的情况下(超出最大元素限制)会被优先被清除掉,以腾出新的空间接受新的数据。策略算法主要比较缓存元素的创建时间。在数据实效性要求场景下可选择该类策略,优先保障最新数据可用。
LFU(less frequently used)
最少使用策略,无论是否过期,根据元素的被使用次数判断,清除使用次数较少的元素释放空间。策略算法主要比较元素的hitCount(命中次数)。在保证高频数据有效性场景下,可选择这类策略。
LRU(least recently used)
最近最少使用策略,无论是否过期,根据元素最后一次被使用的时间戳,清除最远使用时间戳的元素释放空间。策略算法主要比较元素最近一次被get使用时间。在热点数据场景下较适用,优先保证热点数据的有效性。
除此之外,还有一些简单策略比如:
根据过期时间判断,清理过期时间最长的元素;
根据过期时间判断,清理最近要过期的元素;
随机清理;
根据关键字(或元素内容)长短清理等。
缓存介质
虽然从硬件介质上来看,无非就是内存和硬盘两种,但从技术上,可以分成内存、硬盘文件、数据库。
内存:将缓存存储于内存中是最快的选择,无需额外的I/O开销,但是内存的缺点是没有持久化落地物理磁盘,一旦应用异常break down而重新启动,数据很难或者无法复原。
硬盘:一般来说,很多缓存框架会结合使用内存和硬盘,在内存分配空间满了或是在异常的情况下,可以被动或主动的将内存空间数据持久化到硬盘中,达到释放空间或备份数据的目的。
数据库:前面有提到,增加缓存的策略的目的之一就是为了减少数据库的I/O压力。现在使用数据库做缓存介质是不是又回到了老问题上了?其实,数据库也有很多种类型,像那些不支持SQL,只是简单的key-value存储结构的特殊数据库(如BerkeleyDB和Redis),响应速度和吞吐量都远远高于我们常用的关系型数据库等。
缓存分类和应用场景
缓存有各类特征,而且有不同介质的区别,那么实际工程中我们怎么去对缓存分类呢?在目前的应用服务框架中,比较常见的,时根据缓存雨应用的藕合度,分为local cache(本地缓存)和remote cache(分布式缓存):
本地缓存:指的是在应用中的缓存组件,其最大的优点是应用和cache是在同一个进程内部,请求缓存非常快速,没有过多的网络开销等,在单应用不需要集群支持或者集群情况下各节点无需互相通知的场景下使用本地缓存较合适;同时,它的缺点也是应为缓存跟应用程序耦合,多个应用程序无法直接的共享缓存,各应用或集群的各节点都需要维护自己的单独缓存,对内存是一种浪费。
分布式缓存:指的是与应用分离的缓存组件或服务,其最大的优点是自身就是一个独立的应用,与本地应用隔离,多个应用可直接的共享缓存。
目前各种类型的缓存都活跃在成千上万的应用服务中,还没有一种缓存方案可以解决一切的业务场景或数据类型,我们需要根据自身的特殊场景和背景,选择最适合的缓存方案。缓存的使用是程序员、架构师的必备技能,好的程序员能根据数据类型、业务场景来准确判断使用何种类型的缓存,如何使用这种缓存,以最小的成本最快的效率达到最优的目的。
本地缓存
编程直接实现缓存
个别场景下,我们只需要简单的缓存数据的功能,而无需关注更多存取、清空策略等深入的特性时,直接编程实现缓存则是最便捷和高效的。
a. 成员变量或局部变量实现
简单代码示例如下:
public void UseLocalCache(){
//一个本地的缓存变量
Map localCacheStoreMap = new HashMap();
List infosList = this.getInfoList();
for(Object item:infosList){
if(localCacheStoreMap.containsKey(item)){ //缓存命中 使用缓存数据
// todo
} else { // 缓存未命中 IO获取数据,结果存入缓存
Object valueObject = this.getInfoFromDB();
localCacheStoreMap.put(valueObject.toString(), valueObject);
}
}
}
//示例
private List getInfoList(){
return new ArrayList();
}
//示例数据库IO获取
private Object getInfoFromDB(){
return new Object();
}
以局部变量map结构缓存部分业务数据,减少频繁的重复数据库I/O操作。缺点仅限于类的自身作用域内,类间无法共享缓存。
b. 静态变量实现
最常用的单例实现静态资源缓存,代码示例如下:
public class CityUtils {
private static final HttpClient httpClient = ServerHolder.createClientWithPool();
private static Map cityIdNameMap = new HashMap();
private static Map districtIdNameMap = new HashMap();
static {
HttpGet get = new HttpGet("http://gis-in.sankuai.com/api/location/city/all");
BaseAuthorizationUtils.generateAuthAndDateHeader(get,
BaseAuthorizationUtils.CLIENT_TO_REQUEST_MDC,
BaseAuthorizationUtils.SECRET_TO_REQUEST_MDC);
try {
String resultStr = httpClient.execute(get, new BasicResponseHandler());
JSONObject resultJo = new JSONObject(resultStr);
JSONArray dataJa = resultJo.getJSONArray("data");
for (int i = 0; i < dataJa.length(); i++) {
JSONObject itemJo = dataJa.getJSONObject(i);
cityIdNameMap.put(itemJo.getInt("id"), itemJo.getString("name"));
}
} catch (Exception e) {
throw new RuntimeException("Init City List Error!", e);
}
}
static {
HttpGet get = new HttpGet("http://gis-in.sankuai.com/api/location/district/all");
BaseAuthorizationUtils.generateAuthAndDateHeader(get,
BaseAuthorizationUtils.CLIENT_TO_REQUEST_MDC,
BaseAuthorizationUtils.SECRET_TO_REQUEST_MDC);
try {
String resultStr = httpClient.execute(get, new BasicResponseHandler());
JSONObject resultJo = new JSONObject(resultStr);
JSONArray dataJa = resultJo.getJSONArray("data");
for (int i = 0; i < dataJa.length(); i++) {
JSONObject itemJo = dataJa.getJSONObject(i);
districtIdNameMap.put(itemJo.getInt("id"), itemJo.getString("name"));
}
} catch (Exception e) {
throw new RuntimeException("Init District List Error!", e);
}
}
public static String getCityName(int cityId) {
String name = cityIdNameMap.get(cityId);
if (name == null) {
name = "未知";
}
return name;
}
public static String getDistrictName(int districtId) {
String name = districtIdNameMap.get(districtId);
if (name == null) {
name = "未知";
}
return name;
}
}
O2O业务中常用的城市基础基本信息判断,通过静态变量一次获取缓存内存中,减少频繁的I/O读取,静态变量实现类间可共享,进程内可共享,缓存的实时性稍差。
为了解决本地缓存数据的实时性问题,目前大量使用的是结合ZooKeeper的自动发现机制,实时变更本地静态变量缓存:
美团内部的基础配置组件MtConfig,采用的就是类似原理,使用静态变量缓存,结合ZooKeeper的统一管理,做到自动动态更新缓存,如图2所示。
图2 Mtconfig实现图
这类缓存实现,优点是能直接在heap区内读写,最快也最方便;缺点同样是受heap区域影响,缓存的数据量非常有限,同时缓存时间受GC影响。主要满足单机场景下的小数据量缓存需求,同时对缓存数据的变更无需太敏感感知,如上一般配置管理、基础静态数据等场景。
Ehcache
Ehcache是现在最流行的纯Java开源缓存框架,配置简单、结构清晰、功能强大,是一个非常轻量级的缓存实现,我们常用的Hibernate里面就集成了相关缓存功能。
图3 Ehcache框架图
整体上看,Ehcache的使用还是相对简单便捷的,提供了完整的各类API接口。需要注意的是,虽然Ehcache支持磁盘的持久化,但是由于存在两级缓存介质,在一级内存中的缓存,如果没有主动的刷入磁盘持久化的话,在应用异常down机等情形下,依然会出现缓存数据丢失,为此可以根据需要将缓存刷到磁盘,将缓存条目刷到磁盘的操作可以通过cache.flush()方法来执行,需要注意的是,对于对象的磁盘写入,前提是要将对象进行序列化。
主要特性:
快速,针对大型高并发系统场景,Ehcache的多线程机制有相应的优化改善。
简单,很小的jar包,简单配置就可直接使用,单机场景下无需过多的其他服务依赖。
支持多种的缓存策略,灵活。
缓存数据有两级:内存和磁盘,与一般的本地内存缓存相比,有了磁盘的存储空间,将可以支持更大量的数据缓存需求。
具有缓存和缓存管理器的侦听接口,能更简单方便的进行缓存实例的监控管理。
支持多缓存管理器实例,以及一个实例的多个缓存区域。
注意:Ehcache的超时设置主要是针对整个cache实例设置整体的超时策略,而没有较好的处理针对单独的key的个性的超时设置(有策略设置,但是比较复杂,就不描述了),因此,在使用中要注意过期失效的缓存元素无法被GC回收,时间越长缓存越多,内存占用也就越大,内存泄露的概率也越大。
分布式缓存
memcached缓存
memcached是应用较广的开源分布式缓存产品之一,它本身其实不提供分布式解决方案。在服务端,memcached集群环境实际就是一个个memcached服务器的堆积,环境搭建较为简单;cache的分布式主要是在客户端实现,通过客户端的路由处理来达到分布式解决方案的目的。客户端做路由的原理非常简单,应用服务器在每次存取某key的value时,通过某种算法把key映射到某台memcached服务器nodeA上,因此这个key所有操作都在nodeA上,结构图如图6、图7所示。
图6 memcached客户端路由图
图7 memcached一致性hash示例图
memcached客户端采用一致性hash算法作为路由策略,如图7,相对于一般hash(如简单取模)的算法,一致性hash算法除了计算key的hash值外,还会计算每个server对应的hash值,然后将这些hash值映射到一个有限的值域上(比如0~2^32)。通过寻找hash值大于hash(key)的最小server作为存储该key数据的目标server。如果找不到,则直接把具有最小hash值的server作为目标server。同时,一定程度上,解决了扩容问题,增加或删除单个节点,对于整个集群来说,不会有大的影响。最近版本,增加了虚拟节点的设计,进一步提升了可用性。
无特殊场景下,key-value能满足需求的前提下,使用memcached分布式集群是较好的选择,搭建与操作使用都比较简单;分布式集群在单点故障时,只影响小部分数据异常,目前还可以通过Magent缓存代理模式,做单点备份,提升高可用;整个缓存都是基于内存的,因此响应时间是很快,不需要额外的序列化、反序列化的程序,但同时由于基于内存,数据没有持久化,集群故障重启数据无法恢复。高版本的memcached已经支持CAS模式的原子操作,可以低成本的解决并发控制问题。
Redis缓存
Redis是一个远程内存数据库(非关系型数据库),性能强劲,具有复制特性以及解决问题而生的独一无二的数据模型。它可以存储键值对与5种不同类型的值之间的映射,可以将存储在内存的键值对数据持久化到硬盘,可以使用复制特性来扩展读性能,还可以使用客户端分片来扩展写性能。
图9 Redis数据模型图
个人总结了以下多种Web应用场景,在这些场景下可以充分的利用Redis的特性,大大提高效率。
在主页中显示最新的项目列表:Redis使用的是常驻内存的缓存,速度非常快。LPUSH用来插入一个内容ID,作为关键字存储在列表头部。LTRIM用来限制列表中的项目数最多为5000。如果用户需要的检索的数据量超越这个缓存容量,这时才需要把请求发送到数据库。
删除和过滤:如果一篇文章被删除,可以使用LREM从缓存中彻底清除掉。
排行榜及相关问题:排行榜(leader board)按照得分进行排序。ZADD命令可以直接实现这个功能,而ZREVRANGE命令可以用来按照得分来获取前100名的用户,ZRANK可以用来获取用户排名,非常直接而且操作容易。
按照用户投票和时间排序:排行榜,得分会随着时间变化。LPUSH和LTRIM命令结合运用,把文章添加到一个列表中。一项后台任务用来获取列表,并重新计算列表的排序,ZADD命令用来按照新的顺序填充生成列表。列表可以实现非常快速的检索,即使是负载很重的站点。
过期项目处理:使用Unix时间作为关键字,用来保持列表能够按时间排序。对current_time和time_to_live进行检索,完成查找过期项目的艰巨任务。另一项后台任务使用ZRANGE…WITHSCORES进行查询,删除过期的条目。
计数:进行各种数据统计的用途是非常广泛的,比如想知道什么时候封锁一个IP地址。INCRBY命令让这些变得很容易,通过原子递增保持计数;GETSET用来重置计数器;过期属性用来确认一个关键字什么时候应该删除。
特定时间内的特定项目:这是特定访问者的问题,可以通过给每次页面浏览使用SADD命令来解决。SADD不会将已经存在的成员添加到一个集合。
Pub/Sub:在更新中保持用户对数据的映射是系统中的一个普遍任务。Redis的pub/sub功能使用了SUBSCRIBE、UNSUBSCRIBE和PUBLISH命令,让这个变得更加容易。
队列:在当前的编程中队列随处可见。除了push和pop类型的命令之外,Redis还有阻塞队列的命令,能够让一个程序在执行时被另一个程序添加到队列。 缓存常见问题
前面一节说到了《为什么说Redis是单线程的以及Redis为什么这么快!》,今天给大家整理一篇关于Redis经常被问到的问题:缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级等概念的入门及简单解决方案。
一、缓存雪崩
缓存雪崩我们可以简单的理解为:由于原有缓存失效,新缓存未到期间 (例如:我们设置缓存时采用了相同的过期时间,在同一时刻出现大面积的缓存过期),所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机。从而形成一系列连锁反应,造成整个系统崩溃。
缓存正常从Redis中获取,示意图如下:
缓存失效瞬间示意图如下:
缓存失效时的雪崩效应对底层系统的冲击非常可怕!大多数系统设计者考虑用加锁 或者队列 的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。还有一个简单方案就时讲缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
以下简单介绍两种实现方式的伪代码:
(1)碰到这种情况,一般并发量不是特别多的时候,使用最多的解决方案是加锁排队 ,伪代码如下:
//伪代码
public object GetProductListNew() {
int cacheTime = 30;
String cacheKey = "product_list";
String lockKey = cacheKey;
String cacheValue = CacheHelper.get(cacheKey);
if (cacheValue != null) {
return cacheValue;
} else {
synchronized(lockKey) {
cacheValue = CacheHelper.get(cacheKey);
if (cacheValue != null) {
return cacheValue;
} else {
//这里一般是sql查询数据
cacheValue = GetProductListFromDB();
CacheHelper.Add(cacheKey, cacheValue, cacheTime);
}
}
return cacheValue;
}
}
加锁排队只是为了减轻数据库的压力,并没有提高系统吞吐量。假设在高并发下,缓存重建期间key是锁着的,这是过来1000个请求999个都在阻塞的。同样会导致用户等待超时,这是个治标不治本的方法!
注意 :加锁排队的解决方式分布式环境的并发问题,有可能还要解决分布式锁的问题;线程还会被阻塞,用户体验很差!因此,在真正的高并发场景下很少使用!
(2)还有一个解决办法解决方案是:给每一个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓存标记失效,则更新数据缓存,实例伪代码如下:
//伪代码
public object GetProductListNew() {
int cacheTime = 30;
String cacheKey = "product_list";
//缓存标记
String cacheSign = cacheKey + "_sign";
String sign = CacheHelper.Get(cacheSign);
//获取缓存值
String cacheValue = CacheHelper.Get(cacheKey);
if (sign != null) {
return cacheValue; //未过期,直接返回
} else {
CacheHelper.Add(cacheSign, "1", cacheTime);
ThreadPool.QueueUserWorkItem((arg) -> {
//这里一般是 sql查询数据
cacheValue = GetProductListFromDB();
//日期设缓存时间的2倍,用于脏读
CacheHelper.Add(cacheKey, cacheValue, cacheTime * 2);
});
return cacheValue;
}
}
解释说明:
1、缓存标记:记录缓存数据是否过期,如果过期会触发通知另外的线程在后台去更新实际key的缓存;
2、缓存数据:它的过期时间比缓存标记的时间延长1倍,例:标记缓存时间30分钟,数据缓存设置为60分钟。 这样,当缓存标记key过期后,实际缓存还能把旧数据返回给调用端,直到另外的线程在后台更新完成后,才会返回新缓存。
关于缓存崩溃的解决方法,这里提出了三种方案:使用锁或队列、设置过期标志更新缓存、为key设置不同的缓存失效时间,还有一各被称为“二级缓存”的解决方法,有兴趣的读者可以自行研究。
二、缓存穿透
缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题。
有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器 ,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
另外也有一个更为简单粗暴的方法,如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。通过这个直接设置的默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库,这种办法最简单粗暴!
//伪代码
public object GetProductListNew() {
int cacheTime = 30;
String cacheKey = "product_list";
String cacheValue = CacheHelper.Get(cacheKey);
if (cacheValue != null) {
return cacheValue;
}
cacheValue = CacheHelper.Get(cacheKey);
if (cacheValue != null) {
return cacheValue;
} else {
//数据库查询不到,为空
cacheValue = GetProductListFromDB();
if (cacheValue == null) {
//如果发现为空,设置个默认值,也缓存起来
cacheValue = string.Empty;
}
CacheHelper.Add(cacheKey, cacheValue, cacheTime);
return cacheValue;
}
}
把空结果,也给缓存起来,这样下次同样的请求就可以直接返回空了,即可以避免当查询的值为空时引起的缓存穿透。同时也可以单独设置个缓存区域存储空值,对要查询的key进行预先校验,然后再放行给后面的正常缓存处理逻辑。
三、缓存预热
缓存预热这个应该是一个比较常见的概念,相信很多小伙伴都应该可以很容易的理解,缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!
解决思路:
1、直接写个缓存刷新页面,上线时手工操作下;
2、数据量不大,可以在项目启动的时候自动进行加载;
3、定时刷新缓存;
四、缓存更新
除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:
(1)定时去清理过期的缓存;
(2)当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。
两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。
五、缓存降级
当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。
降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。
在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:
(1)一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
(2)警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
(3)错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
(4)严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。
六、总结
这些都是实际项目中,可能碰到的一些问题,也是面试的时候经常会被问到的知识点,实际上还有很多很多各种各样的问题,文中的解决方案,也不可能满足所有的场景,相对来说只是对该问题的入门解决方法。一般正式的业务场景往往要复杂的多,应用场景不同,方法和解决方案也不同,由于上述方案,考虑的问题并不是很全面,因此并不适用于正式的项目开发,但是可以作为概念理解入门,具体解决方案要根据实际情况来确定!
微信公众号
个人公众号:程序员黄小斜
微信公众号【程序员黄小斜】新生代青年聚集地,程序员成长充电站。作者黄小斜,职业是阿里程序员,身份是斜杠青年,希望和更多的程序员交朋友,一起进步和成长!专注于分享技术、面试、职场等成长干货,这一次,我们一起出发。
关注公众号后回复“2019”领取我这两年整理的学习资料,涵盖自学编程、求职面试、算法刷题、Java技术学习、计算机基础和考研等8000G资料合集。
技术公众号:Java技术江湖
微信公众号【Java技术江湖】一位阿里 Java 工程师的技术小站,专注于 Java 相关技术:SSM、SpringBoot、MySQL、分布式、中间件、集群、Linux、网络、多线程,偶尔讲点Docker、ELK,同时也分享技术干货和学习经验,致力于Java全栈开发!
关注公众号后回复“PDF”即可领取200+页的《Java工程师面试指南》强烈推荐,几乎涵盖所有Java工程师必知必会的知识点。
你可能感兴趣的:(分布式系统,分布式系统理论与实践,Java技术江湖)
一分钟快速搭建 Spring Boot 项目
沉默王二
Java进阶之路 Java程序员进阶之路 java spring boot spring
大家好,我是二哥呀!学Java,自然少不了SpringBoot的学习!作为一名Java技术博主,已经被催了N多次SpringBoot的教程了,再不更新真有点鸽的不成样子——鸽上加鸽。因此我打算在接下来一段时间里间歇性地输出一些SpringBoot方面的文章,包括一些非常入门级的教程和一些非常优质的SpringBoot项目推荐,已经身经百战的starter们忽略就好。《Java程序员进阶之路》在Gi
一致性哈希HashRing
留白1108
哈希算法 算法 一致性哈希
一致性哈希HashRing一致性哈希算法是一种高效的分布式存储和负载均衡技术,广泛应用于分布式系统中,如缓存集群、分布式数据库等。它通过将数据和节点映射到一个环形的哈希空间,实现了数据的均匀分布和节点的动态扩展。本文将详细介绍一致性哈希算法的原理,并通过一个完整的Java实现来展示其应用。一、一致性哈希算法原理一致性哈希算法的核心思想是将数据和节点映射到一个环形的哈希空间中。具体步骤如下:1.哈希
基于一致性哈希的分布式Top-K
留白1108
哈希算法 分布式 算法 TopK
基于一致性哈希的分布式Top-K在分布式系统中,数据的高效存储和快速查询是一个常见的挑战。一致性哈希(ConsistentHashing)是一种常用于分布式存储和负载均衡的技术,而Top-K查询则是数据分析中的经典问题。本文将通过一个Java实现的案例,展示如何结合一致性哈希和多线程技术,高效地完成分布式环境下的Top-K计算。实现思路一致性哈希分片:将数据通过一致性哈希算法分配到不同节点。局部T
spring_cache介绍和探索
stayhungerstayflush
spring基础介绍 spring java 后端
SpringAOP技术详解:缓存抽象与性能优化实战引言在分布式系统和高并发场景中,缓存技术犹如性能优化的"瑞士军刀"。Spring框架3.1引入的声明式缓存抽象层,通过简洁的注解实现了缓存逻辑与业务代码的解耦。本文将深入剖析Spring缓存机制的核心原理,结合实战案例演示如何实现毫秒级响应优化。一、核心概念全景解析1.1注解驱动模型Spring缓存抽象通过5大核心注解构建声明式缓存体系://类级别
分布式系统设计(架构能力)
时光不负追梦人
架构 java
一、微服务架构服务治理Nacos注册中心(AP模式)CAP选择:Nacos默认采用AP模式(可用性+分区容忍性),通过心跳检测实现服务健康管理。服务发现:客户端定时拉取服务列表,支持权重路由和元数据过滤。配置管理:通过dataId和group动态推送配置,支持灰度发布。Sentinel熔断规则持久化规则存储:将流控、熔断规则持久化到Nacos/ZooKeeper,避免重启丢失。动态更新:通过Dat
通俗版解释:分布式和微服务就像开餐厅
斗-匕
分布式 微服务 架构
一、分布式系统:把大厨房拆成多个小厨房想象你开了一家超火爆的餐厅,但原来的厨房太小了:问题:一个厨师要同时切菜、炒菜、烤面包,手忙脚乱还容易出错。解决方案:拆分成多个小厨房(分布式):切菜间:专门处理食材准备炒菜间:只管炒菜甜品站:专注做蛋糕优势:效率暴增:每个小厨房专注做一件事抗风险:炒菜间着火了,其他厨房还能工作代价:需要传菜员(网络通信)在各厨房跑腿要协调各厨房的进度(分布式事务)二、微服务
面试基础--分布式一致性算法深度解析
WeiLai1112
后端 面试 分布式 算法 java 后端 架构 spring boot
分布式一致性算法深度解析:RaftvsPaxos原理、实践与源码实现引言在分布式系统设计中,一致性算法是确保多节点数据同步和系统高可用的核心技术。Raft和Paxos作为两种最经典的分布式一致性算法,支撑了Etcd、ZooKeeper、TiDB等众多核心基础设施。本文将从算法原理、工程实践、源码实现三个维度对比Raft与Paxos,结合大厂真实案例,为分布式系统设计提供选型与实现指南。1.分布式一
面试基础---分布式架构基础:CAP 理论与 BASE
WeiLai1112
后端 面试 redis junit java 架构 分布式 后端
分布式架构基础:CAP理论与BASE理论深度解析引言在互联网大厂的高并发、高可用场景下,分布式系统的设计是至关重要的。CAP理论和BASE理论是分布式系统设计的基石,理解这些理论对于设计高可用、高性能的分布式系统至关重要。本文将深入探讨CAP理论和BASE理论,结合实际项目案例和源码分析,帮助读者深入理解其实现原理。1.CAP理论CAP理论是分布式系统设计的基础理论之一,由EricBrewer在2
redis分布式锁
JLiuli
redis 分布式锁 java
什么是分布式锁在分布式系统下,通过锁机制来控制资源的访问,与传统的单体项目中的synchronized,他是基于jvm的锁,即在一个springboot服务下能保证线程同步的问题,但现在我们大部分的项目部署不局限于一台服务器,此时会出现多把jvm锁无法保证对数据的互斥原则。分布式锁就像是把锁单独抽出来为一个应用,让所有springboot项目公用同一把锁。分布式锁的特点1.互斥性:任意时刻锁只能被
深入理解 C# 中的 DTO(数据传输对象)
鲤籽鲲
C# c# 开发语言 C# 知识捡漏
总目录前言在软件开发中,特别是在分布式系统和微服务架构中,数据传输对象(DTO,DataTransferObject)是一个非常重要的设计模式。它用于简化数据在不同层或组件之间的传输过程,提高代码的可维护性和性能。本文将详细介绍C#中的DTO。一、什么是DTO?1.基本定义DTO是一种仅包含数据、不含业务逻辑的轻量级对象,其核心目标是在系统不同层级或组件之间高效、安全地传输数据。DTO将所需数据整
K8S常见的面试题
水月清辉
k8s
kubernetes面试题汇总1、k8s是什么?请说出你的了解?答:Kubenetes是一个针对容器应用,进行自动部署,弹性伸缩和管理的开源系统。主要功能是生产环境中的容器编排。K8S是Google公司推出的,它来源于由Google公司内部使用了15年的Borg系统,集结了Borg的精华。2、K8s架构的组成是什么?答:和大多数分布式系统一样,K8S集群至少需要一个主节点(Master)和多个计算
全方位解读消息队列:原理、优势、实例与实践要点
恩爸编程
消息队列 消息队列 消息队列是什么 消息队列讲解 消息队列介绍 消息队列概念 消息队列认识 消息队列作用
全方位解读消息队列:原理、优势、实例与实践要点一、消息队列基础认知在数字化转型浪潮下,分布式系统架构愈发复杂,消息队列成为其中关键一环。不妨把消息队列想象成一个超级“信息驿站”,在古代,各地的信件、物资运输依赖驿站周转,消息队列与之类似。系统里的不同程序模块,也就是生产者,把各类数据、指令“打包”成消息,投递到这个驿站。驿站有着一套成熟的存放、排序规则,而其他模块,即消费者,按照需求依次来驿站取走
hadoop框架与核心组件刨析(三)YARN
小刘爱喇石( ˝ᗢ̈˝ )
hadoop 大数据 分布式
一、负载均衡的概念负载均衡(LoadBalancing)是一种将工作负载(如网络流量、计算任务或数据请求)分配到多个资源(如服务器、计算节点或存储设备)的技术,目的是优化资源使用、最大化吞吐量、最小化响应时间,并避免单个资源过载。负载均衡广泛应用于计算机网络、分布式系统、云计算等领域。负载均衡的核心目标提高性能:通过将负载分配到多个资源,避免单个资源成为瓶颈,从而提高系统的整体性能。提高可用性:如
Kafka 消息不丢失:全方位保障策略
艾斯比的日常
kafka linq 分布式
Kafka消息不丢失:全方位保障策略引言在现代分布式系统中,Kafka作为一款高性能、高可扩展性的消息队列,被广泛应用于数据传输、日志收集、实时流处理等场景。然而,消息丢失是使用Kafka时可能面临的一个严重问题,这可能会导致数据不一致、业务逻辑错误等后果。因此,确保Kafka消息不丢失至关重要。本文将从生产者、Broker和消费者三个层面详细介绍保障Kafka消息不丢失的方法。生产者层面保障确认
k8s service type_RabbitMQ(k8s)-随手笔记
weixin_39873325
k8s service type
介绍基于Erlang开发消息队列(AMQP)在分布式系统中,有一些功能我们希望能够提高系统稳定性,比如说支付、订单功能,服务后移,长时间操作的功能,同步数据我们通过监听数据变化实现功能联动特点分布式各节点互相冗余元数据(erlang.cookie,队列、交换机、绑定元数据、vhost元数据)实现分布式集群(类似于session),通过转发达到集群任意节点进入都是集群主节点可以任意增加节点实现集群水
分布式中间件:Redis介绍
顾北辰20
分布式中间件 分布式 中间件 redis
目录Redis概述Redis的特点高性能丰富的数据结构持久化分布式特性简单易用Redis的数据结构字符串(String)哈希(Hash)列表(List)集合(Set)有序集合(SortedSet)Redis的应用场景缓存消息队列分布式锁计数器排行榜在当今的分布式系统开发中,中间件起着至关重要的作用。其中,Redis作为一款高性能的键值对存储数据库,在缓存、消息队列、分布式锁等多个领域都有着广泛的应
Java爬取淘宝商品详情高级版接口的完整指南
Jelena15779585792
淘宝API Java java 开发语言
在电商数据分析和市场研究中,获取淘宝商品的详细信息是一项重要任务。淘宝开放平台提供了taobao.item.get和taobao.item.get_pro等高级接口,允许开发者通过商品ID(num_iid)获取商品的标题、价格、图片、描述、SKU等详细信息。本文将详细介绍如何使用Java技术实现一个高效、稳定的爬虫程序,从接口调用、数据解析到存储,帮助你快速获取淘宝商品详情。一、环境搭建与依赖配置
万字总结!常见分布式ID解决方案(数据库、算法、开源组件)
Java爱好狂.
wpf java 后端
分布式ID分布式ID(DistributedID)是指在分布式系统中生成全局唯一的标识符,用于标识不同实体或数据对象。在分布式系统中,由于数据存储、计算和处理都分散在不同的节点上,因此需要一个可靠的方式来跟踪和标识这些数据对象。分布式ID最低要求:erlang复制代码全局唯一:ID的全局唯一性肯定是首先要满足的高性能:分布式ID的生成速度要快,对本地资源消耗要小高可用:生成分布式ID的服务要保证可
调用链追踪(Trace ID)
18你磊哥
java
前言:在Java中实现调用链追踪(TraceID)通常用于分布式系统中跟踪请求的完整链路,常见的实现方式包括手动编码或使用开源框架(如SkyWalking、Zipkin、SpringCloudSleuth等)。以下是具体实现方法及示例:1.手动实现TraceID通过ThreadLocal或MDC(MappedDiagnosticContext)存储TraceID,并在请求链路中传递。步骤1:定义T
请说一下你对分布式和微服务的理解
LiuYuHani
分布式 微服务 架构
分布式系统定义:分布式系统由多个独立计算机(节点)组成,这些节点通过网络通信协作完成任务,对外表现为一个整体。特点:分布性:节点分布在不同的物理位置。并发性:多个节点可以同时执行任务。透明性:用户无需关心系统的分布细节。容错性:部分节点故障时,系统仍能运行。优点:可扩展性:通过增加节点提升系统性能。高可用性:节点故障时,系统仍能提供服务。资源共享:节点可以共享计算和存储资源。挑战:一致性:保持数据
深入探索 Dubbo:高效的 Java RPC 框架
Kale又菜又爱玩
dubbo java rpc
深入探索Dubbo:高效的JavaRPC框架随着微服务架构的流行,分布式系统中的服务间通信变得愈加复杂。Dubbo作为阿里巴巴开源的高性能JavaRPC框架,已成为开发高可用、高性能微服务架构的核心工具之一。本文将深入探讨Dubbo的核心特性、配置方法,以及如何利用Dubbo提供的高级功能来构建一个高效、可靠的分布式系统。什么是Dubbo?Dubbo是一个轻量级、高性能的JavaRPC框架,主要用
从单块巨石到星辰大海:分布式与微服务的本质思考
斗-匕
分布式 微服务 架构
一、分布式系统:宇宙观的代码映射1.核心命题的进化单机时代(1960s-2000s):冯·诺依曼架构的终极演绎,摩尔定律撑起性能天花板分布式觉醒(2000s-):CAP定理的启示——放弃"完美系统"的幻想,在妥协中寻找最优解2.分布式三定律物理定律:光速限制下的通信延迟不可消除经济定律:成本边际效应决定拆分粒度组织定律:康威定律的幽灵始终在场(系统架构≈组织架构)3.典型范式对比模式特征案例主从架
面试基础---MySQL 分布式 ID 方案深度解析
WeiLai1112
mysql vue.js
MySQL分布式ID方案深度解析:UUID、自增ID与雪花算法引言在分布式系统中,生成全局唯一的ID是一个常见的需求。MySQL作为最流行的关系型数据库之一,如何在高并发、分布式环境下生成唯一ID是一个重要的技术挑战。本文将深入探讨MySQL分布式ID的生成方案,包括UUID、自增ID和雪花算法,结合实际项目案例和源码分析,帮助读者深入理解其实现原理。1.分布式ID的需求与挑战在分布式系统中,生成
一致性哈希函数处理负载均衡(简单实现,勿喷)
01_
哈希算法 负载均衡 算法
一致性哈希算法是分布式系统中常用的负载均衡算法,特别适合动态变化的服务节点场景。它的核心思想是将服务节点和数据映射到一个虚拟的哈希环上,通过哈希值定位数据所属的节点。当节点增加或减少时,一致性哈希算法能够最小化数据迁移的影响。算法设计:1.数据结构:哈希环:使用map(有序map)/unordered_map(键无序map)来存储虚拟节点和真实节点的映射关系。虚拟节点:为了提高负载均衡的均匀性,为
Spring Boot 缓存最佳实践:从基础到生产的完整指南
天才选手Yoke
java springboot spring boot 缓存 后端 redis
SpringBoot缓存最佳实践:从基础到生产的完整指南引言在现代分布式系统中,缓存是提升系统性能的银弹。SpringBoot通过spring-boot-starter-cache模块提供了开箱即用的缓存抽象,但如何根据业务需求实现灵活、可靠的缓存方案?本文将带您从零开始,逐步构建符合生产要求的缓存系统。一、基础篇:5分钟快速接入1.1最小化配置pom.xml依赖org.springframewo
java面试问题大全及答案大全
小白教程
java面试题 java 面试 开发语言 java面试题 java面试问题大全 java面试题带答案 Java经典面试题
文章目录前言java面试题-Java基础java面试题-JVM知识java面试题-多线程与并发java面试题-主流框架java面试题-数据库相关java面试题-分布式与微服务java面试题-网络知识前言该文档围绕Java技术栈展开,全面涵盖了基础、JVM、多线程与并发、主流框架、数据库、分布式、网络等核心知识领域,以面试题及参考答案的形式呈现,为Java开发者提供了系统复习与深入理解的资料。有需要
HarmonyNext实战:基于ArkTS的分布式实时消息队列系统开发
harmonyos-next
HarmonyNext实战:基于ArkTS的分布式实时消息队列系统开发引言在HarmonyNext生态系统中,消息队列是实现分布式系统通信和异步处理的核心组件。本文将深入探讨如何使用ArkTS语言开发一个分布式实时消息队列系统,重点介绍消息的生产、消费、路由以及负载均衡等核心功能的实现。我们将通过一个完整的实战案例,展示如何利用HarmonyNext的分布式能力和ArkTS的高效性能,构建一个高效
深入解析Java MDC:日志链路追踪的利器
没什么技术
java MDC
一、什么是MDC?MDC(MappedDiagnosticContext)是SLF4J提供的一个线程安全的诊断上下文工具。它允许开发者在同一线程上下文中存储多个键值对信息,这些信息可以自动附加到日志输出中,实现日志的上下文关联。二、MDC的核心作用作用说明典型场景链路追踪跟踪请求完整处理流程分布式系统调用跟踪上下文传递跨方法传递公共参数用户ID、机构号等透传日志增强自动添加公共字段到日志请求IP、
Grok 3能否打破大模型的魔咒?
TGITCIC
AI-大模型的落地之道 grok grok3 大模型 小模型 scaling law 开源大模型
新模型旧魔咒Grok3的问世,仿佛是科技界的一声惊雷。面对老掉牙的大模型法则,大家不禁要问:这到底意味着什么?以前,一提深度学习就能引出一场血雨腥风,现如今却有人说“没钱也能玩”。这风浪可真是一波未平一波又起。也许这就是科技的魅力:一统江湖的法则瞬间瓦解。缩小与提升大模型不再是唯一的解决方案,大家发现,原来小模型也可以撬动市场。不过,面对如何提升模型的智商,各路英雄却依然不得不面对两个选择:大力度
Redis和MySQL数据一致问题怎么解决
昔我往昔
数据库 redis mysql 数据库
在分布式系统中,Redis和MySQL经常同时使用,Redis通常作为缓存系统,而MySQL作为持久化数据库。二者的数据一致性和安全性问题需要特别关注。常见的挑战包括:数据一致性:Redis和MySQL之间的数据可能存在不同步的情况,尤其是在高并发场景下。缓存穿透:当缓存中没有数据时,系统会直接查询数据库,导致数据库负载过重。缓存击穿:缓存中的数据失效,导致大量并发请求直接访问数据库。缓存雪崩:缓
安装数据库首次应用
Array_06
java oracle sql
可是为什么再一次失败之后就变成直接跳过那个要求
enter full pathname of java.exe的界面
这个java.exe是你的Oracle 11g安装目录中例如:【F:\app\chen\product\11.2.0\dbhome_1\jdk\jre\bin】下的java.exe 。不是你的电脑安装的java jdk下的java.exe!
注意第一次,使用SQL D
Weblogic Server Console密码修改和遗忘解决方法
bijian1013
Welogic
在工作中一同事将Weblogic的console的密码忘记了,通过网上查询资料解决,实践整理了一下。
一.修改Console密码
打开weblogic控制台,安全领域 --> myrealm -->&n
IllegalStateException: Cannot forward a response that is already committed
Cwind
java Servlets
对于初学者来说,一个常见的误解是:当调用 forward() 或者 sendRedirect() 时控制流将会自动跳出原函数。标题所示错误通常是基于此误解而引起的。 示例代码:
protected void doPost() {
if (someCondition) {
sendRedirect();
}
forward(); // Thi
基于流的装饰设计模式
木zi_鸣
设计模式
当想要对已有类的对象进行功能增强时,可以定义一个类,将已有对象传入,基于已有的功能,并提供加强功能。
自定义的类成为装饰类
模仿BufferedReader,对Reader进行包装,体现装饰设计模式
装饰类通常会通过构造方法接受被装饰的对象,并基于被装饰的对象功能,提供更强的功能。
装饰模式比继承灵活,避免继承臃肿,降低了类与类之间的关系
装饰类因为增强已有对象,具备的功能该
Linux中的uniq命令
被触发
linux
Linux命令uniq的作用是过滤重复部分显示文件内容,这个命令读取输入文件,并比较相邻的行。在正常情 况下,第二个及以后更多个重复行将被删去,行比较是根据所用字符集的排序序列进行的。该命令加工后的结果写到输出文件中。输入文件和输出文件必须不同。如 果输入文件用“- ”表示,则从标准输入读取。
AD:
uniq [选项] 文件
说明:这个命令读取输入文件,并比较相邻的行。在正常情况下,第二个
正则表达式Pattern
肆无忌惮_
Pattern
正则表达式是符合一定规则的表达式,用来专门操作字符串,对字符创进行匹配,切割,替换,获取。
例如,我们需要对QQ号码格式进行检验
规则是长度6~12位 不能0开头 只能是数字,我们可以一位一位进行比较,利用parseLong进行判断,或者是用正则表达式来匹配[1-9][0-9]{4,14} 或者 [1-9]\d{4,14}
&nbs
Oracle高级查询之OVER (PARTITION BY ..)
知了ing
oracle sql
一、rank()/dense_rank() over(partition by ...order by ...)
现在客户有这样一个需求,查询每个部门工资最高的雇员的信息,相信有一定oracle应用知识的同学都能写出下面的SQL语句:
select e.ename, e.job, e.sal, e.deptno
from scott.emp e,
(se
Python调试
矮蛋蛋
python pdb
原文地址:
http://blog.csdn.net/xuyuefei1988/article/details/19399137
1、下面网上收罗的资料初学者应该够用了,但对比IBM的Python 代码调试技巧:
IBM:包括 pdb 模块、利用 PyDev 和 Eclipse 集成进行调试、PyCharm 以及 Debug 日志进行调试:
http://www.ibm.com/d
webservice传递自定义对象时函数为空,以及boolean不对应的问题
alleni123
webservice
今天在客户端调用方法
NodeStatus status=iservice.getNodeStatus().
结果NodeStatus的属性都是null。
进行debug之后,发现服务器端返回的确实是有值的对象。
后来发现原来是因为在客户端,NodeStatus的setter全部被我删除了。
本来是因为逻辑上不需要在客户端使用setter, 结果改了之后竟然不能获取带属性值的
java如何干掉指针,又如何巧妙的通过引用来操作指针————>说的就是java指针
百合不是茶
C语言的强大在于可以直接操作指针的地址,通过改变指针的地址指向来达到更改地址的目的,又是由于c语言的指针过于强大,初学者很难掌握, java的出现解决了c,c++中指针的问题 java将指针封装在底层,开发人员是不能够去操作指针的地址,但是可以通过引用来间接的操作:
定义一个指针p来指向a的地址(&是地址符号):
Eclipse打不开,提示“An error has occurred.See the log file ***/.log”
bijian1013
eclipse
打开eclipse工作目录的\.metadata\.log文件,发现如下错误:
!ENTRY org.eclipse.osgi 4 0 2012-09-10 09:28:57.139
!MESSAGE Application error
!STACK 1
java.lang.NoClassDefFoundError: org/eclipse/core/resources/IContai
spring aop实例annotation方法实现
bijian1013
java spring AOP annotation
在spring aop实例中我们通过配置xml文件来实现AOP,这里学习使用annotation来实现,使用annotation其实就是指明具体的aspect,pointcut和advice。1.申明一个切面(用一个类来实现)在这个切面里,包括了advice和pointcut
AdviceMethods.jav
[Velocity一]Velocity语法基础入门
bit1129
velocity
用户和开发人员参考文档
http://velocity.apache.org/engine/releases/velocity-1.7/developer-guide.html
注释
1.行级注释##
2.多行注释#* *#
变量定义
使用$开头的字符串是变量定义,例如$var1, $var2,
赋值
使用#set为变量赋值,例
【Kafka十一】关于Kafka的副本管理
bit1129
kafka
1. 关于request.required.acks
request.required.acks控制者Producer写请求的什么时候可以确认写成功,默认是0,
0表示即不进行确认即返回。
1表示Leader写成功即返回,此时还没有进行写数据同步到其它Follower Partition中
-1表示根据指定的最少Partition确认后才返回,这个在
Th
lua统计nginx内部变量数据
ronin47
lua nginx 统计
server {
listen 80;
server_name photo.domain.com;
location /{set $str $uri;
content_by_lua '
local url = ngx.var.uri
local res = ngx.location.capture(
java-11.二叉树中节点的最大距离
bylijinnan
java
import java.util.ArrayList;
import java.util.List;
public class MaxLenInBinTree {
/*
a. 1
/ \
2 3
/ \ / \
4 5 6 7
max=4 pass "root"
Netty源码学习-ReadTimeoutHandler
bylijinnan
java netty
ReadTimeoutHandler的实现思路:
开启一个定时任务,如果在指定时间内没有接收到消息,则抛出ReadTimeoutException
这个异常的捕获,在开发中,交给跟在ReadTimeoutHandler后面的ChannelHandler,例如
private final ChannelHandler timeoutHandler =
new ReadTim
jquery验证上传文件样式及大小(好用)
cngolon
文件上传 jquery验证
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<script src="jquery1.8/jquery-1.8.0.
浏览器兼容【转】
cuishikuan
css 浏览器 IE
浏览器兼容问题一:不同浏览器的标签默认的外补丁和内补丁不同
问题症状:随便写几个标签,不加样式控制的情况下,各自的margin 和padding差异较大。
碰到频率:100%
解决方案:CSS里 *{margin:0;padding:0;}
备注:这个是最常见的也是最易解决的一个浏览器兼容性问题,几乎所有的CSS文件开头都会用通配符*来设
Shell特殊变量:Shell $0, $#, $*, $@, $?, $$和命令行参数
daizj
shell $# $? 特殊变量
前面已经讲到,变量名只能包含数字、字母和下划线,因为某些包含其他字符的变量有特殊含义,这样的变量被称为特殊变量。例如,$ 表示当前Shell进程的ID,即pid,看下面的代码:
$echo $$
运行结果
29949
特殊变量列表 变量 含义 $0 当前脚本的文件名 $n 传递给脚本或函数的参数。n 是一个数字,表示第几个参数。例如,第一个
程序设计KISS 原则-------KEEP IT SIMPLE, STUPID!
dcj3sjt126com
unix
翻到一本书,讲到编程一般原则是kiss:Keep It Simple, Stupid.对这个原则深有体会,其实不仅编程如此,而且系统架构也是如此。
KEEP IT SIMPLE, STUPID! 编写只做一件事情,并且要做好的程序;编写可以在一起工作的程序,编写处理文本流的程序,因为这是通用的接口。这就是UNIX哲学.所有的哲学真 正的浓缩为一个铁一样的定律,高明的工程师的神圣的“KISS 原
android Activity间List传值
dcj3sjt126com
Activity
第一个Activity:
import java.util.ArrayList;import java.util.HashMap;import java.util.List;import java.util.Map;import android.app.Activity;import android.content.Intent;import android.os.Bundle;import a
tomcat 设置java虚拟机内存
eksliang
tomcat 内存设置
转载请出自出处:http://eksliang.iteye.com/blog/2117772
http://eksliang.iteye.com/
常见的内存溢出有以下两种:
java.lang.OutOfMemoryError: PermGen space
java.lang.OutOfMemoryError: Java heap space
------------
Android 数据库事务处理
gqdy365
android
使用SQLiteDatabase的beginTransaction()方法可以开启一个事务,程序执行到endTransaction() 方法时会检查事务的标志是否为成功,如果程序执行到endTransaction()之前调用了setTransactionSuccessful() 方法设置事务的标志为成功则提交事务,如果没有调用setTransactionSuccessful() 方法则回滚事务。事
Java 打开浏览器
hw1287789687
打开网址 open浏览器 open browser 打开url 打开浏览器
使用java 语言如何打开浏览器呢?
我们先研究下在cmd窗口中,如何打开网址
使用IE 打开
D:\software\bin>cmd /c start iexplore http://hw1287789687.iteye.com/blog/2153709
使用火狐打开
D:\software\bin>cmd /c start firefox http://hw1287789
ReplaceGoogleCDN:将 Google CDN 替换为国内的 Chrome 插件
justjavac
chrome Google google api chrome插件
Chrome Web Store 安装地址: https://chrome.google.com/webstore/detail/replace-google-cdn/kpampjmfiopfpkkepbllemkibefkiice
由于众所周知的原因,只需替换一个域名就可以继续使用Google提供的前端公共库了。 同样,通过script标记引用这些资源,让网站访问速度瞬间提速吧
进程VS.线程
m635674608
线程
资料来源:
http://www.liaoxuefeng.com/wiki/001374738125095c955c1e6d8bb493182103fac9270762a000/001397567993007df355a3394da48f0bf14960f0c78753f000 1、Apache最早就是采用多进程模式 2、IIS服务器默认采用多线程模式 3、多进程优缺点 优点:
多进程模式最大
Linux下安装MemCached
字符串
memcached
前提准备:1. MemCached目前最新版本为:1.4.22,可以从官网下载到。2. MemCached依赖libevent,因此在安装MemCached之前需要先安装libevent。2.1 运行下面命令,查看系统是否已安装libevent。[root@SecurityCheck ~]# rpm -qa|grep libevent libevent-headers-1.4.13-4.el6.n
java设计模式之--jdk动态代理(实现aop编程)
Supanccy2013
java DAO 设计模式 AOP
与静态代理类对照的是动态代理类,动态代理类的字节码在程序运行时由Java反射机制动态生成,无需程序员手工编写它的源代码。动态代理类不仅简化了编程工作,而且提高了软件系统的可扩展性,因为Java 反射机制可以生成任意类型的动态代理类。java.lang.reflect 包中的Proxy类和InvocationHandler 接口提供了生成动态代理类的能力。
&
Spring 4.2新特性-对java8默认方法(default method)定义Bean的支持
wiselyman
spring 4
2.1 默认方法(default method)
java8引入了一个default medthod;
用来扩展已有的接口,在对已有接口的使用不产生任何影响的情况下,添加扩展
使用default关键字
Spring 4.2支持加载在默认方法里声明的bean
2.2
将要被声明成bean的类
public class DemoService {