贼厉害,手撸的 SpringBoot缓存系统,性能杠杠的!

1、缓存基础算法

FIFO(First In First Out),先进先出,和OS里的FIFO思路相同,如果一个数据最先进入缓存中,当缓存满的时候,应当把最先进入缓存的数据给移除掉。

LFU(Least Frequently Used),最不经常使用,如果一个数据在最近一段时间内使用次数很少,那么在将来一段时间内被使用的可能性也很小。

LRU(Least Recently Used),最近最少使用,如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小。也就是说,当限定的空间已存满数据时,应当把最久没有被访问到的数据移除。

2、接口定义

简单定义缓存接口,大致可以抽象如下:

package com.power.demo.cache.contract;

import java.util.function.Function;

/**

* 缓存提供者接口

**/

public interface CacheProviderService {

/**

* 查询缓存

* @param key 缓存键 不可为空

**/

 T get(String key);

/**

* 查询缓存

* @param key      缓存键 不可为空

* @param function 如没有缓存,调用该callable函数返回对象 可为空

**/

 T get(String key, Function<String, T> function);

/**

* 查询缓存

* @param key      缓存键 不可为空

* @param function 如没有缓存,调用该callable函数返回对象 可为空

* @param funcParm function函数的调用参数

**/

<T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm);

/**

* 查询缓存

* @param key        缓存键 不可为空

* @param function   如没有缓存,调用该callable函数返回对象 可为空

* @param expireTime 过期时间(单位:毫秒) 可为空

**/

 T get(String key, Function<String, T> function, Long expireTime);

/**

* 查询缓存

* @param key        缓存键 不可为空

* @param function   如没有缓存,调用该callable函数返回对象 可为空

* @param funcParm   function函数的调用参数

* @param expireTime 过期时间(单位:毫秒) 可为空

**/

<T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm, Long expireTime);

/**

* 设置缓存键值

* @param key 缓存键 不可为空

* @param obj 缓存值 不可为空

**/

 void set(String key, T obj);

/**

* 设置缓存键值

* @param key        缓存键 不可为空

* @param obj        缓存值 不可为空

* @param expireTime 过期时间(单位:毫秒) 可为空

**/

 void set(String key, T obj, Long expireTime);

/**

* 移除缓存

* @param key 缓存键 不可为空

**/

void remove(String key);

/**

* 是否存在缓存

* @param key 缓存键 不可为空

**/

boolean contains(String key);

}

注意,这里列出的只是常见缓存功能接口,一些在特殊场景下用到的统计类的接口、分布式锁、自增(减)等功能不在讨论范围之内。

Get相关方法,注意多个参数的情况,缓存接口里面传人的Function,这是Java8提供的函数式接口,虽然支持的入参个数有限(这里你会非常怀念.NET下的Func委托),但是仅对Java这个语言来说,这真是一个重大的进步_

接口定义好了,下面就要实现缓存提供者程序了。按照存储类型的不同,本文简单实现最常用的两种缓存提供者:本地缓存和分布式缓存

二、本地缓存


本地缓存,也就是JVM级别的缓存(本地缓存可以认为是直接在进程内通信调用,而分布式缓存则需要通过网络进行跨进程通信调用),一般有很多种实现方式,比如直接使用Hashtable、ConcurrentHashMap等天生线程安全的集合作为缓存容器,或者使用一些成熟的开源组件,如EhCache、Guava Cache等。本文选择上手简单的Guava缓存。

1、什么是Guava

Guava,简单来说就是一个开发类库,且是一个非常丰富强大的开发工具包,号称可以让使用Java语言更令人愉悦,主要包括基本工具类库和接口、缓存、发布订阅风格的事件总线等。在实际开发中,我用的最多的是集合、缓存和常用类型帮助类,很多人都对这个类库称赞有加。

2、添加依赖

com.google.guava

guava

3、实现接口

/*

* 本地缓存提供者服务 (Guava Cache)

* */

@Configuration

@ComponentScan(basePackages = AppConst.BASE_PACKAGE_NAME)

@Qualifier(“localCacheService”)

public class LocalCacheProviderImpl implements CacheProviderService {

private static Map<String, Cache<String, Object>> _cacheMap = Maps.newConcurrentMap();

static {

Cache<String, Object> cacheContainer = CacheBuilder.newBuilder()

.maximumSize(AppConst.CACHE_MAXIMUM_SIZE)

.expireAfterWrite(AppConst.CACHE_MINUTE, TimeUnit.MILLISECONDS)//最后一次写入后的一段时间移出

//.expireAfterAccess(AppConst.CACHE_MINUTE, TimeUnit.MILLISECONDS) //最后一次访问后的一段时间移出

.recordStats()//开启统计功能

.build();

_cacheMap.put(String.valueOf(AppConst.CACHE_MINUTE), cacheContainer);

}

/**

* 查询缓存

* @param key 缓存键 不可为空

**/

public  T get(String key) {

T obj = get(key, null, null, AppConst.CACHE_MINUTE);

return obj;

}

/**

* 查询缓存

* @param key      缓存键 不可为空

* @param function 如没有缓存,调用该callable函数返回对象 可为空

**/

public  T get(String key, Function<String, T> function) {

T obj = get(key, function, key, AppConst.CACHE_MINUTE);

return obj;

}

/**

* 查询缓存

* @param key      缓存键 不可为空

* @param function 如没有缓存,调用该callable函数返回对象 可为空

* @param funcParm function函数的调用参数

**/

public <T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm) {

T obj = get(key, function, funcParm, AppConst.CACHE_MINUTE);

return obj;

}

/**

* 查询缓存

* @param key        缓存键 不可为空

* @param function   如没有缓存,调用该callable函数返回对象 可为空

* @param expireTime 过期时间(单位:毫秒) 可为空

**/

public  T get(String key, Function<String, T> function, Long expireTime) {

T obj = get(key, function, key, expireTime);

return obj;

}

/**

* 查询缓存

* @param key        缓存键 不可为空

* @param function   如没有缓存,调用该callable函数返回对象 可为空

* @param funcParm   function函数的调用参数

* @param expireTime 过期时间(单位:毫秒) 可为空

**/

public <T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm, Long expireTime) {

T obj = null;

if (StringUtils.isEmpty(key) == true) {

return obj;

}

expireTime = getExpireTime(expireTime);

Cache<String, Object> cacheContainer = getCacheContainer(expireTime);

try {

if (function == null) {

obj = (T) cacheContainer.getIfPresent(key);

} else {

final Long cachedTime = expireTime;

obj = (T) cacheContainer.get(key, () -> {

T retObj = function.apply(funcParm);

return retObj;

});

}

} catch (Exception e) {

e.printStackTrace();

}

return obj;

}

/**

* 设置缓存键值  直接向缓存中插入值,这会直接覆盖掉给定键之前映射的值

* @param key 缓存键 不可为空

* @param obj 缓存值 不可为空

**/

public  void set(String key, T obj) {

set(key, obj, AppConst.CACHE_MINUTE);

}

/**

* 设置缓存键值  直接向缓存中插入值,这会直接覆盖掉给定键之前映射的值

* @param key        缓存键 不可为空

* @param obj        缓存值 不可为空

* @param expireTime 过期时间(单位:毫秒) 可为空

**/

public  void set(String key, T obj, Long expireTime) {

if (StringUtils.isEmpty(key) == true) {

return;

}

if (obj == null) {

return;

}

expireTime = getExpireTime(expireTime);

Cache<String, Object> cacheContainer = getCacheContainer(expireTime);

cacheContainer.put(key, obj);

}

/**

* 移除缓存

* @param key 缓存键 不可为空

**/

public void remove(String key) {

if (StringUtils.isEmpty(key) == true) {

return;

}

long expireTime = getExpireTime(AppConst.CACHE_MINUTE);

Cache<String, Object> cacheContainer = getCacheContainer(expireTime);

cacheContainer.invalidate(key);

}

/**

* 是否存在缓存

* @param key 缓存键 不可为空

**/

public boolean contains(String key) {

boolean exists = false;

if (StringUtils.isEmpty(key) == true) {

return exists;

}

Object obj = get(key);

if (obj != null) {

exists = true;

}

return exists;

}

private static Lock lock = new ReentrantLock();

private Cache<String, Object> getCacheContainer(Long expireTime) {

Cache<String, Object> cacheContainer = null;

if (expireTime == null) {

return cacheContainer;

}

String mapKey = String.valueOf(expireTime);

if (_cacheMap.containsKey(mapKey) == true) {

cacheContainer = _cacheMap.get(mapKey);

return cacheContainer;

}

try {

lock.lock();

cacheContainer = CacheBuilder.newBuilder()

.maximumSize(AppConst.CACHE_MAXIMUM_SIZE)

.expireAfterWrite(expireTime, TimeUnit.MILLISECONDS)//最后一次写入后的一段时间移出

//.expireAfterAccess(AppConst.CACHE_MINUTE, TimeUnit.MILLISECONDS) //最后一次访问后的一段时间移出

.recordStats()//开启统计功能

.build();

_cacheMap.put(mapKey, cacheContainer);

} finally {

lock.unlock();

}

return cacheContainer;

}

/**

* 获取过期时间 单位:毫秒

* @param expireTime 传人的过期时间 单位毫秒 如小于1分钟,默认为10分钟

**/

private Long getExpireTime(Long expireTime) {

Long result = expireTime;

if (expireTime == null || expireTime < AppConst.CACHE_MINUTE / 10) {

result = AppConst.CACHE_MINUTE;

}

return result;

}

}

4、注意事项

Guava Cache初始化容器时,支持缓存过期策略,类似FIFO、LRU和LFU等算法。

expireAfterWrite:最后一次写入后的一段时间移出。

expireAfterAccess:最后一次访问后的一段时间移出。

Guava Cache对缓存过期时间的设置实在不够友好。常见的应用场景,比如,有些几乎不变的基础数据缓存1天,有些热点数据缓存2小时,有些会话数据缓存5分钟等等。

通常我们认为设置缓存的时候带上缓存的过期时间是非常容易的,而且只要一个缓存容器实例即可,比如.NET下的ObjectCache、System.Runtime.Cache等等。

但是Guava Cache不是这个实现思路,如果缓存的过期时间不同,Guava的CacheBuilder要初始化多份Cache实例。

好在我在实现的时候注意到了这个问题,并且提供了解决方案,可以看到getCacheContainer这个函数,根据过期时长做缓存实例判断,就算不同过期时间的多实例缓存也是完全没有问题的。

三、分布式缓存


分布式缓存产品非常多,本文使用应用普遍的Redis,在Spring Boot应用中使用Redis非常简单。

1、什么是Redis

Redis是一款开源(BSD许可)的、用C语言写成的高性能的键-值存储(key-value store)。它常被称作是一款数据结构服务器(data structure server)。它可以被用作缓存、消息中间件和数据库,在很多应用中,经常看到有人选择使用Redis做缓存,实现分布式锁和分布式Session等。作为缓存系统时,和经典的KV结构的Memcached非常相似,但又有很多不同。

Redis支持丰富的数据类型。Redis的键值可以包括字符串(strings)类型,同时它还包括哈希(hashes)、列表(lists)、集合(sets)和有序集合(sorted sets)等数据类型。对于这些数据类型,你可以执行原子操作。例如:对字符串进行附加操作(append);递增哈希中的值;向列表中增加元素;计算集合的交集、并集与差集等。

Redis的数据类型:

[Keys:非二进制安全的字符类型( not binary-safe strings ),由于key不是binary safe的字符串,所以像“my key”和“mykey\n”这样包含空格和换行的key是不允许的。

Values:Strings、Hash、Lists、 Sets、 Sorted sets。考虑到Redis单线程操作模式,Value的粒度不应该过大,缓存的值越大,越容易造成阻塞和排队。]( )

为了获得优异的性能,Redis采用了内存中(in-memory)数据集(dataset)的方式。同时,Redis支持数据的持久化,你可以每隔一段时间将数据集转存到磁盘上(snapshot),或者在日志尾部追加每一条操作命令(append only file,aof)。

Redis同样支持主从复制(master-slave replication),并且具有非常快速的非阻塞首次同步( non-blocking first synchronization)、网络断开自动重连等功能。

同时Redis还具有其它一些特性,其中包括简单的事物支持、发布订阅 ( pub/sub)、管道(pipeline)和虚拟内存(vm)等 。

2、添加依赖

org.springframework.boot

spring-boot-starter-data-redis

3、配置Redis

在application.properties配置文件中,配置Redis常用参数:

## Redis缓存相关配置

#Redis数据库索引(默认为0)

spring.redis.database=0

#Redis服务器地址

spring.redis.host=127.0.0.1

#Redis服务器端口

spring.redis.port=6379

#Redis服务器密码(默认为空)

spring.redis.password=123321

#Redis连接超时时间 默认:5分钟(单位:毫秒)

spring.redis.timeout=300000ms

#Redis连接池最大连接数(使用负值表示没有限制)

spring.redis.jedis.pool.max-active=512

#Redis连接池中的最小空闲连接

spring.redis.jedis.pool.min-idle=0

#Redis连接池中的最大空闲连接

spring.redis.jedis.pool.max-idle=8

#Redis连接池最大阻塞等待时间(使用负值表示没有限制)

spring.redis.jedis.pool.max-wait=-1ms

常见的需要注意的是最大连接数(spring.redis.jedis.pool.max-active )和超时时间(spring.redis.jedis.pool.max-wait)。Redis在生产环境中出现故障的频率经常和这两个参数息息相关。

接着定义一个继承自CachingConfigurerSupport(请注意cacheManager和keyGenerator这两个方法在子类的实现)的RedisConfig类:

package com.power.demo.cache.config;

import org.springframework.cache.CacheManager;

import org.springframework.cache.annotation.CachingConfigurerSupport;

import org.springframework.cache.annotation.EnableCaching;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.data.redis.cache.RedisCacheManager;

import org.springframework.data.redis.connection.RedisConnectionFactory;

import org.springframework.data.redis.core.RedisTemplate;

import org.springframework.data.redis.serializer.StringRedisSerializer;

/**

* Redis缓存配置类

*/

@Configuration

@EnableCaching

public class RedisConfig extends CachingConfigurerSupport {

@Bean

public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {

return RedisCacheManager.create(connectionFactory);

}

@Bean

public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {

RedisTemplate<String, Object> template = new RedisTemplate<>();

//Jedis的Key和Value的序列化器默认值是JdkSerializationRedisSerializer

//经实验,JdkSerializationRedisSerializer通过RedisDesktopManager看到的键值对不能正常解析

//设置key的序列化器

template.setKeySerializer(new StringRedisSerializer());

设置value的序列化器  默认值是JdkSerializationRedisSerializer

//使用Jackson序列化器的问题是,复杂对象可能序列化失败,比如JodaTime的DateTime类型

//        //使用Jackson2,将对象序列化为JSON

//        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

//        //json转对象类,不设置默认的会将json转成hashmap

//        ObjectMapper om = new ObjectMapper();

//        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);

//        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);

//        jackson2JsonRedisSerializer.setObjectMapper(om);

//        template.setValueSerializer(jackson2JsonRedisSerializer);

//将redis连接工厂设置到模板类中

template.setConnectionFactory(factory);

return template;

}

//    //自定义缓存key生成策略

//    @Bean

//    public KeyGenerator keyGenerator() {

//        return new KeyGenerator() {

//            @Override

//            public Object generate(Object target, java.lang.reflect.Method method, Object… params) {

//                StringBuffer sb = new StringBuffer();

//                sb.append(target.getClass().getName());

//                sb.append(method.getName());

//                for (Object obj : params) {

//                    if (obj == null) {

//                        continue;

//                    }

//                    sb.append(obj.toString());

//                }

//                return sb.toString();

//            }

//        };

//    }

}

在RedisConfig这个类上加上@EnableCaching这个注解,这个注解会被Spring发现,并且会创建一个切面(aspect) 并触发Spring缓存注解的切点(pointcut)。据所使用的注解以及缓存的状态,这个切面会从缓存中获取数据,将数据添加到缓存之中或者从缓存中移除某个值。

cacheManager方法,申明一个缓存管理器(CacheManager)的bean,作用就是@EnableCaching这个切面在新增缓存或者删除缓存的时候会调用这个缓存管理器的方法。keyGenerator方法,可以根据需求自定义缓存key生成策略。

而redisTemplate方法,则主要是设置Redis模板类,比如键和值的序列化器(从这里可以看出,Redis的键值对必须可序列化)、redis连接工厂等。

RedisTemplate支持的序列化器主要有如下几种:

**JdkSerializationRedisSerializer:**使用Java序列化;

**StringRedisSerializer:**序列化String类型的key和value;

**GenericToStringSerializer:**使用Spring转换服务进行序列化;

**JacksonJsonRedisSerializer:**使用Jackson 1,将对象序列化为JSON;

**Jackson2JsonRedisSerializer:**使用Jackson 2,将对象序列化为JSON;

**OxmSerializer:**使用Spring O/X映射的编排器和解排器(marshaler和unmarshaler)实现序列化,用于XML序列化;

_注意:_RedisTemplate的键和值序列化器,默认情况下都是JdkSerializationRedisSerializer,它们都可以自定义设置序列化器。

推荐将字符串键使用StringRedisSerializer序列化器,因为运维的时候好排查问题,JDK序列化器的也能识别,但是可读性稍差(是因为缓存服务器没有JRE吗?),见如下效果:

而值序列化器则要复杂的多,很多人推荐使用Jackson2JsonRedisSerializer序列化器,但是实际开发过程中,经常有人碰到反序列化错误,经过排查多数都和Jackson2JsonRedisSerializer这个序列化器有关。

4、实现接口

使用RedisTemplate,在Spring Boot中调用Redis接口比直接调用Jedis简单多了。

package com.power.demo.cache.impl;

import com.power.demo.cache.contract.CacheProviderService;

import com.power.demo.common.AppConst;

import org.springframework.beans.factory.annotation.Qualifier;

import org.springframework.context.annotation.ComponentScan;

import org.springframework.context.annotation.Configuration;

import org.springframework.data.redis.core.RedisTemplate;

import org.springframework.data.redis.core.ValueOperations;

import org.springframework.util.StringUtils;

import javax.annotation.Resource;

import java.io.Serializable;

import java.util.concurrent.TimeUnit;

import java.util.function.Function;

@Configuration

@ComponentScan(basePackages = AppConst.BASE_PACKAGE_NAME)

@Qualifier(“redisCacheService”)

public class RedisCacheProviderImpl implements CacheProviderService {

@Resource

private RedisTemplate<Serializable, Object> redisTemplate;

/**

* 查询缓存

* @param key 缓存键 不可为空

**/

public  T get(String key) {

T obj = get(key, null, null, AppConst.CACHE_MINUTE);

return obj;

}

/**

* 查询缓存

* @param key      缓存键 不可为空

* @param function 如没有缓存,调用该callable函数返回对象 可为空

**/

public  T get(String key, Function<String, T> function) {

T obj = get(key, function, key, AppConst.CACHE_MINUTE);

return obj;

}

/**

* 查询缓存

* @param key      缓存键 不可为空

* @param function 如没有缓存,调用该callable函数返回对象 可为空

* @param funcParm function函数的调用参数

**/

public <T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm) {

T obj = get(key, function, funcParm, AppConst.CACHE_MINUTE);

return obj;

}

/**

* 查询缓存

* @param key        缓存键 不可为空

* @param function   如没有缓存,调用该callable函数返回对象 可为空

* @param expireTime 过期时间(单位:毫秒) 可为空

**/

public  T get(String key, Function<String, T> function, Long expireTime) {

T obj = get(key, function, key, expireTime);

return obj;

}

/**

* 查询缓存

* @param key        缓存键 不可为空

* @param function   如没有缓存,调用该callable函数返回对象 可为空

* @param funcParm   function函数的调用参数

* @param expireTime 过期时间(单位:毫秒) 可为空

**/

public <T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm, Long expireTime) {

T obj = null;

if (StringUtils.isEmpty(key) == true) {

return obj;

}

expireTime = getExpireTime(expireTime);

try {

ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();

obj = (T) operations.get(key);

if (function != null && obj == null) {

obj = function.apply(funcParm);

if (obj != null) {

set(key, obj, expireTime);//设置缓存信息

}

}

} catch (Exception e) {

e.printStackTrace();

}

return obj;

}

/**

* 设置缓存键值  直接向缓存中插入值,这会直接覆盖掉给定键之前映射的值

* @param key 缓存键 不可为空

* @param obj 缓存值 不可为空

**/

public  void set(String key, T obj) {

set(key, obj, AppConst.CACHE_MINUTE);

}

/**

* 设置缓存键值  直接向缓存中插入值,这会直接覆盖掉给定键之前映射的值

* @param key        缓存键 不可为空

* @param obj        缓存值 不可为空

* @param expireTime 过期时间(单位:毫秒) 可为空

**/

public  void set(String key, T obj, Long expireTime) {

if (StringUtils.isEmpty(key) == true) {

return;

}

if (obj == null) {

return;

}

expireTime = getExpireTime(expireTime);

ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();

operations.set(key, obj);

redisTemplate.expire(key, expireTime, TimeUnit.MILLISECONDS);

}

/**

* 移除缓存

* @param key 缓存键 不可为空

**/

public void remove(String key) {

if (StringUtils.isEmpty(key) == true) {

return;

}

redisTemplate.delete(key);

}

/**

* 是否存在缓存

* @param key 缓存键 不可为空

**/

public boolean contains(String key) {

boolean exists = false;

if (StringUtils.isEmpty(key) == true) {

return exists;

}

Object obj = get(key);

if (obj != null) {

exists = true;

}

return exists;

}

/**

* 获取过期时间 单位:毫秒

* @param expireTime 传人的过期时间 单位毫秒 如小于1分钟,默认为10分钟

**/

private Long getExpireTime(Long expireTime) {

Long result = expireTime;

if (expireTime == null || expireTime < AppConst.CACHE_MINUTE / 10) {

result = AppConst.CACHE_MINUTE;

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

现在正是金三银四的春招高潮,前阵子小编一直在搭建自己的网站,并整理了全套的**【一线互联网大厂Java核心面试题库+解析】:包括Java基础、异常、集合、并发编程、JVM、Spring全家桶、MyBatis、Redis、数据库、中间件MQ、Dubbo、Linux、Tomcat、ZooKeeper、Netty等等**

image
《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!
y) {

boolean exists = false;

if (StringUtils.isEmpty(key) == true) {

return exists;

}

Object obj = get(key);

if (obj != null) {

exists = true;

}

return exists;

}

/**

* 获取过期时间 单位:毫秒

* @param expireTime 传人的过期时间 单位毫秒 如小于1分钟,默认为10分钟

**/

private Long getExpireTime(Long expireTime) {

Long result = expireTime;

if (expireTime == null || expireTime < AppConst.CACHE_MINUTE / 10) {

result = AppConst.CACHE_MINUTE;

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。[外链图片转存中…(img-d0P5Nx0K-1712515144606)]

[外链图片转存中…(img-j2zJDDXU-1712515144606)]

[外链图片转存中…(img-N5jsknKQ-1712515144607)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

现在正是金三银四的春招高潮,前阵子小编一直在搭建自己的网站,并整理了全套的**【一线互联网大厂Java核心面试题库+解析】:包括Java基础、异常、集合、并发编程、JVM、Spring全家桶、MyBatis、Redis、数据库、中间件MQ、Dubbo、Linux、Tomcat、ZooKeeper、Netty等等**

[外链图片转存中…(img-uaJ3yRcq-1712515144607)]
《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!

  • 24
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值