├── .gitignore ├── demo └── multi-layering-cache-demo │ ├── .gitignore │ ├── multi-layering-cache-demo.iml │ ├── pom.xml │ └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── github │ │ │ └── roger │ │ │ └── demo │ │ │ ├── MultiLayeringCacheDemoStarter.java │ │ │ ├── config │ │ │ └── RedisConfig.java │ │ │ ├── controller │ │ │ └── PersonController.java │ │ │ ├── entity │ │ │ └── Person.java │ │ │ ├── service │ │ │ ├── PersonService.java │ │ │ └── impl │ │ │ │ └── PersonServiceImpl.java │ │ │ └── utils │ │ │ └── OkHttpClientUtil.java │ └── resources │ │ ├── application-dev.properties │ │ ├── application-prd.properties │ │ ├── application.properties │ │ └── logback-spring.xml │ └── test │ └── java │ └── com │ └── github │ └── roger │ └── demo │ ├── MultiLayeringCacheDemoStarterTest.java │ ├── controller │ └── PersonControllerTest.java │ └── service │ └── impl │ └── PersonServiceImplTest.java ├── multi-layering-cache-aspecj ├── .gitignore ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── github │ │ │ └── roger │ │ │ ├── annotation │ │ │ ├── CacheEvict.java │ │ │ ├── CachePut.java │ │ │ ├── Cacheable.java │ │ │ ├── FirstCache.java │ │ │ └── SecondaryCache.java │ │ │ ├── aspect │ │ │ └── MultiLayeringCacheAspect.java │ │ │ ├── expression │ │ │ ├── CacheEvaluationContext.java │ │ │ ├── CacheExpressionRootObject.java │ │ │ ├── CacheOperationExpressionEvaluator.java │ │ │ └── VariableNotAvailableException.java │ │ │ ├── key │ │ │ ├── DefaultKey.java │ │ │ ├── KeyGenerator.java │ │ │ └── impl │ │ │ │ └── DefaultKeyGenerator.java │ │ │ ├── support │ │ │ └── CacheOperationInvoker.java │ │ │ └── utils │ │ │ └── CacheAspectUtil.java │ └── resources │ │ └── log4j.properties │ └── test │ ├── java │ └── com │ │ └── github │ │ └── roger │ │ ├── cahce │ │ ├── aspect │ │ │ └── CacheAspectTest.java │ │ ├── config │ │ │ ├── ICacheManagerConfig.java │ │ │ └── RedisConfig.java │ │ └── utils │ │ │ └── RedisCacheKeyTest.java │ │ ├── domain │ │ ├── Address.java │ │ └── User.java │ │ ├── key │ │ └── impl │ │ │ └── DefaultKeyGeneratorTest.java │ │ └── service │ │ └── impl │ │ └── UserServiceImpl.java │ └── resources │ ├── application.properties │ └── log4j.properties ├── multi-layering-cache-core ├── .gitignore ├── pom.xml └── src │ ├── main │ └── java │ │ └── org │ │ └── github │ │ └── roger │ │ ├── MultiLayeringCache.java │ │ ├── MultiLayeringCacheManager.java │ │ ├── cache │ │ ├── AbstractValueAdaptingCache.java │ │ ├── ICache.java │ │ ├── caffeine │ │ │ └── CaffeineCache.java │ │ └── redis │ │ │ └── RedisCache.java │ │ ├── concurrent │ │ ├── MdcThreadPoolTaskExecutor.java │ │ ├── RedisDistriLock.java │ │ └── ThreadTaskUtils.java │ │ ├── enumeration │ │ ├── ExpireMode.java │ │ ├── RedisPubSubMessageType.java │ │ └── Type.java │ │ ├── exception │ │ ├── NestedRuntimeException.java │ │ └── SerializationException.java │ │ ├── listener │ │ ├── RedisMessageListener.java │ │ └── RedisPublisher.java │ │ ├── manager │ │ ├── AbstractCacheManager.java │ │ └── ICacheManager.java │ │ ├── message │ │ └── RedisPubSubMessage.java │ │ ├── serializer │ │ ├── FastJsonRedisSerializer.java │ │ ├── FastJsonSerializerWrapper.java │ │ ├── KryoRedisSerializer.java │ │ └── StringRedisSerializer.java │ │ ├── settings │ │ ├── FirstCacheSetting.java │ │ ├── MultiLayeringCacheSetting.java │ │ └── SecondaryCacheSetting.java │ │ ├── support │ │ ├── AwaitThreadContainer.java │ │ └── NullValue.java │ │ └── utils │ │ ├── RedisCacheKey.java │ │ └── SerializationUtils.java │ └── test │ ├── java │ └── org │ │ └── github │ │ └── roger │ │ ├── cache │ │ ├── config │ │ │ ├── ICacheManagerConfig.java │ │ │ └── RedisConfig.java │ │ └── core │ │ │ └── CacheCoreTest.java │ │ └── utils │ │ └── RedisCacheKeyTest.java │ └── resources │ ├── application.properties │ └── log4j.properties ├── multi-layering-cache-starter ├── .gitignore ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── github │ │ └── roger │ │ └── cache │ │ ├── config │ │ └── MultiLayeringCacheAutoConfig.java │ │ └── properties │ │ └── MultiLayeringCacheProperties.java │ └── resources │ └── META-INF │ └── spring.factories ├── multi-layering-cache.iml └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | /target/ -------------------------------------------------------------------------------- /demo/multi-layering-cache-demo/.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | /target/ -------------------------------------------------------------------------------- /demo/multi-layering-cache-demo/multi-layering-cache-demo.iml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /demo/multi-layering-cache-demo/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.github.roger 8 | multi-layering-cache-demo 9 | 1.0-SNAPSHOT 10 | 11 | 12 | org.springframework.boot 13 | spring-boot-starter-parent 14 | 2.1.0.RELEASE 15 | 16 | 17 | 18 | 19 | 20 | UTF-8 21 | UTF-8 22 | 1.8 23 | 24 | 25 | 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-web 30 | 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-starter-test 35 | 36 | 37 | 38 | org.springframework.boot 39 | spring-boot-starter-logging 40 | 41 | 42 | 43 | com.fasterxml.jackson.core 44 | jackson-databind 45 | 2.9.7 46 | 47 | 48 | 49 | com.github.roger 50 | multi-layering-cache-starter 51 | 1.0-SNAPSHOT 52 | 53 | 54 | slf4j-log4j12 55 | org.slf4j 56 | 57 | 58 | 59 | 60 | 61 | com.squareup.okhttp3 62 | okhttp 63 | 3.6.0 64 | 65 | 66 | 67 | org.projectlombok 68 | lombok 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /demo/multi-layering-cache-demo/src/main/java/com/github/roger/demo/MultiLayeringCacheDemoStarter.java: -------------------------------------------------------------------------------- 1 | package com.github.roger.demo; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class MultiLayeringCacheDemoStarter { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(MultiLayeringCacheDemoStarter.class,args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /demo/multi-layering-cache-demo/src/main/java/com/github/roger/demo/config/RedisConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.roger.demo.config; 2 | 3 | import com.alibaba.fastjson.parser.ParserConfig; 4 | import com.fasterxml.jackson.annotation.JsonAutoDetect; 5 | import com.fasterxml.jackson.annotation.PropertyAccessor; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import lombok.extern.log4j.Log4j; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.github.roger.serializer.FastJsonRedisSerializer; 10 | import org.github.roger.serializer.KryoRedisSerializer; 11 | import org.github.roger.serializer.StringRedisSerializer; 12 | import org.springframework.context.annotation.Bean; 13 | import org.springframework.context.annotation.Configuration; 14 | import org.springframework.data.redis.connection.RedisConnectionFactory; 15 | import org.springframework.data.redis.core.RedisTemplate; 16 | import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; 17 | 18 | /** 19 | * @author yuhao.wang 20 | */ 21 | @Configuration 22 | @Slf4j 23 | public class RedisConfig { 24 | 25 | /** 26 | * 重写Redis序列化方式,使用Json方式: 27 | * 当我们的数据存储到Redis的时候,我们的键(key)和值(value)都是通过Spring提供的Serializer序列化到数据库的。RedisTemplate默认使用的是JdkSerializationRedisSerializer,StringRedisTemplate默认使用的是StringRedisSerializer。 28 | * Spring Data JPA为我们提供了下面的Serializer: 29 | * GenericToStringSerializer、Jackson2JsonRedisSerializer、JacksonJsonRedisSerializer、JdkSerializationRedisSerializer、OxmSerializer、StringRedisSerializer。 30 | * 在此我们将自己配置RedisTemplate并定义Serializer。 31 | * 32 | * @param redisConnectionFactory 33 | * @return 34 | */ 35 | @Bean 36 | public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { 37 | RedisTemplate redisTemplate = new RedisTemplate<>(); 38 | redisTemplate.setConnectionFactory(redisConnectionFactory); 39 | 40 | Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); 41 | ObjectMapper om = new ObjectMapper(); 42 | om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); 43 | om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); 44 | jackson2JsonRedisSerializer.setObjectMapper(om); 45 | 46 | KryoRedisSerializer kryoRedisSerializer = new KryoRedisSerializer(Object.class); 47 | 48 | FastJsonRedisSerializer fastJsonRedisSerializer = 49 | new FastJsonRedisSerializer(Object.class,"com.github.roger."); 50 | 51 | // 设置值(value)的序列化采用 kryoRedisSerializer。 52 | redisTemplate.setValueSerializer(fastJsonRedisSerializer); 53 | redisTemplate.setHashValueSerializer(fastJsonRedisSerializer); 54 | // 设置键(key)的序列化采用StringRedisSerializer。 55 | redisTemplate.setKeySerializer(new StringRedisSerializer()); 56 | redisTemplate.setHashKeySerializer(new StringRedisSerializer()); 57 | 58 | redisTemplate.afterPropertiesSet(); 59 | log.info("redisTemplate客户端初始化完毕"); 60 | return redisTemplate; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /demo/multi-layering-cache-demo/src/main/java/com/github/roger/demo/controller/PersonController.java: -------------------------------------------------------------------------------- 1 | package com.github.roger.demo.controller; 2 | 3 | import com.github.roger.demo.entity.Person; 4 | import com.github.roger.demo.service.PersonService; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.web.bind.annotation.RequestBody; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | @RestController 11 | public class PersonController { 12 | @Autowired 13 | PersonService personService; 14 | 15 | @RequestMapping("/put") 16 | public long put(@RequestBody Person person) { 17 | Person p = personService.save(person); 18 | return p.getId(); 19 | } 20 | 21 | @RequestMapping("/able") 22 | public Person cacheable(@RequestBody Person person) { 23 | 24 | return personService.findOne(person); 25 | } 26 | 27 | @RequestMapping("/evit") 28 | public String evit(@RequestBody Person person) { 29 | 30 | personService.remove(person.getId()); 31 | return "ok"; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /demo/multi-layering-cache-demo/src/main/java/com/github/roger/demo/entity/Person.java: -------------------------------------------------------------------------------- 1 | package com.github.roger.demo.entity; 2 | 3 | import lombok.*; 4 | 5 | @Data 6 | @NoArgsConstructor 7 | @RequiredArgsConstructor 8 | @AllArgsConstructor 9 | @ToString 10 | public class Person { 11 | 12 | private long id; 13 | 14 | @NonNull 15 | private String name; 16 | @NonNull 17 | private Integer age; 18 | 19 | private String address; 20 | } 21 | -------------------------------------------------------------------------------- /demo/multi-layering-cache-demo/src/main/java/com/github/roger/demo/service/PersonService.java: -------------------------------------------------------------------------------- 1 | package com.github.roger.demo.service; 2 | 3 | import com.github.roger.demo.entity.Person; 4 | 5 | public interface PersonService { 6 | 7 | Person save(Person person); 8 | 9 | void remove(Long id); 10 | 11 | void removeAll(); 12 | 13 | Person findOne(Person person); 14 | } 15 | -------------------------------------------------------------------------------- /demo/multi-layering-cache-demo/src/main/java/com/github/roger/demo/service/impl/PersonServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.roger.demo.service.impl; 2 | 3 | import com.github.roger.annotation.CacheEvict; 4 | import com.github.roger.annotation.Cacheable; 5 | import com.github.roger.annotation.FirstCache; 6 | import com.github.roger.annotation.SecondaryCache; 7 | import com.github.roger.demo.entity.Person; 8 | import com.github.roger.demo.service.PersonService; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.stereotype.Service; 11 | 12 | @Service 13 | @Slf4j 14 | public class PersonServiceImpl implements PersonService { 15 | 16 | @Cacheable(value = "people" , key = "#person.id") 17 | @Override 18 | public Person save(Person person) { 19 | log.info("为id、key为:" + person.getId() + "数据做了缓存"); 20 | return person; 21 | } 22 | 23 | @CacheEvict(value = "people", key = "#id") 24 | @Override 25 | public void remove(Long id) { 26 | log.info("删除了id、key为" + id + "的数据缓存"); 27 | } 28 | 29 | @CacheEvict(value = "people", allEntries = true) 30 | @Override 31 | public void removeAll() { 32 | log.info("删除了所有缓存的数据缓存"); 33 | } 34 | 35 | @Cacheable(value = "people", key = "#person.id", 36 | firstCache = @FirstCache(expireTime = 4), 37 | secondaryCache = @SecondaryCache(expireTime = 5, preloadTime = 1, forceRefresh = true)) 38 | @Override 39 | public Person findOne(Person person) { 40 | Person p = new Person(2L, "name2", 12,"address2"); 41 | log.info("为id、key为:" + p.getId() + "数据做了缓存"); 42 | return p; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /demo/multi-layering-cache-demo/src/main/java/com/github/roger/demo/utils/OkHttpClientUtil.java: -------------------------------------------------------------------------------- 1 | package com.github.roger.demo.utils; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import lombok.extern.slf4j.Slf4j; 5 | import okhttp3.*; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.remoting.RemoteAccessException; 9 | 10 | import java.io.IOException; 11 | import java.util.Map; 12 | import java.util.concurrent.TimeUnit; 13 | 14 | /** 15 | * OkHttpClient工具 16 | * 17 | * @author yuhao.wang3 18 | */ 19 | @Slf4j 20 | public abstract class OkHttpClientUtil { 21 | 22 | private static OkHttpClient okHttpClient = new OkHttpClient.Builder() 23 | .connectTimeout(10, TimeUnit.SECONDS) 24 | .writeTimeout(10, TimeUnit.SECONDS) 25 | .readTimeout(20, TimeUnit.SECONDS) 26 | .build(); 27 | 28 | 29 | /** 30 | * 发起post请求,不做任何签名 31 | * 32 | * @param url 发送请求的URL 33 | * @param requestBody 请求体 34 | * @throws IOException 35 | */ 36 | public static String post(String url, RequestBody requestBody) throws IOException { 37 | Request request = new Request.Builder() 38 | //请求的url 39 | .url(url) 40 | .post(requestBody) 41 | .build(); 42 | 43 | //创建/Call 44 | Response response = okHttpClient.newCall(request).execute(); 45 | if (!response.isSuccessful()) { 46 | log.error("访问外部系统异常 {}: {}", url, JSON.toJSONString(response)); 47 | throw new RemoteAccessException("访问外部系统异常 " + url); 48 | } 49 | return response.body().string(); 50 | } 51 | 52 | /** 53 | * 发起post请求,不做任何签名 宽松的参数构造 54 | * 55 | * @param url 发送请求的URL 56 | * @param builder 请求体 57 | * @throws IOException 58 | */ 59 | public static String post(String url, Request.Builder builder) throws IOException { 60 | 61 | Request request = builder.url(url).build(); 62 | //创建/Call 63 | Response response = okHttpClient.newCall(request).execute(); 64 | if (!response.isSuccessful()) { 65 | log.error("访问外部系统异常 {}: {}", url, JSON.toJSONString(response)); 66 | throw new RemoteAccessException("访问外部系统异常 " + url); 67 | } 68 | return response.body().string(); 69 | } 70 | 71 | 72 | public static String post(String url, Map param, Map header) throws Exception { 73 | // 生成requestBody 74 | RequestBody requestBody = FormBody.create(MediaType.parse("application/json; charset=utf-8") 75 | , JSON.toJSONString(param)); 76 | 77 | Request.Builder builder = new Request.Builder() 78 | //请求的url 79 | .url(url) 80 | .post(requestBody); 81 | 82 | for (String key : header.keySet()) { 83 | builder.header(key, header.get(key)); 84 | } 85 | 86 | Request request = builder.build(); 87 | 88 | //创建/Call 89 | Response response = okHttpClient.newCall(request).execute(); 90 | if (!response.isSuccessful()) { 91 | log.error("访问外部系统异常 {}: {}", url, JSON.toJSONString(response)); 92 | throw new RemoteAccessException("访问外部系统异常 " + url); 93 | } 94 | return response.body().string(); 95 | } 96 | 97 | public static String get(String url) throws IOException { 98 | Request request = new Request.Builder() 99 | //请求的url 100 | .url(url) 101 | .get() 102 | .build(); 103 | 104 | Response response = okHttpClient.newCall(request).execute(); 105 | if (!response.isSuccessful()) { 106 | log.error("访问外部系统异常 {}: {}", url, JSON.toJSONString(response)); 107 | throw new RemoteAccessException("访问外部系统异常 " + url); 108 | } 109 | return response.body().string(); 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /demo/multi-layering-cache-demo/src/main/resources/application-dev.properties: -------------------------------------------------------------------------------- 1 | server.port=8081 2 | 3 | #redis配置 4 | #redis 5 | #database name 6 | spring.redis.database=0 7 | #server host 8 | spring.redis.host=127.0.0.1 9 | #server password 10 | spring.redis.password= 11 | #connection port 12 | spring.redis.port=6379 -------------------------------------------------------------------------------- /demo/multi-layering-cache-demo/src/main/resources/application-prd.properties: -------------------------------------------------------------------------------- 1 | server.port=8082 2 | 3 | #redis配置 4 | #redis 5 | #database name 6 | spring.redis.database=0 7 | #server host 8 | spring.redis.host=127.0.0.1 9 | #server password 10 | spring.redis.password= 11 | #connection port 12 | spring.redis.port=6379 13 | 14 | spring.multi-layering-cache.namespace=mutli-layering-cahce -------------------------------------------------------------------------------- /demo/multi-layering-cache-demo/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.profiles.active=dev 2 | 3 | -------------------------------------------------------------------------------- /demo/multi-layering-cache-demo/src/main/resources/logback-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ${CONSOLE_LOG_PATTERN} 34 | utf8 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | INFO 43 | ACCEPT 44 | DENY 45 | 46 | 47 | ${FILE_LOG_PATTERN} 48 | 49 | 50 | 51 | 52 | ${LOG_HOME}/info.%d{yyyy-MM-dd}.log 53 | 54 | 55 | 30 56 | 57 | 58 | 59 | 60 | 10MB 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | WARN 69 | ACCEPT 70 | DENY 71 | 72 | 73 | ${FILE_LOG_PATTERN} 74 | 75 | 76 | ${LOG_HOME}/warn.%d{yyyy-MM-dd}.log 77 | 30 78 | 79 | 80 | 81 | 82 | 83 | 84 | ERROR 85 | ACCEPT 86 | DENY 87 | 88 | 89 | ${FILE_LOG_PATTERN} 90 | 91 | 92 | ${LOG_HOME}/error.%d{yyyy-MM-dd}.log 93 | 30 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /demo/multi-layering-cache-demo/src/test/java/com/github/roger/demo/MultiLayeringCacheDemoStarterTest.java: -------------------------------------------------------------------------------- 1 | package com.github.roger.demo; 2 | 3 | import org.junit.runner.RunWith; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | import org.springframework.test.context.junit4.SpringRunner; 6 | 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest 10 | public class MultiLayeringCacheDemoStarterTest { 11 | 12 | } -------------------------------------------------------------------------------- /demo/multi-layering-cache-demo/src/test/java/com/github/roger/demo/controller/PersonControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.github.roger.demo.controller; 2 | 3 | 4 | import com.alibaba.fastjson.JSON; 5 | import com.github.roger.demo.MultiLayeringCacheDemoStarterTest; 6 | import com.github.roger.demo.entity.Person; 7 | import com.github.roger.demo.utils.OkHttpClientUtil; 8 | import lombok.extern.slf4j.Slf4j; 9 | import okhttp3.FormBody; 10 | import okhttp3.MediaType; 11 | import okhttp3.RequestBody; 12 | import org.junit.Test; 13 | 14 | import java.io.IOException; 15 | 16 | @Slf4j 17 | public class PersonControllerTest extends MultiLayeringCacheDemoStarterTest { 18 | 19 | String host = "http://127.0.0.1:8081/"; 20 | 21 | @Test 22 | public void testPut() throws IOException { 23 | Person person = new Person(1, "name1", 12, "address1"); 24 | 25 | RequestBody requestBody = FormBody.create(MediaType.parse("application/json; charset=utf-8"), 26 | JSON.toJSONString(person)); 27 | 28 | String post = OkHttpClientUtil.post(host + "put",requestBody); 29 | 30 | log.info("返回结果 result = " + post); 31 | } 32 | } -------------------------------------------------------------------------------- /demo/multi-layering-cache-demo/src/test/java/com/github/roger/demo/service/impl/PersonServiceImplTest.java: -------------------------------------------------------------------------------- 1 | package com.github.roger.demo.service.impl; 2 | 3 | import com.github.roger.demo.MultiLayeringCacheDemoStarterTest; 4 | import com.github.roger.demo.entity.Person; 5 | import com.github.roger.demo.service.PersonService; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.junit.Assert; 8 | import org.junit.Test; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | @Slf4j 14 | public class PersonServiceImplTest extends MultiLayeringCacheDemoStarterTest { 15 | 16 | @Autowired 17 | private PersonService personService; 18 | 19 | @Test 20 | public void testSave() { 21 | Person p = new Person(1, "name1", 12, "address1"); 22 | //保存之后,写入缓存 23 | personService.save(p); 24 | //从缓存中抓取数据,一次虽然真正的方法返回结果的id为2 ,实际得到的结果 25 | //id为1 26 | Person person = personService.findOne(p); 27 | Assert.assertEquals(person.getId(), 1); 28 | } 29 | 30 | @Test 31 | public void testRemove() { 32 | Person p = new Person(5, "name1", 12, "address1"); 33 | personService.save(p); 34 | log.info("缓存中一个缓存 id = 5 的 Person 对象"); 35 | 36 | personService.remove(5L); 37 | log.info("删除一个缓存 id = 5 的 Person 对象"); 38 | 39 | Person person = personService.findOne(p); 40 | log.info("从数据库中抓取一个 id = 2 的对象,并存入缓存"); 41 | Assert.assertEquals(person.getId(), 2); 42 | } 43 | } -------------------------------------------------------------------------------- /multi-layering-cache-aspecj/.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | /target/ -------------------------------------------------------------------------------- /multi-layering-cache-aspecj/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | multi-layering-cache 7 | com.github.roger 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | multi-layering-cache-aspecj 13 | multi-layering-cache-aspecj 14 | 多级缓存注解模块 15 | jar 16 | 17 | 18 | 19 | 20 | com.github.roger 21 | multi-layering-cache-core 22 | 1.0-SNAPSHOT 23 | 24 | 25 | 26 | org.aspectj 27 | aspectjweaver 28 | 29 | 30 | 31 | org.springframework 32 | spring-aop 33 | 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-starter-test 38 | test 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /multi-layering-cache-aspecj/src/main/java/com/github/roger/annotation/CacheEvict.java: -------------------------------------------------------------------------------- 1 | package com.github.roger.annotation; 2 | 3 | import org.springframework.core.annotation.AliasFor; 4 | 5 | import java.lang.annotation.*; 6 | 7 | @Documented 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Inherited 10 | @Target({ElementType.METHOD}) 11 | public @interface CacheEvict { 12 | /** 13 | * 别名是 {@link #cacheNames}. 14 | * 15 | * @return String[] 16 | */ 17 | @AliasFor("cacheNames") 18 | String[] value() default {}; 19 | 20 | /** 21 | * 缓存名称,支持SpEL表达式 22 | * 23 | * @return String[] 24 | */ 25 | @AliasFor("value") 26 | String[] cacheNames() default {}; 27 | 28 | /** 29 | * 缓存key,支持SpEL表达式 30 | * 31 | * @return String 32 | */ 33 | String key() default ""; 34 | 35 | 36 | /** 37 | * 是否忽略在操作缓存中遇到的异常,如反序列化异常,默认true。 38 | *

true: 有异常会输出warn级别的日志,并直接执行被缓存的方法(缓存将失效)

39 | *

false:有异常会输出error级别的日志,并抛出异常

40 | * 41 | * @return boolean 42 | */ 43 | boolean ignoreException() default true; 44 | 45 | /** 46 | * 是否删除缓存中所有数据 47 | *

默认情况下是只删除关联key的缓存数据 48 | *

注意:当该参数设置成 {@code true} 时 {@link #key} 参数将无效 49 | * 50 | * @return boolean 51 | */ 52 | boolean allEntries() default false; 53 | } 54 | -------------------------------------------------------------------------------- /multi-layering-cache-aspecj/src/main/java/com/github/roger/annotation/CachePut.java: -------------------------------------------------------------------------------- 1 | package com.github.roger.annotation; 2 | 3 | import org.springframework.core.annotation.AliasFor; 4 | 5 | import java.lang.annotation.*; 6 | 7 | @Documented 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Inherited 10 | @Target({ElementType.METHOD}) 11 | public @interface CachePut { 12 | /** 13 | * 别名是 {@link #cacheNames}. 14 | * 15 | * @return String[] 16 | */ 17 | @AliasFor("cacheNames") 18 | String[] value() default {}; 19 | 20 | /** 21 | * 缓存名称,支持SpEL表达式 22 | * 23 | * @return String[] 24 | */ 25 | @AliasFor("value") 26 | String[] cacheNames() default {}; 27 | 28 | /** 29 | * 缓存key,支持SpEL表达式 30 | * 31 | * @return String 32 | */ 33 | String key() default ""; 34 | 35 | 36 | /** 37 | * 是否忽略在操作缓存中遇到的异常,如反序列化异常,默认true。 38 | *

true: 有异常会输出warn级别的日志,并直接执行被缓存的方法(缓存将失效)

39 | *

false:有异常会输出error级别的日志,并抛出异常

40 | * 41 | * @return boolean 42 | */ 43 | boolean ignoreException() default true; 44 | 45 | /** 46 | * 一级缓存配置 47 | * 48 | * @return FirstCache 49 | */ 50 | FirstCache firstCache() default @FirstCache(); 51 | 52 | /** 53 | * 二级缓存配置 54 | * 55 | * @return SecondaryCache 56 | */ 57 | SecondaryCache secondaryCache() default @SecondaryCache(); 58 | } 59 | -------------------------------------------------------------------------------- /multi-layering-cache-aspecj/src/main/java/com/github/roger/annotation/Cacheable.java: -------------------------------------------------------------------------------- 1 | package com.github.roger.annotation; 2 | 3 | import org.springframework.core.annotation.AliasFor; 4 | 5 | import java.lang.annotation.*; 6 | 7 | /** 8 | * 表示调用的方法(或类中的所有方法)的结果是可以被缓存的。 9 | * 当该方法被调用时先检查缓存是否命中,如果没有命中再调用被缓存的方法,并将其返回值放到缓存中。 10 | * 这里的value和key都支持SpEL 表达式 11 | */ 12 | @Documented 13 | @Retention(RetentionPolicy.RUNTIME) 14 | @Inherited 15 | @Target({ElementType.METHOD,ElementType.TYPE}) 16 | public @interface Cacheable { 17 | 18 | /** 19 | * 别名是 {@link #cacheNames}. 20 | * 21 | * @return String[] 22 | */ 23 | @AliasFor("cacheNames") 24 | String[] value() default {}; 25 | 26 | /** 27 | * 缓存名称,支持SpEL表达式 28 | * 29 | * @return String[] 30 | */ 31 | @AliasFor("value") 32 | String[] cacheNames() default {}; 33 | 34 | /** 35 | * 缓存key,支持SpEL表达式 36 | * 37 | * @return String 38 | */ 39 | String key() default ""; 40 | 41 | 42 | /** 43 | * 是否忽略在操作缓存中遇到的异常,如反序列化异常,默认true。 44 | *

true: 有异常会输出warn级别的日志,并直接执行被缓存的方法(缓存将失效)

45 | *

false:有异常会输出error级别的日志,并抛出异常

46 | * 47 | * @return boolean 48 | */ 49 | boolean ignoreException() default true; 50 | 51 | /** 52 | * 一级缓存配置 53 | * 54 | * @return FirstCache 55 | */ 56 | FirstCache firstCache() default @FirstCache(); 57 | 58 | /** 59 | * 二级缓存配置 60 | * 61 | * @return SecondaryCache 62 | */ 63 | SecondaryCache secondaryCache() default @SecondaryCache(); 64 | } 65 | -------------------------------------------------------------------------------- /multi-layering-cache-aspecj/src/main/java/com/github/roger/annotation/FirstCache.java: -------------------------------------------------------------------------------- 1 | package com.github.roger.annotation; 2 | 3 | import org.github.roger.enumeration.ExpireMode; 4 | 5 | import java.lang.annotation.*; 6 | import java.util.concurrent.TimeUnit; 7 | 8 | /** 9 | 一级缓存配置项 10 | */ 11 | @Documented 12 | @Retention(RetentionPolicy.RUNTIME) 13 | @Inherited 14 | @Target({ElementType.METHOD,ElementType.TYPE}) 15 | public @interface FirstCache { 16 | 17 | /** 18 | * 缓存初始Size 19 | */ 20 | int initialCapacity() default 10; 21 | 22 | /** 23 | * 缓存最大Size 24 | */ 25 | int maximumSize() default 5000; 26 | 27 | /** 28 | * 缓存有效时间 29 | */ 30 | int expireTime() default 9; 31 | 32 | /** 33 | * 缓存时间单位 34 | */ 35 | TimeUnit timeUnit() default TimeUnit.MINUTES; 36 | 37 | /** 38 | * 缓存失效模式{@link ExpireMode} 39 | */ 40 | ExpireMode expireMode() default ExpireMode.WRITE; 41 | } 42 | -------------------------------------------------------------------------------- /multi-layering-cache-aspecj/src/main/java/com/github/roger/annotation/SecondaryCache.java: -------------------------------------------------------------------------------- 1 | package com.github.roger.annotation; 2 | 3 | import java.lang.annotation.*; 4 | import java.util.concurrent.TimeUnit; 5 | 6 | /** 7 | 二级缓存配置项 8 | */ 9 | @Documented 10 | @Retention(RetentionPolicy.RUNTIME) 11 | @Inherited 12 | @Target({ElementType.METHOD,ElementType.TYPE}) 13 | public @interface SecondaryCache { 14 | 15 | /** 16 | * 缓存有效时间 17 | * 不能设置成0 因为设置成0 后,如果注解中不配置该项属性 18 | * 则redis保存时有误 19 | */ 20 | long expireTime() default 5; 21 | 22 | /** 23 | * 缓存主动在失效前强制刷新缓存的时间 24 | * 建议是: preloadTime default expireTime * 0.2 25 | * 26 | * @return long 27 | */ 28 | long preloadTime() default 1; 29 | 30 | /** 31 | * 时间单位 {@link TimeUnit} 32 | */ 33 | TimeUnit timeUnit() default TimeUnit.HOURS; 34 | 35 | /** 36 | * 是否强制刷新(走数据库),默认是false 37 | */ 38 | boolean forceRefresh() default false; 39 | 40 | 41 | boolean isUsePrefix() default true; 42 | 43 | /** 44 | * 是否允许存NULL值 45 | */ 46 | boolean isAllowNullValue() default false; 47 | 48 | /** 49 | * 非空值和null值之间的时间倍率,默认是1。allowNullValuedefaulttrue才有效 50 | * 51 | * 如配置缓存的有效时间是200秒,倍率这设置成10, 52 | * 那么当缓存value为null时,缓存的有效时间将是20秒,非空时为200秒 53 | */ 54 | int magnification() default 1; 55 | } 56 | 57 | -------------------------------------------------------------------------------- /multi-layering-cache-aspecj/src/main/java/com/github/roger/aspect/MultiLayeringCacheAspect.java: -------------------------------------------------------------------------------- 1 | package com.github.roger.aspect; 2 | 3 | import com.github.roger.annotation.CacheEvict; 4 | import com.github.roger.annotation.CachePut; 5 | import com.github.roger.annotation.Cacheable; 6 | import com.github.roger.expression.CacheOperationExpressionEvaluator; 7 | import com.github.roger.key.KeyGenerator; 8 | import com.github.roger.key.impl.DefaultKeyGenerator; 9 | import com.github.roger.support.CacheOperationInvoker; 10 | import com.github.roger.utils.CacheAspectUtil; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.aspectj.lang.ProceedingJoinPoint; 13 | import org.aspectj.lang.annotation.Around; 14 | import org.aspectj.lang.annotation.Aspect; 15 | import org.aspectj.lang.annotation.Pointcut; 16 | import org.github.roger.cache.ICache; 17 | import org.github.roger.exception.SerializationException; 18 | import org.github.roger.manager.ICacheManager; 19 | import org.github.roger.settings.FirstCacheSetting; 20 | import org.github.roger.settings.MultiLayeringCacheSetting; 21 | import org.github.roger.settings.SecondaryCacheSetting; 22 | import org.springframework.beans.factory.annotation.Autowired; 23 | import org.springframework.core.annotation.AnnotationUtils; 24 | import org.springframework.util.Assert; 25 | import org.springframework.util.CollectionUtils; 26 | 27 | import java.lang.reflect.Method; 28 | import java.util.Collection; 29 | 30 | @Aspect 31 | @Slf4j 32 | public class MultiLayeringCacheAspect { 33 | 34 | private static final String CACHE_KEY_ERROR_MESSAGE = "缓存Key %s 不能为NULL"; 35 | private static final String CACHE_NAME_ERROR_MESSAGE = "缓存名称不能为NULL"; 36 | 37 | @Autowired(required = false)//如果自定义了使用自定义的,否则使用默认的 38 | private KeyGenerator keyGenerator = new DefaultKeyGenerator(); 39 | 40 | @Autowired 41 | private ICacheManager iCacheManager; 42 | 43 | @Pointcut("@annotation(com.github.roger.annotation.Cacheable)") 44 | public void cacheablePointCut(){ 45 | } 46 | 47 | @Pointcut("@annotation(com.github.roger.annotation.CachePut)") 48 | public void cachePutPointCut(){ 49 | } 50 | 51 | @Pointcut("@annotation(com.github.roger.annotation.CacheEvict)") 52 | public void cacheEvictPointCut(){ 53 | } 54 | 55 | @Around("cacheablePointCut()") 56 | public Object cacheableAroundAdvice(ProceedingJoinPoint pJoinPoint) throws Throwable{ 57 | //通过非缓存的方式获取数据的操作类接口 58 | CacheOperationInvoker aopInvoker = CacheAspectUtil.getCacheOpreationInvoker(pJoinPoint); 59 | 60 | //获取正在执行的目标方法 61 | Method method = CacheAspectUtil.getSpecificMethod(pJoinPoint); 62 | 63 | //获取方法上的Cacheable注解 64 | Cacheable cacheable = AnnotationUtils.findAnnotation(method,Cacheable.class); 65 | try { 66 | //执行查询缓存的方法 67 | return executeCachealbe(aopInvoker, method, cacheable, pJoinPoint); 68 | }catch (SerializationException sex){ 69 | // 如果是序列化异常需要先删除原有缓存 70 | String[] cacheNames = cacheable.cacheNames(); 71 | // 删除缓存 72 | delete(cacheNames, cacheable.key(), method, pJoinPoint); 73 | 74 | // 忽略操作缓存过程中遇到的异常 75 | if (cacheable.ignoreException()) { 76 | log.warn(sex.getMessage(), sex); 77 | return aopInvoker.invoke(); 78 | } 79 | throw sex; 80 | }catch (Exception ex){ 81 | // 忽略操作缓存过程中遇到的异常 82 | if (cacheable.ignoreException()) { 83 | log.warn(ex.getMessage(), ex); 84 | return aopInvoker.invoke(); 85 | } 86 | throw ex; 87 | } 88 | } 89 | 90 | private Object executeCachealbe(CacheOperationInvoker aopInvoker, Method method, Cacheable cacheable, ProceedingJoinPoint pJoinPoint) { 91 | // 解析SpEL表达式获取cacheName和key 92 | String[] cacheNames = cacheable.cacheNames(); 93 | Assert.notEmpty(cacheable.cacheNames(), CACHE_NAME_ERROR_MESSAGE); 94 | String cacheName = cacheNames[0]; 95 | 96 | Object key = CacheAspectUtil.generateKey(keyGenerator,cacheable.key(), method, pJoinPoint); 97 | Assert.notNull(key, String.format(CACHE_KEY_ERROR_MESSAGE, cacheable.key())); 98 | 99 | // 构造多级缓存配置信息 100 | MultiLayeringCacheSetting layeringCacheSetting = CacheAspectUtil.generateMultiLayeringCacheSetting(cacheable.firstCache(),cacheable.secondaryCache()); 101 | 102 | // 通过cacheName和缓存配置获取Cache 103 | ICache iCache = iCacheManager.getCache(cacheName, layeringCacheSetting); 104 | 105 | // 通Cache获取值 106 | return iCache.get(key, () -> aopInvoker.invoke()); 107 | 108 | } 109 | 110 | @Around("cacheEvictPointCut()") 111 | public Object cacheEvicArountAdvice(ProceedingJoinPoint pJoinPoint) throws Throwable{ 112 | //通过非缓存的方式获取数据的操作类接口 113 | CacheOperationInvoker aopInvoker = CacheAspectUtil.getCacheOpreationInvoker(pJoinPoint); 114 | 115 | //获取正在执行的目标方法 116 | Method method = CacheAspectUtil.getSpecificMethod(pJoinPoint); 117 | 118 | //获取方法上的Cacheable注解 119 | CacheEvict cacheEvict = AnnotationUtils.findAnnotation(method,CacheEvict.class); 120 | try{ 121 | return executeCacheEvict(aopInvoker,method,cacheEvict,pJoinPoint); 122 | }catch (Exception ex){ 123 | // 忽略操作缓存过程中遇到的异常 124 | if (cacheEvict.ignoreException()) { 125 | log.warn(ex.getMessage(), ex); 126 | return aopInvoker.invoke(); 127 | } 128 | throw ex; 129 | } 130 | } 131 | 132 | private Object executeCacheEvict(CacheOperationInvoker aopInvoker, Method method, CacheEvict cacheEvict, ProceedingJoinPoint pJoinPoint) throws Throwable { 133 | // 解析SpEL表达式获取cacheName和key 134 | String[] cacheNames = cacheEvict.cacheNames(); 135 | Assert.notEmpty(cacheEvict.cacheNames(), CACHE_NAME_ERROR_MESSAGE); 136 | // 判断是否删除所有缓存数据 137 | if(cacheEvict.allEntries()){ 138 | // 删除所有缓存数据(清空) 139 | for (String cacheName : cacheNames) { 140 | Collection iCaches = iCacheManager.getCache(cacheName); 141 | if (CollectionUtils.isEmpty(iCaches)) { 142 | // 如果没有找到Cache就新建一个默认的 143 | ICache iCache = iCacheManager.getCache(cacheName, 144 | new MultiLayeringCacheSetting(new FirstCacheSetting(), new SecondaryCacheSetting())); 145 | iCache.clear(); 146 | } else { 147 | for (ICache iCache : iCaches) { 148 | iCache.clear(); 149 | } 150 | } 151 | } 152 | }else{ 153 | delete(cacheNames,cacheEvict.key(),method,pJoinPoint); 154 | } 155 | return aopInvoker.invoke(); 156 | } 157 | 158 | 159 | /** 160 | * 删除执行缓存名称上的指定key 161 | * */ 162 | private void delete(String[] cacheNames, String keySpEL, Method method, ProceedingJoinPoint pJoinPoint) { 163 | Object key = CacheAspectUtil.generateKey(keyGenerator,keySpEL, method, pJoinPoint); 164 | Assert.notNull(key, String.format(CACHE_KEY_ERROR_MESSAGE, keySpEL)); 165 | for (String cacheName : cacheNames) { 166 | Collection iCaches = iCacheManager.getCache(cacheName); 167 | if (CollectionUtils.isEmpty(iCaches)) { 168 | // 如果没有找到Cache就新建一个默认的 169 | ICache iCache = iCacheManager.getCache(cacheName, 170 | new MultiLayeringCacheSetting(new FirstCacheSetting(), new SecondaryCacheSetting())); 171 | iCache.evict(key); 172 | } else { 173 | for (ICache iCache : iCaches) { 174 | iCache.evict(key); 175 | } 176 | } 177 | } 178 | } 179 | 180 | @Around("cachePutPointCut()") 181 | public Object cachePutAroundAdvice(ProceedingJoinPoint pJoinPoint) throws Throwable{ 182 | 183 | //通过非缓存的方式获取数据的操作类接口 184 | CacheOperationInvoker aopInvoker = CacheAspectUtil.getCacheOpreationInvoker(pJoinPoint); 185 | 186 | //获取正在执行的目标方法 187 | Method method = CacheAspectUtil.getSpecificMethod(pJoinPoint); 188 | 189 | //获取方法上的CachePut注解 190 | CachePut cachePut = AnnotationUtils.findAnnotation(method,CachePut.class); 191 | 192 | try { 193 | // 执行查询缓存方法 194 | return executeCachePut(aopInvoker, method, cachePut, pJoinPoint); 195 | } catch (Exception e) { 196 | // 忽略操作缓存过程中遇到的异常 197 | if (cachePut.ignoreException()) { 198 | log.warn(e.getMessage(), e); 199 | return aopInvoker.invoke(); 200 | } 201 | throw e; 202 | } 203 | 204 | } 205 | 206 | private Object executeCachePut(CacheOperationInvoker aopInvoker, Method method, CachePut cachePut, ProceedingJoinPoint pJoinPoint) throws Throwable{ 207 | 208 | // 解析SpEL表达式获取cacheName和key 209 | String[] cacheNames = cachePut.cacheNames(); 210 | Assert.notEmpty(cachePut.cacheNames(), CACHE_NAME_ERROR_MESSAGE); 211 | 212 | Object key = CacheAspectUtil.generateKey(keyGenerator,cachePut.key(), method, pJoinPoint); 213 | Assert.notNull(key, String.format(CACHE_KEY_ERROR_MESSAGE, cachePut.key())); 214 | 215 | // 构造多级缓存配置信息 216 | MultiLayeringCacheSetting layeringCacheSetting = CacheAspectUtil.generateMultiLayeringCacheSetting(cachePut.firstCache(),cachePut.secondaryCache()); 217 | 218 | // 指定调用方法获取缓存值 219 | Object result = aopInvoker.invoke(); 220 | for (String cacheName : cacheNames) { 221 | // 通过cacheName和缓存配置获取Cache 222 | ICache iCache = iCacheManager.getCache(cacheName, layeringCacheSetting); 223 | iCache.put(key, result); 224 | } 225 | return result; 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /multi-layering-cache-aspecj/src/main/java/com/github/roger/expression/CacheEvaluationContext.java: -------------------------------------------------------------------------------- 1 | package com.github.roger.expression; 2 | 3 | import org.springframework.context.expression.MethodBasedEvaluationContext; 4 | import org.springframework.core.ParameterNameDiscoverer; 5 | 6 | import java.lang.reflect.Method; 7 | import java.util.HashSet; 8 | import java.util.Set; 9 | 10 | public class CacheEvaluationContext extends MethodBasedEvaluationContext { 11 | 12 | private final Set unavailableVariables = new HashSet(1); 13 | 14 | public CacheEvaluationContext(CacheExpressionRootObject rootObject, Method targetMethod, Object[] args, ParameterNameDiscoverer parameterNameDiscoverer) { 15 | super(rootObject, targetMethod, args, parameterNameDiscoverer); 16 | } 17 | 18 | public void addUnavailableVariable(String name) { 19 | this.unavailableVariables.add(name); 20 | } 21 | 22 | @Override 23 | public Object lookupVariable(String name) { 24 | if(this.unavailableVariables.contains(name)){ 25 | throw new VariableNotAvailableException(name); 26 | } 27 | return super.lookupVariable(name); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /multi-layering-cache-aspecj/src/main/java/com/github/roger/expression/CacheExpressionRootObject.java: -------------------------------------------------------------------------------- 1 | package com.github.roger.expression; 2 | 3 | import lombok.Getter; 4 | import org.springframework.util.Assert; 5 | 6 | import java.lang.reflect.Method; 7 | 8 | /** 9 | * 描述表达式计算期间使用的根对象。 10 | */ 11 | @Getter 12 | public class CacheExpressionRootObject { 13 | private final Method method; 14 | 15 | private final Object[] args; 16 | 17 | private final Object target; 18 | 19 | private final Class targetClass; 20 | 21 | public CacheExpressionRootObject(Method method, Object[] args, Object target, Class targetClass) { 22 | Assert.notNull(method, "Method is required"); 23 | Assert.notNull(targetClass, "targetClass is required"); 24 | this.method = method; 25 | this.target = target; 26 | this.targetClass = targetClass; 27 | this.args = args; 28 | } 29 | 30 | public String getMethodName() { 31 | return this.method.getName(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /multi-layering-cache-aspecj/src/main/java/com/github/roger/expression/CacheOperationExpressionEvaluator.java: -------------------------------------------------------------------------------- 1 | package com.github.roger.expression; 2 | 3 | import org.springframework.aop.support.AopUtils; 4 | import org.springframework.context.expression.AnnotatedElementKey; 5 | import org.springframework.context.expression.CachedExpressionEvaluator; 6 | import org.springframework.expression.EvaluationContext; 7 | import org.springframework.expression.Expression; 8 | 9 | import java.lang.reflect.Method; 10 | import java.util.Map; 11 | import java.util.concurrent.ConcurrentHashMap; 12 | 13 | public class CacheOperationExpressionEvaluator extends CachedExpressionEvaluator { 14 | 15 | /** 16 | * 指示没有结果变量 17 | */ 18 | public static final Object NO_RESULT = new Object(); 19 | 20 | /** 21 | * 指示结果变量根本不能使用 22 | */ 23 | public static final Object RESULT_UNAVAILABLE = new Object(); 24 | 25 | /** 26 | * 保存结果对象的变量的名称。 27 | */ 28 | public static final String RESULT_VARIABLE = "result"; 29 | 30 | private final Map keyCache = new ConcurrentHashMap(64); 31 | 32 | private final Map cacheNameCache = new ConcurrentHashMap(64); 33 | 34 | private final Map conditionCache = new ConcurrentHashMap(64); 35 | 36 | private final Map unlessCache = new ConcurrentHashMap(64); 37 | 38 | private final Map targetMethodCache = 39 | new ConcurrentHashMap(64); 40 | 41 | public EvaluationContext createEvaluationContext(Method method, Object[] args, Object target, Class targetClass) { 42 | 43 | return createEvaluationContext(method, args, target, targetClass, NO_RESULT); 44 | } 45 | 46 | public EvaluationContext createEvaluationContext(Method method, Object[] args, 47 | Object target, Class targetClass, Object result) { 48 | 49 | CacheExpressionRootObject rootObject = new CacheExpressionRootObject( 50 | method, args, target, targetClass); 51 | Method targetMethod = getTargetMethod(targetClass, method); 52 | CacheEvaluationContext evaluationContext = new CacheEvaluationContext( 53 | rootObject, targetMethod, args, getParameterNameDiscoverer()); 54 | if (result == RESULT_UNAVAILABLE) { 55 | evaluationContext.addUnavailableVariable(RESULT_VARIABLE); 56 | } else if (result != NO_RESULT) { 57 | evaluationContext.setVariable(RESULT_VARIABLE, result); 58 | } 59 | return evaluationContext; 60 | } 61 | 62 | 63 | private Method getTargetMethod(Class targetClass, Method method) { 64 | AnnotatedElementKey methodKey = new AnnotatedElementKey(method, targetClass); 65 | Method targetMethod = this.targetMethodCache.get(methodKey); 66 | if(targetMethod == null){ 67 | targetMethod = AopUtils.getMostSpecificMethod(method, targetClass); 68 | if (targetMethod == null) { 69 | targetMethod = method; 70 | } 71 | this.targetMethodCache.put(methodKey, targetMethod); 72 | } 73 | return targetMethod; 74 | } 75 | 76 | 77 | public Object key(String expression, AnnotatedElementKey methodKey, EvaluationContext evalContext) { 78 | 79 | return getExpression(this.keyCache, methodKey, expression).getValue(evalContext); 80 | } 81 | 82 | public Object cacheName(String expression, AnnotatedElementKey methodKey, EvaluationContext evalContext) { 83 | 84 | return getExpression(this.cacheNameCache, methodKey, expression).getValue(evalContext); 85 | } 86 | 87 | public boolean condition(String conditionExpression, AnnotatedElementKey methodKey, EvaluationContext evalContext) { 88 | return getExpression(this.conditionCache, methodKey, conditionExpression).getValue(evalContext, boolean.class); 89 | } 90 | 91 | public boolean unless(String unlessExpression, AnnotatedElementKey methodKey, EvaluationContext evalContext) { 92 | return getExpression(this.unlessCache, methodKey, unlessExpression).getValue(evalContext, boolean.class); 93 | } 94 | 95 | /** 96 | * Clear all caches. 97 | */ 98 | void clear() { 99 | this.keyCache.clear(); 100 | this.conditionCache.clear(); 101 | this.unlessCache.clear(); 102 | this.targetMethodCache.clear(); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /multi-layering-cache-aspecj/src/main/java/com/github/roger/expression/VariableNotAvailableException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2002-2016 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.roger.expression; 18 | 19 | import lombok.Getter; 20 | import org.springframework.expression.EvaluationException; 21 | 22 | 23 | @Getter 24 | public class VariableNotAvailableException extends EvaluationException { 25 | 26 | private final String name; 27 | 28 | public VariableNotAvailableException(String name) { 29 | super("Variable '" + name + "' is not available"); 30 | this.name = name; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /multi-layering-cache-aspecj/src/main/java/com/github/roger/key/DefaultKey.java: -------------------------------------------------------------------------------- 1 | package com.github.roger.key; 2 | 3 | import lombok.Getter; 4 | import org.springframework.util.Assert; 5 | import org.springframework.util.StringUtils; 6 | 7 | import java.util.Arrays; 8 | 9 | public class DefaultKey { 10 | 11 | 12 | public static final DefaultKey EMPTY = new DefaultKey(); 13 | 14 | @Getter 15 | private final Object[] params; 16 | private final int hashCode; 17 | 18 | public DefaultKey(Object... elements) { 19 | Assert.notNull(elements, "Elements must not be null"); 20 | this.params = new Object[elements.length]; 21 | System.arraycopy(elements, 0, this.params, 0, elements.length); 22 | this.hashCode = Arrays.deepHashCode(this.params); 23 | } 24 | 25 | @Override 26 | public boolean equals(Object obj) { 27 | return (this == obj || (obj instanceof DefaultKey 28 | && Arrays.deepEquals(this.params, ((DefaultKey) obj).params))); 29 | } 30 | 31 | @Override 32 | public final int hashCode() { 33 | return this.hashCode; 34 | } 35 | 36 | @Override 37 | public String toString() { 38 | return getClass().getSimpleName() + " [" + StringUtils.arrayToCommaDelimitedString(this.params) + "]"; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /multi-layering-cache-aspecj/src/main/java/com/github/roger/key/KeyGenerator.java: -------------------------------------------------------------------------------- 1 | package com.github.roger.key; 2 | 3 | import java.lang.reflect.Method; 4 | 5 | public interface KeyGenerator { 6 | 7 | Object generate(Object target, Method method, Object... params); 8 | } 9 | -------------------------------------------------------------------------------- /multi-layering-cache-aspecj/src/main/java/com/github/roger/key/impl/DefaultKeyGenerator.java: -------------------------------------------------------------------------------- 1 | package com.github.roger.key.impl; 2 | 3 | import com.github.roger.key.DefaultKey; 4 | import com.github.roger.key.KeyGenerator; 5 | 6 | import java.lang.reflect.Method; 7 | 8 | public class DefaultKeyGenerator implements KeyGenerator { 9 | 10 | public Object generate(Object target, Method method, Object... params) { 11 | return generateKey(params); 12 | } 13 | 14 | private Object generateKey(Object[] params) { 15 | if (params == null || params.length == 0) { 16 | return DefaultKey.EMPTY; 17 | } 18 | if (params.length == 1) { 19 | Object param = params[0]; 20 | if (param != null && !param.getClass().isArray()) { 21 | return param; 22 | } 23 | } 24 | return new DefaultKey(params); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /multi-layering-cache-aspecj/src/main/java/com/github/roger/support/CacheOperationInvoker.java: -------------------------------------------------------------------------------- 1 | package com.github.roger.support; 2 | 3 | import lombok.Getter; 4 | 5 | public interface CacheOperationInvoker { 6 | 7 | /** 8 | * 调用此实例定义的缓存操作. 9 | * 如果缓存中获取不到对应的信息,使用此方法调用具体的获取数据 10 | * @return 11 | * @throws ThrowableWrapperException 12 | */ 13 | Object invoke() throws ThrowableWrapperException; 14 | 15 | class ThrowableWrapperException extends Exception { 16 | 17 | @Getter 18 | private final Throwable original; 19 | 20 | public ThrowableWrapperException(Throwable original) { 21 | super(original.getMessage(), original); 22 | this.original = original; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /multi-layering-cache-aspecj/src/main/java/com/github/roger/utils/CacheAspectUtil.java: -------------------------------------------------------------------------------- 1 | package com.github.roger.utils; 2 | 3 | import com.github.roger.annotation.FirstCache; 4 | import com.github.roger.annotation.SecondaryCache; 5 | import com.github.roger.expression.CacheOperationExpressionEvaluator; 6 | import com.github.roger.key.KeyGenerator; 7 | import com.github.roger.support.CacheOperationInvoker; 8 | import org.aspectj.lang.ProceedingJoinPoint; 9 | import org.aspectj.lang.reflect.MethodSignature; 10 | import org.github.roger.settings.FirstCacheSetting; 11 | import org.github.roger.settings.MultiLayeringCacheSetting; 12 | import org.github.roger.settings.SecondaryCacheSetting; 13 | import org.springframework.aop.framework.AopProxyUtils; 14 | import org.springframework.context.expression.AnnotatedElementKey; 15 | import org.springframework.core.BridgeMethodResolver; 16 | import org.springframework.expression.EvaluationContext; 17 | import org.springframework.util.ClassUtils; 18 | import org.springframework.util.StringUtils; 19 | 20 | import java.lang.reflect.Method; 21 | import java.util.Objects; 22 | 23 | public class CacheAspectUtil { 24 | 25 | /** 26 | * SpEL表达式计算器 27 | */ 28 | private static final CacheOperationExpressionEvaluator evaluator = new CacheOperationExpressionEvaluator(); 29 | 30 | public static CacheOperationInvoker getCacheOpreationInvoker(ProceedingJoinPoint pJoinPoint) { 31 | //就是返回一个CacheOperationInvoker接口实现类,也即实现invoker方法 32 | return () -> { 33 | try { 34 | return pJoinPoint.proceed(); 35 | } catch (Throwable ex) { 36 | throw new CacheOperationInvoker.ThrowableWrapperException(ex); 37 | } 38 | }; 39 | 40 | } 41 | 42 | public static Method getSpecificMethod(ProceedingJoinPoint pJoinPoint) { 43 | MethodSignature methodSignature = (MethodSignature) pJoinPoint.getSignature(); 44 | Method method = methodSignature.getMethod(); 45 | //方法可能在接口上,但我们需要来自目标类的属性。 46 | //如果目标类为空,则方法将保持不变。 47 | Class targetClass = AopProxyUtils.ultimateTargetClass(pJoinPoint.getTarget()); 48 | if (targetClass == null && pJoinPoint.getTarget() != null) { 49 | targetClass = pJoinPoint.getTarget().getClass(); 50 | } 51 | Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass); 52 | //如果我们要处理带有泛型参数的方法,请找到原始方法 53 | specificMethod = BridgeMethodResolver.findBridgedMethod(specificMethod); 54 | return specificMethod; 55 | } 56 | 57 | public static Object generateKey(KeyGenerator keyGenerator, String keySpEl, Method method, ProceedingJoinPoint pJoinPoint) { 58 | 59 | if(StringUtils.hasText(keySpEl)){ 60 | // 获取注解上的key属性值 61 | Class targetClass = getTargetClass(pJoinPoint.getTarget()); 62 | EvaluationContext evaluationContext = evaluator.createEvaluationContext( 63 | method,pJoinPoint.getArgs(),pJoinPoint.getTarget(), 64 | targetClass,CacheOperationExpressionEvaluator.NO_RESULT); 65 | AnnotatedElementKey methodCacheKey = new AnnotatedElementKey(method, targetClass); 66 | 67 | Object keyValue = evaluator.key(keySpEl,methodCacheKey,evaluationContext); 68 | 69 | return Objects.isNull(keyValue) ? "null" : keyValue; 70 | } 71 | 72 | return keyGenerator.generate(pJoinPoint.getTarget(), method, pJoinPoint.getArgs()); 73 | } 74 | 75 | /** 76 | * 获取类信息 77 | * 78 | * @param target Object 79 | * @return targetClass 80 | */ 81 | public static Class getTargetClass(Object target) { 82 | Class targetClass = AopProxyUtils.ultimateTargetClass(target); 83 | if (targetClass == null) { 84 | targetClass = target.getClass(); 85 | } 86 | return targetClass; 87 | } 88 | 89 | public static MultiLayeringCacheSetting generateMultiLayeringCacheSetting(FirstCache firstCache, SecondaryCache secondaryCache) { 90 | 91 | FirstCacheSetting firstCacheSetting = new FirstCacheSetting(firstCache.initialCapacity(), firstCache.maximumSize(), 92 | firstCache.expireTime(), firstCache.timeUnit(), firstCache.expireMode()); 93 | 94 | SecondaryCacheSetting secondaryCacheSetting = new SecondaryCacheSetting(secondaryCache.expireTime(), 95 | secondaryCache.preloadTime(), secondaryCache.timeUnit(), secondaryCache.forceRefresh(), 96 | secondaryCache.isUsePrefix(),secondaryCache.isAllowNullValue(), secondaryCache.magnification()); 97 | 98 | return new MultiLayeringCacheSetting(firstCacheSetting,secondaryCacheSetting); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /multi-layering-cache-aspecj/src/main/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | #定义LOG输出级别 2 | log4j.rootLogger=DEBUG,Console,File 3 | #定义日志输出目的地为控制台 4 | log4j.appender.Console=org.apache.log4j.ConsoleAppender 5 | log4j.appender.Console.Target=System.out 6 | #可以灵活地指定日志输出格式,下面一行是指定具体的格式 7 | log4j.appender.Console.layout = org.apache.log4j.PatternLayout 8 | log4j.appender.Console.layout.ConversionPattern=[%c] - %m%n 9 | 10 | #文件大小到达指定尺寸的时候产生一个新的文件 11 | log4j.appender.File = org.apache.log4j.RollingFileAppender 12 | #指定输出目录 13 | log4j.appender.File.File = F:/logs/multi-layering-cache-core.log 14 | #定义文件最大大小 15 | log4j.appender.File.MaxFileSize = 10MB 16 | # 输出所以日志,如果换成DEBUG表示输出DEBUG以上级别日志 17 | log4j.appender.File.Threshold = ALL 18 | log4j.appender.File.layout = org.apache.log4j.PatternLayout 19 | log4j.appender.File.layout.ConversionPattern =[%p] [%d{yyyy-MM-dd HH\:mm\:ss}][%c]%m%n 20 | 21 | -------------------------------------------------------------------------------- /multi-layering-cache-aspecj/src/test/java/com/github/roger/cahce/aspect/CacheAspectTest.java: -------------------------------------------------------------------------------- 1 | package com.github.roger.cahce.aspect; 2 | 3 | import com.github.roger.cahce.config.ICacheManagerConfig; 4 | import com.github.roger.domain.User; 5 | import com.github.roger.service.impl.UserServiceImpl; 6 | import lombok.extern.log4j.Log4j; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.junit.Assert; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.data.redis.core.RedisTemplate; 13 | import org.springframework.test.context.ContextConfiguration; 14 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 15 | 16 | // SpringJUnit4ClassRunner再Junit环境下提供Spring TestContext Framework的功能。 17 | @RunWith(SpringJUnit4ClassRunner.class) 18 | // @ContextConfiguration用来加载配置ApplicationContext,其中classes用来加载配置类 19 | @ContextConfiguration(classes = {ICacheManagerConfig.class}) 20 | @Log4j 21 | public class CacheAspectTest { 22 | 23 | @Autowired 24 | private UserServiceImpl userService; 25 | @Autowired 26 | private RedisTemplate redisTemplate; 27 | 28 | 29 | @Test 30 | public void testGetUserById(){ 31 | long userId = 111; 32 | // 二级缓存key : user:info:111 有效时间100秒 强制刷新时间 30秒 33 | // 根据UserId获取数据,首先从缓存中获取,如果获取不到,去数据库中获取 34 | // 然后把缓存写入一级,二级缓存 35 | User user = userService.getUserById(userId); 36 | sleep(10); 37 | // 已经写入二级redis缓存,因此可以从缓存中获取 38 | Object result = redisTemplate.opsForValue().get("user:info:111"); 39 | Assert.assertNotNull(result); 40 | 41 | sleep(65); 42 | long ttl = redisTemplate.getExpire("user:info:111"); 43 | log.debug("进入强制刷新缓存时间段 ttl = " + ttl); 44 | Assert.assertTrue(ttl > 0 && ttl < 30); 45 | userService.getUserById(userId); 46 | // 因为是开启线程去刷新二级缓存,因此这里在获取有效时间的时候,需要延后几秒,才能获取到新的有效时间 47 | sleep(2); 48 | long ttl2 = redisTemplate.getExpire("user:info:111"); 49 | log.debug("强制刷新缓存后,有效时间发生变化 ttl2 = " + ttl2); 50 | Assert.assertTrue(ttl2 > 50); 51 | 52 | } 53 | 54 | @Test 55 | public void testSaveUser(){ 56 | User user = new User(); 57 | // 二级缓存key : user:info:32有效时间100秒 强制刷新时间 30秒 58 | // @Cacheable 注解的value值 + 传递参数对象的 user.getUserId() 组成二级缓存的缓存key 59 | userService.saveUser(user); 60 | log.debug("保存User对象 " + user); 61 | } 62 | 63 | @Test 64 | public void testGetUserNoKey(){ 65 | userService.getUserNoKey(123,new String[]{"w","y","z"}); 66 | } 67 | 68 | private void sleep(int time) { 69 | try { 70 | Thread.sleep(time * 1000); 71 | } catch (InterruptedException e) { 72 | log.error(e.getMessage(), e); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /multi-layering-cache-aspecj/src/test/java/com/github/roger/cahce/config/ICacheManagerConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.roger.cahce.config; 2 | 3 | import com.github.roger.aspect.MultiLayeringCacheAspect; 4 | import com.github.roger.service.impl.UserServiceImpl; 5 | import org.github.roger.MultiLayeringCacheManager; 6 | import org.github.roger.manager.ICacheManager; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.context.annotation.EnableAspectJAutoProxy; 10 | import org.springframework.context.annotation.Import; 11 | import org.springframework.data.redis.core.RedisTemplate; 12 | 13 | @Configuration 14 | @Import({RedisConfig.class}) 15 | @EnableAspectJAutoProxy 16 | public class ICacheManagerConfig { 17 | 18 | 19 | @Bean 20 | public ICacheManager cacheManager(RedisTemplate redisTemplate) { 21 | MultiLayeringCacheManager layeringCacheManager = new MultiLayeringCacheManager(redisTemplate); 22 | 23 | return layeringCacheManager; 24 | } 25 | 26 | @Bean//把切面交给Spring 容器管理 27 | public MultiLayeringCacheAspect layeringCacheAspect(){ 28 | return new MultiLayeringCacheAspect(); 29 | } 30 | 31 | @Bean 32 | public UserServiceImpl userService(){ 33 | return new UserServiceImpl(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /multi-layering-cache-aspecj/src/test/java/com/github/roger/cahce/config/RedisConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.roger.cahce.config; 2 | 3 | import io.lettuce.core.resource.ClientResources; 4 | import io.lettuce.core.resource.DefaultClientResources; 5 | import org.github.roger.serializer.FastJsonRedisSerializer; 6 | import org.github.roger.serializer.KryoRedisSerializer; 7 | import org.github.roger.serializer.StringRedisSerializer; 8 | import org.springframework.beans.factory.annotation.Value; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | import org.springframework.context.annotation.PropertySource; 12 | import org.springframework.data.redis.connection.RedisConnectionFactory; 13 | import org.springframework.data.redis.connection.RedisPassword; 14 | import org.springframework.data.redis.connection.RedisStandaloneConfiguration; 15 | import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration; 16 | import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration.LettuceClientConfigurationBuilder; 17 | import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; 18 | import org.springframework.data.redis.core.RedisTemplate; 19 | 20 | @Configuration 21 | @PropertySource({"classpath:application.properties"}) 22 | public class RedisConfig { 23 | 24 | @Value("${spring.redis.database:0}") 25 | private int database; 26 | 27 | @Value("${spring.redis.host:127.0.0.1}") 28 | private String host; 29 | 30 | @Value("${spring.redis.password:}") 31 | private String password; 32 | 33 | @Value("${spring.redis.port:6379}") 34 | private int port; 35 | 36 | @Value("${spring.redis.pool.max-idle:200}") 37 | private int maxIdle; 38 | 39 | @Value("${spring.redis.pool.min-idle:10}") 40 | private int minIdle; 41 | 42 | @Value("${spring.redis.pool.max-active:80}") 43 | private int maxActive; 44 | 45 | @Value("${spring.redis.pool.max-wait:-1}") 46 | private int maxWait; 47 | 48 | 49 | // @Bean 50 | // public JedisConnectionFactory redisConnectionFactory() { 51 | // JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); 52 | // jedisPoolConfig.setMinIdle(minIdle); 53 | // jedisPoolConfig.setMaxIdle(maxIdle); 54 | // jedisPoolConfig.setMaxTotal(maxActive); 55 | // jedisPoolConfig.setMaxWaitMillis(maxWait); 56 | // 57 | // JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(jedisPoolConfig); 58 | // jedisConnectionFactory.setDatabase(database); 59 | // jedisConnectionFactory.setHostName(host); 60 | // jedisConnectionFactory.setPassword(password); 61 | // jedisConnectionFactory.setPort(port); 62 | // jedisConnectionFactory.setUsePool(true); 63 | // return jedisConnectionFactory; 64 | // } 65 | 66 | @Bean 67 | public LettuceConnectionFactory redisConnectionFactory() { 68 | ClientResources clientResources = DefaultClientResources.create(); 69 | LettuceClientConfigurationBuilder builder = LettuceClientConfiguration.builder(); 70 | builder.clientResources(clientResources); 71 | LettuceClientConfiguration configuration = builder.build(); 72 | 73 | RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(); 74 | config.setHostName(host); 75 | config.setPort(port); 76 | config.setPassword(RedisPassword.of(password)); 77 | config.setDatabase(database); 78 | return new LettuceConnectionFactory(config, configuration); 79 | } 80 | 81 | @Bean 82 | public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { 83 | RedisTemplate redisTemplate = new RedisTemplate(); 84 | redisTemplate.setConnectionFactory(redisConnectionFactory); 85 | 86 | FastJsonRedisSerializer fastJsonRedisSerializer = new FastJsonRedisSerializer(Object.class, "com.xxx."); 87 | KryoRedisSerializer kryoRedisSerializer = new KryoRedisSerializer(Object.class); 88 | 89 | // 设置值(value)的序列化采用FastJsonRedisSerializer。 90 | redisTemplate.setValueSerializer(kryoRedisSerializer); 91 | redisTemplate.setHashValueSerializer(kryoRedisSerializer); 92 | // 设置键(key)的序列化采用StringRedisSerializer。 93 | redisTemplate.setKeySerializer(new StringRedisSerializer()); 94 | redisTemplate.setHashKeySerializer(new StringRedisSerializer()); 95 | 96 | redisTemplate.afterPropertiesSet(); 97 | return redisTemplate; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /multi-layering-cache-aspecj/src/test/java/com/github/roger/cahce/utils/RedisCacheKeyTest.java: -------------------------------------------------------------------------------- 1 | package com.github.roger.cahce.utils; 2 | 3 | import com.github.roger.key.DefaultKey; 4 | import org.github.roger.serializer.StringRedisSerializer; 5 | import org.github.roger.utils.RedisCacheKey; 6 | import org.junit.Test; 7 | 8 | public class RedisCacheKeyTest { 9 | 10 | @Test 11 | public void testGetKey() { 12 | 13 | DefaultKey defaultKey = new DefaultKey("arg1","arg2"); 14 | System.out.println("defaultKey = " + defaultKey); 15 | RedisCacheKey redisCacheKey = new RedisCacheKey(defaultKey,new StringRedisSerializer()); 16 | redisCacheKey.cacheName("cacheName"); 17 | String cacheKeyString = redisCacheKey.getKey(); 18 | 19 | System.out.println("缓存key = " + cacheKeyString); 20 | } 21 | } -------------------------------------------------------------------------------- /multi-layering-cache-aspecj/src/test/java/com/github/roger/domain/Address.java: -------------------------------------------------------------------------------- 1 | package com.github.roger.domain; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class Address { 7 | 8 | private String addredd = "上海"; 9 | } 10 | -------------------------------------------------------------------------------- /multi-layering-cache-aspecj/src/test/java/com/github/roger/domain/User.java: -------------------------------------------------------------------------------- 1 | package com.github.roger.domain; 2 | 3 | import lombok.Data; 4 | import lombok.ToString; 5 | 6 | import java.math.BigDecimal; 7 | import java.util.*; 8 | 9 | @Data 10 | @ToString 11 | public class User { 12 | 13 | private long userId; 14 | 15 | private String name; 16 | 17 | private Address address; 18 | 19 | private String[] lastName; 20 | 21 | private List lastNameList; 22 | 23 | private Set lastNameSet; 24 | 25 | private int age; 26 | 27 | private double height; 28 | 29 | private BigDecimal income; 30 | 31 | private Date birthday; 32 | 33 | 34 | public User() { 35 | this.userId = 32L; 36 | this.name = "name"; 37 | this.address = new Address(); 38 | List lastNameList = new ArrayList(); 39 | lastNameList.add("W"); 40 | lastNameList.add("成都"); 41 | this.lastNameList = lastNameList; 42 | this.lastNameSet = new HashSet(lastNameList); 43 | this.lastName = new String[]{"w", "四川", "~!@#%……&*()——+{}:“?》:''\">?《~!@#$%^&*()_+{}\\"}; 44 | this.age = 122; 45 | this.height = 18.2; 46 | this.income = new BigDecimal(22.22); 47 | this.birthday = new Date(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /multi-layering-cache-aspecj/src/test/java/com/github/roger/key/impl/DefaultKeyGeneratorTest.java: -------------------------------------------------------------------------------- 1 | package com.github.roger.key.impl; 2 | 3 | 4 | import org.junit.Test; 5 | 6 | public class DefaultKeyGeneratorTest { 7 | 8 | 9 | @Test 10 | public void generate() { 11 | DefaultKeyGenerator defaultKeyGenerator = new DefaultKeyGenerator(); 12 | 13 | System.out.println(defaultKeyGenerator.generate(null,null,null)); 14 | System.out.println(defaultKeyGenerator.generate(null,null,"1")); 15 | System.out.println(defaultKeyGenerator.generate(null,null,"1","2")); 16 | } 17 | } -------------------------------------------------------------------------------- /multi-layering-cache-aspecj/src/test/java/com/github/roger/service/impl/UserServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.roger.service.impl; 2 | 3 | import com.github.roger.annotation.Cacheable; 4 | import com.github.roger.annotation.FirstCache; 5 | import com.github.roger.annotation.SecondaryCache; 6 | import com.github.roger.domain.User; 7 | import lombok.extern.slf4j.Slf4j; 8 | 9 | import java.util.concurrent.TimeUnit; 10 | 11 | @Slf4j 12 | public class UserServiceImpl { 13 | 14 | 15 | @Cacheable(value = "user:info", key = "#userId", ignoreException = false, 16 | firstCache = @FirstCache(expireTime = 4, timeUnit = TimeUnit.SECONDS), 17 | secondaryCache = @SecondaryCache(expireTime = 100, preloadTime = 30, 18 | forceRefresh = true, timeUnit = TimeUnit.SECONDS, isAllowNullValue = true)) 19 | public User getUserById(long userId) { 20 | log.debug("测试正常配置的缓存方法,参数是基本类型"); 21 | User user = new User(); 22 | user.setUserId(userId); 23 | user.setAge(31); 24 | user.setLastName(new String[]{"w", "y", "h"}); 25 | return user; 26 | } 27 | 28 | 29 | @Cacheable(value = "user:info", key = "#user.userId", ignoreException = false, 30 | firstCache = @FirstCache(expireTime = 4, timeUnit = TimeUnit.SECONDS), 31 | secondaryCache = @SecondaryCache(expireTime = 100, preloadTime = 30, 32 | forceRefresh = true, timeUnit = TimeUnit.SECONDS, isAllowNullValue = true)) 33 | public User saveUser(User user) { 34 | log.debug("测试正常配置的缓存方法,参数是基本类型"); 35 | user.setAge(32); 36 | user.setLastName(new String[]{"w", "y", "h"}); 37 | return user; 38 | } 39 | 40 | @Cacheable(value = "user:info", ignoreException = false, 41 | firstCache = @FirstCache(expireTime = 4, timeUnit = TimeUnit.SECONDS), 42 | secondaryCache = @SecondaryCache(expireTime = 100, preloadTime = 30, forceRefresh = true, timeUnit = TimeUnit.SECONDS)) 43 | public User getUserNoKey(long userId, String[] lastName) { 44 | log.debug("测试没有配置key的缓存方法,参数是基本类型和数组的缓存缓存方法"); 45 | User user = new User(); 46 | user.setUserId(userId); 47 | user.setAge(31); 48 | user.setLastName(lastName); 49 | return user; 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /multi-layering-cache-aspecj/src/test/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.redis.pool.max-idle=200 2 | spring.redis.pool.min-idle=10 3 | spring.redis.pool.max-active=80 4 | spring.redis.pool.max-wait=-1 5 | 6 | spring.redis.database=0 7 | #server host 8 | spring.redis.host=127.0.0.1 9 | #server password 10 | spring.redis.password= 11 | #connection port 12 | spring.redis.port=6379 13 | -------------------------------------------------------------------------------- /multi-layering-cache-aspecj/src/test/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | #定义LOG输出级别 2 | log4j.rootLogger=debug,stdout,File 3 | #定义日志输出目的地为控制台 4 | log4j.appender.stdout=org.apache.log4j.ConsoleAppender 5 | log4j.appender.stdout.Target=System.out 6 | #可以灵活地指定日志输出格式,下面一行是指定具体的格式 7 | log4j.appender.stdout.layout = org.apache.log4j.PatternLayout 8 | log4j.appender.stdout.layout.ConversionPattern=[%c] - %m%n 9 | 10 | #文件大小到达指定尺寸的时候产生一个新的文件 11 | log4j.appender.File = org.apache.log4j.RollingFileAppender 12 | #指定输出目录 13 | log4j.appender.File.File = F:/logs/multi-layering-cache-core.log 14 | #定义文件最大大小 15 | log4j.appender.File.MaxFileSize = 10MB 16 | # 输出所以日志,如果换成DEBUG表示输出DEBUG以上级别日志 17 | log4j.appender.File.Threshold = ALL 18 | log4j.appender.File.layout = org.apache.log4j.PatternLayout 19 | log4j.appender.File.layout.ConversionPattern =[%p] [%d{yyyy-MM-dd HH\:mm\:ss}][%c]%m%n 20 | 21 | -------------------------------------------------------------------------------- /multi-layering-cache-core/.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | /target/ -------------------------------------------------------------------------------- /multi-layering-cache-core/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | multi-layering-cache 7 | com.github.roger 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | multi-layering-cache-core 13 | 14 | multi-layering-cache-core 15 | 多级缓存核心模块 16 | jar 17 | 18 | 19 | 20 | 21 | 22 | com.esotericsoftware 23 | kryo-shaded 24 | 25 | 26 | 27 | com.alibaba 28 | fastjson 29 | 30 | 31 | 32 | com.github.ben-manes.caffeine 33 | caffeine 34 | 35 | 36 | 37 | org.springframework 38 | spring-beans 39 | 40 | 41 | 42 | org.springframework.data 43 | spring-data-redis 44 | 45 | 46 | 47 | io.lettuce 48 | lettuce-core 49 | 50 | 51 | 52 | org.springframework.boot 53 | spring-boot-starter-test 54 | test 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /multi-layering-cache-core/src/main/java/org/github/roger/MultiLayeringCache.java: -------------------------------------------------------------------------------- 1 | package org.github.roger; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.github.roger.cache.AbstractValueAdaptingCache; 6 | import org.github.roger.cache.ICache; 7 | import org.github.roger.cache.caffeine.CaffeineCache; 8 | import org.github.roger.cache.redis.RedisCache; 9 | import org.github.roger.enumeration.RedisPubSubMessageType; 10 | import org.github.roger.listener.RedisPublisher; 11 | import org.github.roger.message.RedisPubSubMessage; 12 | import org.github.roger.settings.MultiLayeringCacheSetting; 13 | import org.springframework.data.redis.core.RedisTemplate; 14 | import org.springframework.data.redis.listener.ChannelTopic; 15 | 16 | import java.util.concurrent.Callable; 17 | 18 | @Slf4j 19 | public class MultiLayeringCache extends AbstractValueAdaptingCache { 20 | 21 | /** 22 | * redis 客户端 23 | */ 24 | private RedisTemplate redisTemplate; 25 | 26 | private AbstractValueAdaptingCache firstCache; 27 | 28 | private AbstractValueAdaptingCache secondCache; 29 | 30 | private boolean useFirstCache = true; 31 | private MultiLayeringCacheSetting multilayeringCacheSetting; 32 | 33 | public MultiLayeringCache(RedisTemplate redisTemplate, AbstractValueAdaptingCache firstCache, AbstractValueAdaptingCache secondCache, MultiLayeringCacheSetting multilayeringCacheSetting) { 34 | this(secondCache.getName(),redisTemplate,firstCache,secondCache,true,multilayeringCacheSetting); 35 | } 36 | 37 | public MultiLayeringCache(String name,RedisTemplate redisTemplate,AbstractValueAdaptingCache firstCache, AbstractValueAdaptingCache secondCache, boolean useFirstCache,MultiLayeringCacheSetting multilayeringCacheSetting) { 38 | super(name); 39 | this.redisTemplate = redisTemplate; 40 | this.firstCache = firstCache; 41 | this.secondCache = secondCache; 42 | this.useFirstCache = useFirstCache; 43 | this.multilayeringCacheSetting = multilayeringCacheSetting; 44 | } 45 | 46 | public Object getRealCache() { 47 | return this; 48 | } 49 | 50 | public Object get(Object key) { 51 | Object storeValue = null; 52 | if(useFirstCache){ 53 | storeValue = firstCache.get(key); 54 | log.debug("查询一级缓存。 key={},返回值是:{}", key, JSON.toJSONString(storeValue)); 55 | } 56 | if(storeValue == null){ 57 | storeValue = secondCache.get(key); 58 | firstCache.putIfAbsent(key, storeValue); 59 | log.debug("查询二级缓存,并将数据放到一级缓存。 key={},返回值是:{}", key, JSON.toJSONString(storeValue)); 60 | } 61 | return fromStoreValue(storeValue); 62 | } 63 | 64 | @Override 65 | public T get(Object key, Class type) { 66 | if (useFirstCache) { 67 | Object result = firstCache.get(key, type); 68 | log.debug("查询一级缓存。 key={},返回值是:{}", key, JSON.toJSONString(result)); 69 | if (result != null) { 70 | return (T) fromStoreValue(result); 71 | } 72 | } 73 | 74 | T result = secondCache.get(key, type); 75 | firstCache.putIfAbsent(key, result); 76 | log.debug("查询二级缓存,并将数据放到一级缓存。 key={},返回值是:{}", key, JSON.toJSONString(result)); 77 | return result; 78 | } 79 | 80 | public T get(Object key, Callable valueLoader) { 81 | if (useFirstCache) { 82 | Object result = firstCache.get(key); 83 | log.debug("查询一级缓存。 key={},返回值是:{}", key, JSON.toJSONString(result)); 84 | if (result != null) { 85 | return (T) fromStoreValue(result); 86 | } 87 | } 88 | T result = secondCache.get(key, valueLoader); 89 | firstCache.putIfAbsent(key, result); 90 | log.debug("查询二级缓存,并将数据放到一级缓存。 key={},返回值是:{}", key, JSON.toJSONString(result)); 91 | return result; 92 | } 93 | 94 | public void put(Object key, Object value) { 95 | secondCache.put(key, value); 96 | // 删除一级缓存 97 | if (useFirstCache) { 98 | deleteFirstCache(key); 99 | } 100 | } 101 | 102 | public Object putIfAbsent(Object key, Object value) { 103 | Object result = secondCache.putIfAbsent(key, value); 104 | // 删除一级缓存 105 | if (useFirstCache) { 106 | deleteFirstCache(key); 107 | } 108 | return result; 109 | } 110 | 111 | public void evict(Object key) { 112 | // 删除的时候要先删除二级缓存再删除一级缓存,否则有并发问题 113 | secondCache.evict(key); 114 | // 删除一级缓存 115 | if (useFirstCache) { 116 | deleteFirstCache(key); 117 | } 118 | } 119 | 120 | private void deleteFirstCache(Object key) { 121 | // 删除一级缓存需要用到redis的Pub/Sub(订阅/发布)模式,否则集群中其他服服务器节点的一级缓存数据无法删除 122 | RedisPubSubMessage message = new RedisPubSubMessage(); 123 | message.setCacheName(getName()); 124 | message.setKey(key); 125 | message.setMessageType(RedisPubSubMessageType.EVICT); 126 | // 发布消息 127 | RedisPublisher.publisher(redisTemplate, new ChannelTopic(getName()), message); 128 | } 129 | 130 | public void clear() { 131 | // 删除的时候要先删除二级缓存再删除一级缓存,否则有并发问题 132 | secondCache.clear(); 133 | if (useFirstCache) { 134 | // 清除一级缓存需要用到redis的订阅/发布模式,否则集群中其他服服务器节点的一级缓存数据无法删除 135 | RedisPubSubMessage message = new RedisPubSubMessage(); 136 | message.setCacheName(getName()); 137 | message.setMessageType(RedisPubSubMessageType.CLEAR); 138 | // 发布消息 139 | RedisPublisher.publisher(redisTemplate, new ChannelTopic(getName()), message); 140 | } 141 | } 142 | 143 | public ICache getFirstCache() { 144 | return firstCache; 145 | } 146 | 147 | public ICache getSecondCache() { 148 | return secondCache; 149 | } 150 | 151 | public boolean isAllowNullValues() { 152 | return secondCache.isAllowNullValues(); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /multi-layering-cache-core/src/main/java/org/github/roger/MultiLayeringCacheManager.java: -------------------------------------------------------------------------------- 1 | package org.github.roger; 2 | 3 | import org.github.roger.cache.ICache; 4 | import org.github.roger.cache.caffeine.CaffeineCache; 5 | import org.github.roger.cache.redis.RedisCache; 6 | import org.github.roger.manager.AbstractCacheManager; 7 | import org.github.roger.settings.MultiLayeringCacheSetting; 8 | import org.springframework.data.redis.core.RedisTemplate; 9 | 10 | public class MultiLayeringCacheManager extends AbstractCacheManager { 11 | 12 | public MultiLayeringCacheManager(RedisTemplate redisTemplate) { 13 | this.redisTemplate = redisTemplate; 14 | cacheManagers.add(this); 15 | } 16 | 17 | @Override 18 | protected ICache getMissingCache(String name, MultiLayeringCacheSetting multilayeringCacheSetting) { 19 | // 创建一级缓存 20 | CaffeineCache caffeineCache = new CaffeineCache(name, multilayeringCacheSetting.getFirstCacheSetting()); 21 | // 创建二级缓存 22 | RedisCache redisCache = new RedisCache(name, redisTemplate, multilayeringCacheSetting.getSecondaryCacheSetting()); 23 | return new MultiLayeringCache(redisTemplate, caffeineCache, redisCache, multilayeringCacheSetting); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /multi-layering-cache-core/src/main/java/org/github/roger/cache/AbstractValueAdaptingCache.java: -------------------------------------------------------------------------------- 1 | package org.github.roger.cache; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import org.github.roger.support.NullValue; 5 | import org.springframework.util.Assert; 6 | 7 | import java.util.concurrent.Callable; 8 | 9 | public abstract class AbstractValueAdaptingCache implements ICache{ 10 | 11 | //缓存名称 12 | private String name; 13 | 14 | public AbstractValueAdaptingCache(String name) { 15 | Assert.notNull(name,"缓存名称不能为空"); 16 | this.name = name; 17 | } 18 | 19 | /** 20 | * 缓存对象是否允许为空 21 | * @return true 允许 false 不允许 22 | */ 23 | public abstract boolean isAllowNullValues(); 24 | 25 | @Override 26 | public String getName() { 27 | return this.name; 28 | } 29 | 30 | @Override 31 | public T get(Object key, Class type) { 32 | return (T) fromStoreValue(get(key)); 33 | } 34 | 35 | protected Object fromStoreValue(Object storeValue) { 36 | if(isAllowNullValues() && storeValue instanceof NullValue){ 37 | return null; 38 | } 39 | return storeValue; 40 | } 41 | 42 | protected Object toStoreValue(Object userValue){ 43 | if(isAllowNullValues() && userValue == null){ 44 | return NullValue.INSTANCE; 45 | } 46 | return userValue; 47 | } 48 | 49 | 50 | /** 51 | * {@link #get(Object, Callable)} 方法加载缓存值的包装异常 52 | */ 53 | public class LoaderCacheValueException extends RuntimeException { 54 | 55 | private final Object key; 56 | 57 | public LoaderCacheValueException(Object key, Throwable ex) { 58 | super(String.format("加载key为 %s 的缓存数据,执行被缓存方法异常", JSON.toJSONString(key)), ex); 59 | this.key = key; 60 | } 61 | 62 | public Object getKey() { 63 | return this.key; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /multi-layering-cache-core/src/main/java/org/github/roger/cache/ICache.java: -------------------------------------------------------------------------------- 1 | package org.github.roger.cache; 2 | 3 | import java.util.concurrent.Callable; 4 | 5 | /** 6 | * 缓存的顶级接口 7 | */ 8 | public interface ICache { 9 | 10 | /** 11 | * 定义缓存对象的名称,是为了区分不同的缓存对象 12 | * @return 返回缓存的名称 13 | */ 14 | String getName(); 15 | 16 | /** 17 | * @return 返回真实的缓存对象 18 | */ 19 | Object getRealCache(); 20 | 21 | /** 22 | * 根据key获取其对应的缓存对象,如果没有就返回null 23 | * @param key 24 | * @return 返回缓存key对应的缓存对象 25 | */ 26 | Object get(Object key); 27 | 28 | /** 29 | * 根据key获取其对应的缓存对象,并将返回的缓存对象转换成对应的类型 30 | * 如果没有就返回null 31 | * @param key 32 | * @param type 缓存对象的类型 33 | * @param 34 | * @return 35 | */ 36 | T get(Object key,Class type); 37 | 38 | /** 39 | * 根据key获取其对应的缓存对象,并将返回的缓存对象转换成对应的类型 40 | * 如果对应key不存在则调用valueLoader加载数据 41 | * @param key 42 | * @param valueLoader 加载缓存的回调方法 43 | * @param 44 | * @return 45 | */ 46 | T get(Object key, Callable valueLoader); 47 | 48 | /** 49 | * 将对应key-value放到缓存,如果key原来有值就直接覆盖 50 | * 51 | * @param key 缓存key 52 | * @param value 缓存的值 53 | */ 54 | void put(Object key, Object value); 55 | 56 | /** 57 | * 如果缓存key没有对应的值就将值put到缓存,如果有就直接返回原有的值 58 | * 就相当于: 59 | * Object existingValue = cache.get(key); 60 | * if (existingValue == null) { 61 | * cache.put(key, value); 62 | * return null; 63 | * } else { 64 | * return existingValue; 65 | * } 66 | * @param key 缓存key 67 | * @param value 缓存key对应的值 68 | * @return 因为值本身可能为NULL,或者缓存key本来就没有对应值的时候也为NULL, 69 | * 所以如果返回NULL就表示已经将key-value键值对放到了缓存中 70 | */ 71 | Object putIfAbsent(Object key, Object value); 72 | 73 | /** 74 | * 在缓存中删除对应的key 75 | * 76 | * @param key 缓存key 77 | */ 78 | void evict(Object key); 79 | 80 | /** 81 | * 清楚缓存 82 | */ 83 | void clear(); 84 | 85 | } 86 | -------------------------------------------------------------------------------- /multi-layering-cache-core/src/main/java/org/github/roger/cache/caffeine/CaffeineCache.java: -------------------------------------------------------------------------------- 1 | package org.github.roger.cache.caffeine; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.github.benmanes.caffeine.cache.Cache; 5 | import com.github.benmanes.caffeine.cache.Caffeine; 6 | import com.github.benmanes.caffeine.cache.LoadingCache; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.github.roger.cache.AbstractValueAdaptingCache; 9 | import org.github.roger.enumeration.ExpireMode; 10 | import org.github.roger.settings.FirstCacheSetting; 11 | import org.github.roger.support.NullValue; 12 | 13 | import java.util.concurrent.Callable; 14 | 15 | @Slf4j 16 | public class CaffeineCache extends AbstractValueAdaptingCache { 17 | 18 | /** caffeine 缓存对象 */ 19 | private Cache cache; 20 | 21 | 22 | /** 23 | * 使用name和{@link FirstCacheSetting}创建一个 {@link CaffeineCache} 实例 24 | * 25 | * @param name 缓存名称 26 | * @param firstCacheSetting 一级缓存配置 {@link FirstCacheSetting} 27 | */ 28 | public CaffeineCache(String name, FirstCacheSetting firstCacheSetting) { 29 | super( name); 30 | this.cache = getCache(firstCacheSetting); 31 | } 32 | 33 | /** 34 | * 构建一个caffeine缓存对象,用于存储一级缓存 35 | * @param firstCacheSetting 一级缓存配置 36 | * @return 一级缓存对象 37 | */ 38 | private Cache getCache(FirstCacheSetting firstCacheSetting) { 39 | //根据一级缓存设置,构建caffeine缓存对象 40 | Caffeine cacheBuilder = Caffeine.newBuilder(); 41 | cacheBuilder.initialCapacity(firstCacheSetting.getInitialCapacity()); 42 | cacheBuilder.maximumSize(firstCacheSetting.getMaximumSize()); 43 | if(ExpireMode.WRITE.equals(firstCacheSetting.getExpireMode())){ 44 | cacheBuilder.expireAfterWrite(firstCacheSetting.getExpireTime(),firstCacheSetting.getTimeUnit()); 45 | } 46 | if(ExpireMode.ACCESS.equals(firstCacheSetting.getExpireMode())){ 47 | cacheBuilder.expireAfterAccess(firstCacheSetting.getExpireTime(),firstCacheSetting.getTimeUnit()); 48 | } 49 | return cacheBuilder.build(); 50 | } 51 | 52 | 53 | public boolean isAllowNullValues() { 54 | return false; 55 | } 56 | 57 | public Object getRealCache() { 58 | return this.cache; 59 | } 60 | 61 | public Object get(Object key) { 62 | log.debug("caffeine缓存 key={} 获取缓存", JSON.toJSONString(key)); 63 | 64 | if (this.cache instanceof LoadingCache) { 65 | return ((LoadingCache) this.cache).get(key); 66 | } 67 | 68 | return cache.getIfPresent(key); 69 | } 70 | 71 | public T get(Object key, Callable valueLoader) { 72 | log.debug("caffeine缓存 key={} 获取缓存, 如果没有命中就走库加载缓存", JSON.toJSONString(key)); 73 | // 获取key对应的缓存值,如果没有,就使用valuLoader 获取值,类似设置一个默认值 74 | Object result = this.cache.get(key, (k) -> loaderValue(key, valueLoader)); 75 | // 如果不允许存NULL值 直接删除NULL值缓存 76 | boolean isEvict = !isAllowNullValues() && (result == null || result instanceof NullValue); 77 | if (isEvict) { 78 | evict(key); 79 | } 80 | return (T) fromStoreValue(result); 81 | } 82 | 83 | /** 84 | * 加载数据 85 | */ 86 | private Object loaderValue(Object key, Callable valueLoader) { 87 | try { 88 | T t = valueLoader.call(); 89 | log.debug("caffeine缓存 key={} 从库加载缓存", JSON.toJSONString(key), JSON.toJSONString(t)); 90 | 91 | return toStoreValue(t); 92 | } catch (Exception e) { 93 | throw new LoaderCacheValueException(key, e); 94 | } 95 | } 96 | 97 | public void put(Object key, Object value) { 98 | // 允许存NULL值 99 | if (isAllowNullValues()) { 100 | log.debug("caffeine缓存 key={} put缓存,缓存值:{}", JSON.toJSONString(key), JSON.toJSONString(value)); 101 | this.cache.put(key, toStoreValue(value)); 102 | return; 103 | } 104 | 105 | // 不允许存NULL值 106 | if (value != null && !(value instanceof NullValue)) { 107 | log.debug("caffeine缓存 key={} put缓存,缓存值:{}", JSON.toJSONString(key), JSON.toJSONString(value)); 108 | this.cache.put(key, toStoreValue(value)); 109 | } 110 | log.debug("缓存值为NULL并且不允许存NULL值,不缓存数据"); 111 | } 112 | 113 | public Object putIfAbsent(Object key, Object value) { 114 | log.debug("caffeine缓存 key={} putIfAbsent 缓存,缓存值:{}", JSON.toJSONString(key), JSON.toJSONString(value)); 115 | boolean flag = !isAllowNullValues() && (value == null || value instanceof NullValue); 116 | if (flag) { 117 | return null; 118 | } 119 | Object result = this.cache.get(key, k -> toStoreValue(value)); 120 | return fromStoreValue(result); 121 | } 122 | 123 | public void evict(Object key) { 124 | log.debug("caffeine缓存 key={} 清除缓存", JSON.toJSONString(key)); 125 | this.cache.invalidate(key); 126 | } 127 | 128 | public void clear() { 129 | log.debug("caffeine缓存 key={} 清空缓存"); 130 | this.cache.invalidateAll(); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /multi-layering-cache-core/src/main/java/org/github/roger/cache/redis/RedisCache.java: -------------------------------------------------------------------------------- 1 | package org.github.roger.cache.redis; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import lombok.Getter; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.github.roger.cache.AbstractValueAdaptingCache; 7 | import org.github.roger.concurrent.RedisDistriLock; 8 | import org.github.roger.concurrent.ThreadTaskUtils; 9 | import org.github.roger.settings.SecondaryCacheSetting; 10 | import org.github.roger.support.AwaitThreadContainer; 11 | import org.github.roger.support.NullValue; 12 | import org.github.roger.utils.RedisCacheKey; 13 | import org.springframework.data.redis.core.RedisTemplate; 14 | import org.springframework.util.Assert; 15 | import org.springframework.util.CollectionUtils; 16 | 17 | import java.util.Set; 18 | import java.util.concurrent.Callable; 19 | import java.util.concurrent.TimeUnit; 20 | 21 | @Slf4j 22 | @Getter 23 | public class RedisCache extends AbstractValueAdaptingCache { 24 | /** 25 | * 刷新缓存重试次数 26 | */ 27 | private static final int RETRY_COUNT = 20; 28 | 29 | /** 30 | * 刷新缓存等待时间,单位毫秒 31 | */ 32 | private static final long WAIT_TIME = 20; 33 | 34 | /** 35 | * 等待线程容器 36 | */ 37 | private AwaitThreadContainer container = new AwaitThreadContainer(); 38 | 39 | /** 40 | * redis 客户端 即缓存对象 41 | */ 42 | private RedisTemplate redisTemplate; 43 | 44 | /** 45 | * 缓存有效时间,毫秒 46 | */ 47 | private long expiration; 48 | 49 | /** 50 | * 缓存在失效前主动强制刷新缓存的时间 51 | * 比如: 缓存有效时间是 10s 52 | * 主动刷新时间为 4s 53 | * 那么当缓存有效时间为5-10s时 获取缓存数据时,不会刷新缓存 54 | * 0-4s包含4s时 获取缓存数据时,会刷新缓存时间 55 | * 一种时软刷新,直接更新缓存的有效时间为10s 56 | * 一种是硬刷新,不只修改缓存的有效时间,还修改缓存的值 57 | * 单位:毫秒 58 | */ 59 | private long preloadTime = 0; 60 | 61 | /** 62 | * 是否强制刷新(执行被缓存的方法),默认是false 63 | */ 64 | private boolean forceRefresh = false; 65 | 66 | /** 67 | * 是否使用缓存名称作为 redis key 前缀 68 | */ 69 | private boolean usePrefix; 70 | 71 | /** 72 | * 是否允许为NULL 73 | */ 74 | private final boolean allowNullValues; 75 | 76 | /** 77 | * 非空值和null值之间的时间倍率,默认是1。allowNullValue=true才有效 78 | *

79 | * 如配置缓存的有效时间是200秒,倍率这设置成10, 80 | * 那么当缓存value为null时,缓存的有效时间将是20秒,非空时为200秒 81 | *

82 | */ 83 | private final int magnification; 84 | 85 | /** 86 | * @param name 缓存名称 87 | * @param redisTemplate redis客户端 redis 客户端 88 | * @param secondaryCacheSetting 二级缓存配置{@link SecondaryCacheSetting} 89 | */ 90 | public RedisCache(String name, RedisTemplate redisTemplate, SecondaryCacheSetting secondaryCacheSetting) { 91 | 92 | this(name, redisTemplate, secondaryCacheSetting.getTimeUnit().toMillis(secondaryCacheSetting.getExpiration()), 93 | secondaryCacheSetting.getTimeUnit().toMillis(secondaryCacheSetting.getPreloadTime()), 94 | secondaryCacheSetting.isForceRefresh(), secondaryCacheSetting.isUsePrefix(), 95 | secondaryCacheSetting.isAllowNullValue(), secondaryCacheSetting.getMagnification()); 96 | } 97 | 98 | /** 99 | * @param name 缓存名称 100 | * @param redisTemplate redis客户端 redis 客户端 101 | * @param expiration key的有效时间 102 | * @param preloadTime 缓存主动在失效前强制刷新缓存的时间 103 | * @param forceRefresh 是否强制刷新(执行被缓存的方法),默认是false 104 | * @param usePrefix 是否使用缓存名称作为前缀 105 | * @param allowNullValues 是否允许存NULL值,模式允许 106 | * @param magnification 非空值和null值之间的时间倍率 107 | */ 108 | public RedisCache(String name, RedisTemplate redisTemplate, long expiration, long preloadTime, 109 | boolean forceRefresh, boolean usePrefix, boolean allowNullValues, int magnification) { 110 | super(name); 111 | 112 | Assert.notNull(redisTemplate, "RedisTemplate 不能为NULL"); 113 | this.redisTemplate = redisTemplate; 114 | this.expiration = expiration; 115 | this.preloadTime = preloadTime; 116 | this.forceRefresh = forceRefresh; 117 | this.usePrefix = usePrefix; 118 | this.allowNullValues = allowNullValues; 119 | this.magnification = magnification; 120 | } 121 | 122 | @Override 123 | public boolean isAllowNullValues() { 124 | return this.allowNullValues; 125 | } 126 | 127 | @Override 128 | public RedisTemplate getRealCache() { 129 | return this.redisTemplate; 130 | } 131 | 132 | /** 133 | * 获取 RedisCacheKey 134 | * 135 | * @param key 缓存key 136 | * @return RedisCacheKey 137 | */ 138 | public RedisCacheKey getRedisCacheKey(Object key) { 139 | return new RedisCacheKey(key, redisTemplate.getKeySerializer()) 140 | .cacheName(getName()).usePrefix(usePrefix); 141 | } 142 | 143 | @Override 144 | public Object get(Object key) { 145 | 146 | RedisCacheKey redisCacheKey = getRedisCacheKey(key); 147 | 148 | log.debug("redis缓存 key= {} 查询redis缓存", redisCacheKey.getKey()); 149 | return redisTemplate.opsForValue().get(redisCacheKey.getKey()); 150 | } 151 | 152 | @Override 153 | public T get(Object key, Callable valueLoader) { 154 | RedisCacheKey redisCacheKey = getRedisCacheKey(key); 155 | log.debug("redis缓存 key= {} 查询redis缓存如果没有命中,从数据库获取数据", redisCacheKey.getKey()); 156 | // 先获取缓存,如果有直接返回 157 | Object result = redisTemplate.opsForValue().get(redisCacheKey.getKey()); 158 | if (result != null || redisTemplate.hasKey(redisCacheKey.getKey())) { 159 | // 刷新缓存 160 | refreshCache(redisCacheKey, valueLoader, result); 161 | return (T) fromStoreValue(result); 162 | } 163 | // 执行缓存方法 164 | return executeCacheMethod(redisCacheKey, valueLoader); 165 | } 166 | 167 | /** 168 | * 刷新缓存数据 169 | */ 170 | private void refreshCache(RedisCacheKey redisCacheKey, Callable valueLoader, Object result) { 171 | Long ttl = redisTemplate.getExpire(redisCacheKey.getKey()); 172 | Long preload = preloadTime; 173 | // 允许缓存NULL值,则自动刷新时间也要除以倍数 174 | boolean flag = isAllowNullValues() && (result instanceof NullValue || result == null); 175 | if (flag) { 176 | preload = preload / getMagnification(); 177 | } 178 | //这里的判断含义是:当前缓存的key还没到失效时间 179 | //并且是否满足强制刷新的时间 TimeUnit.SECONDS.toMillis(ttl) <= preload 180 | if (null != ttl && ttl > 0 && TimeUnit.SECONDS.toMillis(ttl) <= preload) { 181 | // 判断是否需要强制刷新在开启刷新线程 182 | if (!isForceRefresh()) { 183 | log.debug("redis缓存 key={} 软刷新缓存模式", redisCacheKey.getKey()); 184 | softRefresh(redisCacheKey); 185 | } else { 186 | log.debug("redis缓存 key={} 强刷新缓存模式", redisCacheKey.getKey()); 187 | forceRefresh(redisCacheKey, valueLoader); 188 | } 189 | } 190 | } 191 | 192 | /** 193 | * 软刷新,直接修改缓存时间 194 | * 195 | * @param redisCacheKey {@link RedisCacheKey} 196 | */ 197 | private void softRefresh(RedisCacheKey redisCacheKey) { 198 | // 加一个分布式锁,只放一个请求去刷新缓存 199 | RedisDistriLock redisLock = new RedisDistriLock(redisTemplate, redisCacheKey.getKey() + "_lock"); 200 | try { 201 | if (redisLock.tryLock()) { 202 | redisTemplate.expire(redisCacheKey.getKey(), this.expiration, TimeUnit.MILLISECONDS); 203 | } 204 | } catch (Exception e) { 205 | log.error(e.getMessage(), e); 206 | } finally { 207 | redisLock.unlock(); 208 | } 209 | } 210 | 211 | /** 212 | * 硬刷新(执行被缓存的方法) 213 | * 214 | * @param redisCacheKey {@link RedisCacheKey} 215 | * @param valueLoader 数据加载器 216 | */ 217 | private void forceRefresh(RedisCacheKey redisCacheKey, Callable valueLoader) { 218 | // 尽量少的去开启线程,因为线程池是有限的 219 | ThreadTaskUtils.run(() -> { 220 | // 加一个分布式锁,只放一个请求去刷新缓存 221 | RedisDistriLock redisLock = new RedisDistriLock(redisTemplate, redisCacheKey.getKey() + "_lock"); 222 | try { 223 | if (redisLock.lock()) { 224 | // 获取锁之后再判断一下过期时间,看是否需要加载数据 225 | Long ttl = redisTemplate.getExpire(redisCacheKey.getKey()); 226 | if (null != ttl && ttl > 0 && TimeUnit.SECONDS.toMillis(ttl) <= preloadTime) { 227 | // 加载数据并放到缓存 228 | loaderAndPutValue(redisCacheKey, valueLoader, false); 229 | } 230 | } 231 | } catch (Exception e) { 232 | log.error(e.getMessage(), e); 233 | } finally { 234 | redisLock.unlock(); 235 | } 236 | }); 237 | } 238 | 239 | /** 240 | * 加载并将数据放到redis缓存 241 | */ 242 | private T loaderAndPutValue(RedisCacheKey key, Callable valueLoader, boolean isLoad) { 243 | long start = System.currentTimeMillis(); 244 | 245 | try { 246 | // 加载数据 247 | Object result = putValue(key, valueLoader.call()); 248 | log.debug("redis缓存 key={} 执行被缓存的方法,并将其放入缓存, 耗时:{}。数据:{}", key.getKey(), System.currentTimeMillis() - start, JSON.toJSONString(result)); 249 | 250 | return (T) fromStoreValue(result); 251 | } catch (Exception e) { 252 | throw new LoaderCacheValueException(key.getKey(), e); 253 | } 254 | } 255 | 256 | private Object putValue(RedisCacheKey key, Object value) { 257 | Object result = toStoreValue(value); 258 | // redis 缓存不允许直接存NULL,如果结果返回NULL需要删除缓存 259 | if (result == null) { 260 | redisTemplate.delete(key.getKey()); 261 | return result; 262 | } 263 | // 不允许缓存NULL值,删除缓存 264 | if (!isAllowNullValues() && result instanceof NullValue) { 265 | redisTemplate.delete(key.getKey()); 266 | return result; 267 | } 268 | 269 | // 允许缓存NULL值 270 | long expirationTime = this.expiration; 271 | // 允许缓存NULL值且缓存为值为null时需要重新计算缓存时间 272 | if (isAllowNullValues() && result instanceof NullValue) { 273 | expirationTime = expirationTime / getMagnification(); 274 | } 275 | // 将数据放到缓存 276 | redisTemplate.opsForValue().set(key.getKey(), result, expirationTime, TimeUnit.MILLISECONDS); 277 | return result; 278 | } 279 | 280 | 281 | /** 282 | * 同一个线程循环5次查询缓存,每次等待20毫秒,如果还是没有数据直接去执行被缓存的方法 283 | */ 284 | private T executeCacheMethod(RedisCacheKey redisCacheKey, Callable valueLoader) { 285 | RedisDistriLock redisLock = new RedisDistriLock(redisTemplate, redisCacheKey.getKey() + "_sync_lock"); 286 | // 同一个线程循环20次查询缓存,每次等待20毫秒,如果还是没有数据直接去执行被缓存的方法 287 | for (int i = 0; i < RETRY_COUNT; i++) { 288 | try { 289 | // 先取缓存,如果有直接返回,没有再去做拿锁操作 290 | Object result = redisTemplate.opsForValue().get(redisCacheKey.getKey()); 291 | if (result != null) { 292 | log.debug("redis缓存 key= {} 获取到锁后查询查询缓存命中,不需要执行被缓存的方法", redisCacheKey.getKey()); 293 | return (T) fromStoreValue(result); 294 | } 295 | 296 | // 获取分布式锁去后台查询数据 297 | if (redisLock.lock()) { 298 | T t = loaderAndPutValue(redisCacheKey, valueLoader, true); 299 | log.debug("redis缓存 key= {} 从数据库获取数据完毕,唤醒所有等待线程", redisCacheKey.getKey()); 300 | // 唤醒线程 301 | container.signalAll(redisCacheKey.getKey()); 302 | return t; 303 | } 304 | // 线程等待 305 | log.debug("redis缓存 key= {} 从数据库获取数据未获取到锁,进入等待状态,等待{}毫秒", redisCacheKey.getKey(), WAIT_TIME); 306 | container.await(redisCacheKey.getKey(), WAIT_TIME); 307 | } catch (Exception e) { 308 | container.signalAll(redisCacheKey.getKey()); 309 | throw new LoaderCacheValueException(redisCacheKey.getKey(), e); 310 | } finally { 311 | redisLock.unlock(); 312 | } 313 | } 314 | log.debug("redis缓存 key={} 等待{}次,共{}毫秒,任未获取到缓存,直接去执行被缓存的方法", redisCacheKey.getKey(), RETRY_COUNT, RETRY_COUNT * WAIT_TIME, WAIT_TIME); 315 | return loaderAndPutValue(redisCacheKey, valueLoader, true); 316 | } 317 | 318 | @Override 319 | public void put(Object key, Object value) { 320 | RedisCacheKey redisCacheKey = getRedisCacheKey(key); 321 | log.debug("redis缓存 key= {} put缓存,缓存值:{}", redisCacheKey.getKey(), JSON.toJSONString(value)); 322 | putValue(redisCacheKey, value); 323 | } 324 | 325 | /** 326 | * 缓存key中已经有缓存对象,则不在缓存,直接返回缓存的对象 327 | * 如果缓存key在缓存中不存在,则把新对象缓存,并返回null 328 | * @param key 缓存key 329 | * @param value 缓存key对应的值 330 | * @return 331 | */ 332 | @Override 333 | public Object putIfAbsent(Object key, Object value) { 334 | log.debug("redis缓存 key= {} putIfAbsent缓存,缓存值:{}", getRedisCacheKey(key).getKey(), JSON.toJSONString(value)); 335 | Object result = get(key); 336 | if (result != null) { 337 | return result; 338 | } 339 | put(key, value); 340 | return null; 341 | } 342 | 343 | @Override 344 | public void evict(Object key) { 345 | RedisCacheKey redisCacheKey = getRedisCacheKey(key); 346 | log.info("清除redis缓存 key= {} ", redisCacheKey.getKey()); 347 | redisTemplate.delete(redisCacheKey.getKey()); 348 | } 349 | 350 | @Override 351 | public void clear() { 352 | // 必须开启了使用缓存名称作为前缀,clear才有效 353 | if (usePrefix) { 354 | log.info("清空redis缓存 ,缓存前缀为{}", getName()); 355 | Set keys = redisTemplate.keys(getName() + "*"); 356 | if (!CollectionUtils.isEmpty(keys)) { 357 | redisTemplate.delete(keys); 358 | } 359 | } 360 | } 361 | } 362 | -------------------------------------------------------------------------------- /multi-layering-cache-core/src/main/java/org/github/roger/concurrent/MdcThreadPoolTaskExecutor.java: -------------------------------------------------------------------------------- 1 | package org.github.roger.concurrent; 2 | 3 | import org.slf4j.MDC; 4 | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; 5 | 6 | import java.util.Map; 7 | 8 | /** 9 | * 这是{@link ThreadPoolTaskExecutor}的一个简单替换,可以在每个任务之前设置子线程的MDC数据。 10 | *

11 | * 在记录日志的时候,一般情况下我们会使用MDC来存储每个线程的特有参数,如身份信息等,以便更好的查询日志。 12 | * 但是Logback在最新的版本中因为性能问题,不会自动的将MDC的内存传给子线程。所以Logback建议在执行异步线程前 13 | * 先通过MDC.getCopyOfContextMap()方法将MDC内存获取出来,再传给线程。 14 | * 并在子线程的执行的最开始调用MDC.setContextMap(context)方法将父线程的MDC内容传给子线程。 15 | *

16 | * https://logback.qos.ch/manual/mdc.html 17 | * 18 | */ 19 | public class MdcThreadPoolTaskExecutor extends ThreadPoolTaskExecutor { 20 | 21 | /** 22 | * 所有线程都会委托给这个execute方法,在这个方法中我们把父线程的MDC内容赋值给子线程 23 | * https://logback.qos.ch/manual/mdc.html#managedThreads 24 | * 25 | * @param runnable runnable 26 | */ 27 | @Override 28 | public void execute(Runnable runnable) { 29 | // 获取父线程MDC中的内容,必须在run方法之前,否则等异步线程执行的时候有可能MDC里面的值已经被清空了,这个时候就会返回null 30 | Map context = MDC.getCopyOfContextMap(); 31 | super.execute(() -> run(runnable, context)); 32 | } 33 | 34 | /** 35 | * 子线程委托的执行方法 36 | * 37 | * @param runnable {@link Runnable} 38 | * @param context 父线程MDC内容 39 | */ 40 | private void run(Runnable runnable, Map context) { 41 | // 将父线程的MDC内容传给子线程 42 | if (context != null) { 43 | try { 44 | MDC.setContextMap(context); 45 | } catch (Exception e) { 46 | logger.error(e.getMessage(), e); 47 | } 48 | } 49 | try { 50 | // 执行异步操作 51 | runnable.run(); 52 | } finally { 53 | // 清空MDC内容 54 | MDC.clear(); 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /multi-layering-cache-core/src/main/java/org/github/roger/concurrent/RedisDistriLock.java: -------------------------------------------------------------------------------- 1 | package org.github.roger.concurrent; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.data.redis.core.RedisTemplate; 6 | import org.springframework.data.redis.core.script.RedisScript; 7 | import org.springframework.util.Assert; 8 | import org.springframework.util.StringUtils; 9 | 10 | import java.util.*; 11 | import java.util.concurrent.TimeUnit; 12 | 13 | /** 14 | * Redis分布式锁 15 | * 使用 SET resource-name anystring NX EX max-lock-time 实现 16 | *

17 | * 该方案在 Redis 官方 SET 命令页有详细介绍。 18 | * http://doc.redisfans.com/string/set.html 19 | *

20 | * 在介绍该分布式锁设计之前,我们先来看一下在从 Redis 2.6.12 开始 SET 提供的新特性, 21 | * 命令 SET key value [EX seconds] [PX milliseconds] [NX|XX],其中: 22 | *

23 | * EX seconds — 以秒为单位设置 key 的过期时间; 24 | * PX milliseconds — 以毫秒为单位设置 key 的过期时间; 25 | * NX — 将key 的值设为value ,当且仅当key 不存在,等效于 SETNX。 26 | * XX — 将key 的值设为value ,当且仅当key 存在,等效于 SETEX。 27 | *

28 | * 命令 SET resource-name anystring NX EX max-lock-time 是一种在 Redis 中实现锁的简单方法。 29 | *

30 | * 客户端执行以上的命令: 31 | *

32 | * 如果服务器返回 OK ,那么这个客户端获得锁。 33 | * 如果服务器返回 NIL ,那么客户端获取锁失败,可以在稍后再重试。 34 | * 35 | */ 36 | public class RedisDistriLock { 37 | 38 | private static Logger logger = LoggerFactory.getLogger(RedisDistriLock.class); 39 | 40 | private RedisTemplate redisTemplate; 41 | 42 | /** 43 | * 调用set后的返回值 44 | */ 45 | private static final String OK = "OK"; 46 | 47 | /** 48 | * 默认请求锁的超时时间(ms 毫秒) 49 | */ 50 | private static final long TIME_OUT = 100; 51 | 52 | /** 53 | * 默认锁的有效时间(s) 54 | */ 55 | private static final int EXPIRE = 60; 56 | 57 | /** 58 | * 解锁的lua脚本 59 | */ 60 | private static final String UNLOCK_LUA; 61 | 62 | static { 63 | UNLOCK_LUA = "if redis.call(\"get\",KEYS[1]) == ARGV[1] " 64 | + "then " 65 | + " return redis.call(\"del\",KEYS[1]) " 66 | + "else " 67 | + " return 0 " 68 | + "end "; 69 | } 70 | 71 | /** 72 | * 锁标志对应的key 73 | */ 74 | private String lockKey; 75 | 76 | /** 77 | * 记录到日志的锁标志对应的key 78 | */ 79 | private String lockKeyLog = ""; 80 | 81 | /** 82 | * 锁对应的值 83 | */ 84 | private String lockValue; 85 | 86 | /** 87 | * 锁的有效时间(s) 88 | */ 89 | private int expireTime = EXPIRE; 90 | 91 | /** 92 | * 请求锁的超时时间(ms) 93 | */ 94 | private long timeOut = TIME_OUT; 95 | 96 | /** 97 | * 锁标记 98 | */ 99 | private volatile boolean locked = false; 100 | 101 | private final Random random = new Random(); 102 | 103 | /** 104 | * 使用默认的锁过期时间和请求锁的超时时间 105 | * 106 | * @param redisTemplate redis客户端 107 | * @param lockKey 锁的key(Redis的Key) 108 | */ 109 | public RedisDistriLock(RedisTemplate redisTemplate, String lockKey) { 110 | this.redisTemplate = redisTemplate; 111 | this.lockKey = lockKey + "_lock"; 112 | } 113 | 114 | /** 115 | * 使用默认的请求锁的超时时间,指定锁的过期时间 116 | * 117 | * @param redisTemplate redis客户端 118 | * @param lockKey 锁的key(Redis的Key) 119 | * @param expireTime 锁的过期时间(单位:秒) 120 | */ 121 | public RedisDistriLock(RedisTemplate redisTemplate, String lockKey, int expireTime) { 122 | this(redisTemplate, lockKey); 123 | this.expireTime = expireTime; 124 | } 125 | 126 | /** 127 | * 使用默认的锁的过期时间,指定请求锁的超时时间 128 | * 129 | * @param redisTemplate redis客户端 130 | * @param lockKey 锁的key(Redis的Key) 131 | * @param timeOut 请求锁的超时时间(单位:毫秒) 132 | */ 133 | public RedisDistriLock(RedisTemplate redisTemplate, String lockKey, long timeOut) { 134 | this(redisTemplate, lockKey); 135 | this.timeOut = timeOut; 136 | } 137 | 138 | /** 139 | * 锁的过期时间和请求锁的超时时间都是用指定的值 140 | * 141 | * @param redisTemplate redis客户端 142 | * @param lockKey 锁的key(Redis的Key) 143 | * @param expireTime 锁的过期时间(单位:秒) 144 | * @param timeOut 请求锁的超时时间(单位:毫秒) 145 | */ 146 | public RedisDistriLock(RedisTemplate redisTemplate, String lockKey, int expireTime, long timeOut) { 147 | this(redisTemplate, lockKey, expireTime); 148 | this.timeOut = timeOut; 149 | } 150 | 151 | /** 152 | * 尝试获取锁 超时返回 153 | * 154 | * @return boolean 155 | */ 156 | public boolean tryLock() { 157 | // 生成随机key 158 | this.lockValue = UUID.randomUUID().toString(); 159 | // 请求锁超时时间,纳秒 160 | long timeout = timeOut * 1000000; 161 | // 系统当前时间,纳秒 162 | long nowTime = System.nanoTime(); 163 | while ((System.nanoTime() - nowTime) < timeout) { 164 | if (this.set(lockKey, lockValue, expireTime)) { 165 | locked = true; 166 | // 上锁成功结束请求 167 | return locked; 168 | } 169 | 170 | // 每次请求等待一段时间 171 | seleep(10, 50000); 172 | } 173 | return locked; 174 | } 175 | 176 | /** 177 | * 尝试获取锁 立即返回 178 | * 179 | * @return 是否成功获得锁 180 | */ 181 | public boolean lock() { 182 | this.lockValue = UUID.randomUUID().toString(); 183 | //不存在则添加 且设置过期时间(单位ms) 184 | locked = set(lockKey, lockValue, expireTime); 185 | return locked; 186 | } 187 | 188 | /** 189 | * 以阻塞方式的获取锁 190 | * 191 | * @return 是否成功获得锁 192 | */ 193 | public boolean lockBlock() { 194 | this.lockValue = UUID.randomUUID().toString(); 195 | while (true) { 196 | //不存在则添加 且设置过期时间(单位ms) 197 | locked = set(lockKey, lockValue, expireTime); 198 | if (locked) { 199 | return locked; 200 | } 201 | // 每次请求等待一段时间 202 | seleep(10, 50000); 203 | } 204 | } 205 | 206 | /** 207 | * 解锁 208 | *

209 | * 可以通过以下修改,让这个锁实现更健壮: 210 | *

211 | * 不使用固定的字符串作为键的值,而是设置一个不可猜测(non-guessable)的长随机字符串,作为口令串(token)。 212 | * 不使用 DEL 命令来释放锁,而是发送一个 Lua 脚本,这个脚本只在客户端传入的值和键的口令串相匹配时,才对键进行删除。 213 | * 这两个改动可以防止持有过期锁的客户端误删现有锁的情况出现。 214 | * 215 | * @return Boolean 216 | */ 217 | public Boolean unlock() { 218 | // 只有加锁成功并且锁还有效才去释放锁 219 | // 只有加锁成功并且锁还有效才去释放锁 220 | if (locked) { 221 | try { 222 | RedisScript script = RedisScript.of(UNLOCK_LUA, Long.class); 223 | List keys = new ArrayList<>(); 224 | keys.add(lockKey); 225 | Long result = redisTemplate.execute(script, keys, lockValue); 226 | if (result == 0 && !StringUtils.isEmpty(lockKeyLog)) { 227 | logger.debug("Redis分布式锁,解锁{}失败!解锁时间:{}", lockKeyLog, System.currentTimeMillis()); 228 | } 229 | 230 | locked = result == 0; 231 | return result == 1; 232 | } catch (Throwable e) { 233 | logger.warn("Redis不支持EVAL命令,使用降级方式解锁:{}", e.getMessage()); 234 | String value = this.get(lockKey, String.class); 235 | if (lockValue.equals(value)) { 236 | redisTemplate.delete(lockKey); 237 | return true; 238 | } 239 | return false; 240 | } 241 | } 242 | 243 | return true; 244 | } 245 | 246 | /** 247 | * 重写redisTemplate的set方法 248 | *

249 | * 命令 SET resource-name anystring NX EX max-lock-time 是一种在 Redis 中实现锁的简单方法。 250 | *

251 | * 客户端执行以上的命令: 252 | *

253 | * 如果服务器返回 OK ,那么这个客户端获得锁。 254 | * 如果服务器返回 NIL ,那么客户端获取锁失败,可以在稍后再重试。 255 | * 256 | * @param key 锁的Key 257 | * @param value 锁里面的值 258 | * @param seconds 过去时间(秒) 259 | * @return String 260 | */ 261 | private boolean set(final String key, final String value, final long seconds) { 262 | Assert.isTrue(!StringUtils.isEmpty(key), "key不能为空"); 263 | Boolean success = redisTemplate.opsForValue().setIfAbsent(key, value, seconds, TimeUnit.SECONDS); 264 | if (!StringUtils.isEmpty(lockKeyLog) && Objects.nonNull(success) && success) { 265 | logger.debug("获取锁{}的时间:{}", lockKeyLog, System.currentTimeMillis()); 266 | } 267 | return Objects.nonNull(success) && success; 268 | } 269 | 270 | /** 271 | * 获取redis里面的值 272 | * 273 | * @param key key 274 | * @param aClass class 275 | * @return T 276 | */ 277 | private T get(final String key, Class aClass) { 278 | Assert.isTrue(!StringUtils.isEmpty(key), "key不能为空"); 279 | return (T) redisTemplate.opsForValue().get(key); 280 | } 281 | 282 | /** 283 | * 获取锁状态 284 | * 285 | * @return boolean 286 | * @author yuhao.wang 287 | */ 288 | public boolean isLock() { 289 | 290 | return locked; 291 | } 292 | 293 | /** 294 | * @param millis 毫秒 295 | * @param nanos 纳秒 296 | * @Title: seleep 297 | * @Description: 线程等待时间 298 | * @author yuhao.wang 299 | */ 300 | private void seleep(long millis, int nanos) { 301 | try { 302 | Thread.sleep(millis, random.nextInt(nanos)); 303 | } catch (Exception e) { 304 | logger.debug("获取分布式锁休眠被中断:", e); 305 | } 306 | } 307 | 308 | public String getLockKeyLog() { 309 | return lockKeyLog; 310 | } 311 | 312 | public void setLockKeyLog(String lockKeyLog) { 313 | this.lockKeyLog = lockKeyLog; 314 | } 315 | 316 | public int getExpireTime() { 317 | return expireTime; 318 | } 319 | 320 | public void setExpireTime(int expireTime) { 321 | this.expireTime = expireTime; 322 | } 323 | 324 | public long getTimeOut() { 325 | return timeOut; 326 | } 327 | 328 | public void setTimeOut(long timeOut) { 329 | this.timeOut = timeOut; 330 | } 331 | } -------------------------------------------------------------------------------- /multi-layering-cache-core/src/main/java/org/github/roger/concurrent/ThreadTaskUtils.java: -------------------------------------------------------------------------------- 1 | package org.github.roger.concurrent; 2 | 3 | import java.util.concurrent.ThreadPoolExecutor; 4 | 5 | /** 6 | * 线程池 7 | * 8 | * @author yuhao.wang3 9 | */ 10 | public class ThreadTaskUtils { 11 | private static MdcThreadPoolTaskExecutor taskExecutor = null; 12 | 13 | static { 14 | taskExecutor = new MdcThreadPoolTaskExecutor(); 15 | // 核心线程数 16 | taskExecutor.setCorePoolSize(8); 17 | // 最大线程数 18 | taskExecutor.setMaxPoolSize(64); 19 | // 队列最大长度 20 | taskExecutor.setQueueCapacity(1000); 21 | // 线程池维护线程所允许的空闲时间(单位秒) 22 | taskExecutor.setKeepAliveSeconds(120); 23 | /* 24 | * 线程池对拒绝任务(无限程可用)的处理策略 25 | * ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 26 | * ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。 27 | * ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程) 28 | * ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务,如果执行器已关闭,则丢弃. 29 | */ 30 | taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy()); 31 | 32 | taskExecutor.initialize(); 33 | } 34 | 35 | public static void run(Runnable runnable) { 36 | taskExecutor.execute(runnable); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /multi-layering-cache-core/src/main/java/org/github/roger/enumeration/ExpireMode.java: -------------------------------------------------------------------------------- 1 | package org.github.roger.enumeration; 2 | 3 | public enum ExpireMode { 4 | 5 | /** 6 | * 每写入一次重新计算一次缓存的有效时间 7 | */ 8 | WRITE("计算到期时间的标准是:距离最后一次写入时间到期时失效"), 9 | 10 | /** 11 | * 每访问一次重新计算一次缓存的有效时间 12 | */ 13 | ACCESS("计算到期时间的标准是:距离最后一次访问时间到期时失效"); 14 | 15 | private String label; 16 | 17 | ExpireMode(String label) { 18 | this.label = label; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /multi-layering-cache-core/src/main/java/org/github/roger/enumeration/RedisPubSubMessageType.java: -------------------------------------------------------------------------------- 1 | package org.github.roger.enumeration; 2 | 3 | import lombok.Getter; 4 | 5 | @Getter 6 | public enum RedisPubSubMessageType { 7 | 8 | /** 9 | * 删除缓存 10 | */ 11 | EVICT("删除缓存"), 12 | 13 | /** 14 | * 清空缓存 15 | */ 16 | CLEAR("清空缓存"); 17 | 18 | private String label; 19 | 20 | RedisPubSubMessageType(String label) { 21 | this.label = label; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /multi-layering-cache-core/src/main/java/org/github/roger/enumeration/Type.java: -------------------------------------------------------------------------------- 1 | package org.github.roger.enumeration; 2 | 3 | /** 4 | * 对象类型 5 | * 6 | */ 7 | public enum Type { 8 | /** 9 | * null 10 | */ 11 | NULL("null"), 12 | 13 | /** 14 | * string 15 | */ 16 | STRING("string"), 17 | 18 | /** 19 | * object 20 | */ 21 | OBJECT("Object 对象"), 22 | 23 | /** 24 | * List集合 25 | */ 26 | LIST("List集合"), 27 | 28 | /** 29 | * Set集合 30 | */ 31 | SET("Set集合"), 32 | 33 | /** 34 | * 数组 35 | */ 36 | ARRAY("数组"), 37 | 38 | /** 39 | * 枚举 40 | */ 41 | ENUM("枚举"), 42 | 43 | /** 44 | * 其他类型 45 | */ 46 | OTHER("其他类型"); 47 | 48 | private String label; 49 | 50 | Type(String label) { 51 | this.label = label; 52 | } 53 | 54 | public static Type parse(String name) { 55 | for (Type type : Type.values()) { 56 | if (type.name().equals(name)) { 57 | return type; 58 | } 59 | } 60 | return OTHER; 61 | } 62 | } -------------------------------------------------------------------------------- /multi-layering-cache-core/src/main/java/org/github/roger/exception/NestedRuntimeException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2002-2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.github.roger.exception; 18 | 19 | import org.springframework.core.NestedCheckedException; 20 | import org.springframework.core.NestedExceptionUtils; 21 | 22 | /** 23 | * Handy class for wrapping runtime {@code Exceptions} with a root cause. 24 | * 25 | *

This class is {@code abstract} to force the programmer to extend 26 | * the class. {@code getMessage} will include nested exception 27 | * information; {@code printStackTrace} and other like methods will 28 | * delegate to the wrapped exception, if any. 29 | * 30 | *

The similarity between this class and the {@link NestedCheckedException} 31 | * class is unavoidable, as Java forces these two classes to have different 32 | * superclasses (ah, the inflexibility of concrete inheritance!). 33 | * 34 | * @author Rod Johnson 35 | * @author Juergen Hoeller 36 | * @see #getMessage 37 | * @see #printStackTrace 38 | * @see NestedCheckedException 39 | */ 40 | public abstract class NestedRuntimeException extends RuntimeException { 41 | 42 | /** Use serialVersionUID from Spring 1.2 for interoperability */ 43 | private static final long serialVersionUID = 5439915454935047936L; 44 | 45 | static { 46 | // Eagerly load the NestedExceptionUtils class to avoid classloader deadlock 47 | // issues on OSGi when calling getMessage(). Reported by Don Brown; SPR-5607. 48 | NestedExceptionUtils.class.getName(); 49 | } 50 | 51 | 52 | /** 53 | * Construct a {@code NestedRuntimeException} with the specified detail message. 54 | * @param msg the detail message 55 | */ 56 | public NestedRuntimeException(String msg) { 57 | super(msg); 58 | } 59 | 60 | /** 61 | * Construct a {@code NestedRuntimeException} with the specified detail message 62 | * and nested exception. 63 | * @param msg the detail message 64 | * @param cause the nested exception 65 | */ 66 | public NestedRuntimeException(String msg, Throwable cause) { 67 | super(msg, cause); 68 | } 69 | 70 | 71 | /** 72 | * Return the detail message, including the message from the nested exception 73 | * if there is one. 74 | */ 75 | @Override 76 | public String getMessage() { 77 | return NestedExceptionUtils.buildMessage(super.getMessage(), getCause()); 78 | } 79 | 80 | 81 | /** 82 | * Retrieve the innermost cause of this exception, if any. 83 | * @return the innermost exception, or {@code null} if none 84 | * @since 2.0 85 | */ 86 | public Throwable getRootCause() { 87 | return NestedExceptionUtils.getRootCause(this); 88 | } 89 | 90 | /** 91 | * Retrieve the most specific cause of this exception, that is, 92 | * either the innermost cause (root cause) or this exception itself. 93 | *

Differs from {@link #getRootCause()} in that it falls back 94 | * to the present exception if there is no root cause. 95 | * @return the most specific cause (never {@code null}) 96 | * @since 2.0.3 97 | */ 98 | public Throwable getMostSpecificCause() { 99 | Throwable rootCause = getRootCause(); 100 | return (rootCause != null ? rootCause : this); 101 | } 102 | 103 | /** 104 | * Check whether this exception contains an exception of the given type: 105 | * either it is of the given class itself or it contains a nested cause 106 | * of the given type. 107 | * @param exType the exception type to look for 108 | * @return whether there is a nested exception of the specified type 109 | */ 110 | public boolean contains(Class exType) { 111 | if (exType == null) { 112 | return false; 113 | } 114 | if (exType.isInstance(this)) { 115 | return true; 116 | } 117 | Throwable cause = getCause(); 118 | if (cause == this) { 119 | return false; 120 | } 121 | if (cause instanceof NestedRuntimeException) { 122 | return ((NestedRuntimeException) cause).contains(exType); 123 | } 124 | else { 125 | while (cause != null) { 126 | if (exType.isInstance(cause)) { 127 | return true; 128 | } 129 | if (cause.getCause() == cause) { 130 | break; 131 | } 132 | cause = cause.getCause(); 133 | } 134 | return false; 135 | } 136 | } 137 | 138 | } 139 | -------------------------------------------------------------------------------- /multi-layering-cache-core/src/main/java/org/github/roger/exception/SerializationException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011-2013 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.github.roger.exception; 17 | 18 | 19 | /** 20 | * Generic exception indicating a serialization/deserialization error. 21 | * 22 | * @author Costin Leau 23 | */ 24 | public class SerializationException extends NestedRuntimeException { 25 | 26 | /** 27 | * Constructs a new SerializationException instance. 28 | * 29 | * @param msg msg 30 | * @param cause 原因 31 | */ 32 | public SerializationException(String msg, Throwable cause) { 33 | super(msg, cause); 34 | } 35 | 36 | /** 37 | * Constructs a new SerializationException instance. 38 | * 39 | * @param msg msg 40 | */ 41 | public SerializationException(String msg) { 42 | super(msg); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /multi-layering-cache-core/src/main/java/org/github/roger/listener/RedisMessageListener.java: -------------------------------------------------------------------------------- 1 | package org.github.roger.listener; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import lombok.Setter; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.github.roger.MultiLayeringCache; 7 | import org.github.roger.cache.ICache; 8 | import org.github.roger.manager.AbstractCacheManager; 9 | import org.github.roger.message.RedisPubSubMessage; 10 | import org.springframework.data.redis.connection.Message; 11 | import org.springframework.data.redis.listener.adapter.MessageListenerAdapter; 12 | 13 | import java.util.Collection; 14 | 15 | @Setter 16 | @Slf4j 17 | public class RedisMessageListener extends MessageListenerAdapter { 18 | 19 | /** 20 | * 缓存管理器 21 | */ 22 | private AbstractCacheManager cacheManager; 23 | 24 | 25 | @Override 26 | public void onMessage(Message message, byte[] pattern) { 27 | super.onMessage(message, pattern); 28 | // 解析订阅发布的信息,获取缓存的名称和缓存的key 29 | RedisPubSubMessage redisPubSubMessage = (RedisPubSubMessage) cacheManager.getRedisTemplate() 30 | .getValueSerializer().deserialize(message.getBody()); 31 | log.debug("redis消息订阅者接收到频道【{}】发布的消息。消息内容:{}", new String(message.getChannel()), JSON.toJSONString(redisPubSubMessage)); 32 | 33 | // 根据缓存名称获取多级缓存,可能有多个 34 | Collection caches = cacheManager.getCache(redisPubSubMessage.getCacheName()); 35 | for (ICache cache : caches) { 36 | // 判断缓存是否是多级缓存 37 | if (cache != null && cache instanceof MultiLayeringCache) { 38 | switch (redisPubSubMessage.getMessageType()) { 39 | case EVICT: 40 | // 获取一级缓存,并删除一级缓存数据 41 | ((MultiLayeringCache) cache).getFirstCache().evict(redisPubSubMessage.getKey()); 42 | log.info("删除一级缓存{}数据,key={}", redisPubSubMessage.getCacheName(), redisPubSubMessage.getKey()); 43 | break; 44 | 45 | case CLEAR: 46 | // 获取一级缓存,并删除一级缓存数据 47 | ((MultiLayeringCache) cache).getFirstCache().clear(); 48 | log.info("清除一级缓存{}数据", redisPubSubMessage.getCacheName()); 49 | break; 50 | 51 | default: 52 | log.error("接收到没有定义的订阅消息频道数据"); 53 | break; 54 | } 55 | 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /multi-layering-cache-core/src/main/java/org/github/roger/listener/RedisPublisher.java: -------------------------------------------------------------------------------- 1 | package org.github.roger.listener; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.data.redis.core.RedisTemplate; 5 | import org.springframework.data.redis.listener.ChannelTopic; 6 | 7 | @Slf4j 8 | public class RedisPublisher { 9 | 10 | private RedisPublisher() { 11 | } 12 | 13 | /** 14 | * 发布消息到频道(Channel) 15 | * 16 | * @param redisTemplate redis客户端 17 | * @param channelTopic 发布预订阅的频道 18 | * @param message 消息内容 19 | */ 20 | public static void publisher(RedisTemplate redisTemplate, ChannelTopic channelTopic, Object message) { 21 | redisTemplate.convertAndSend(channelTopic.toString(), message); 22 | log.debug("redis消息发布者向频道【{}】发布了【{}】消息", channelTopic.toString(), message.toString()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /multi-layering-cache-core/src/main/java/org/github/roger/manager/AbstractCacheManager.java: -------------------------------------------------------------------------------- 1 | package org.github.roger.manager; 2 | 3 | import lombok.Getter; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.github.roger.cache.ICache; 6 | import org.github.roger.listener.RedisMessageListener; 7 | import org.github.roger.settings.MultiLayeringCacheSetting; 8 | import org.springframework.beans.factory.DisposableBean; 9 | import org.springframework.beans.factory.InitializingBean; 10 | import org.springframework.context.SmartLifecycle; 11 | import org.springframework.data.redis.core.RedisTemplate; 12 | import org.springframework.data.redis.listener.ChannelTopic; 13 | import org.springframework.data.redis.listener.RedisMessageListenerContainer; 14 | import org.springframework.util.CollectionUtils; 15 | 16 | import java.util.Collection; 17 | import java.util.Collections; 18 | import java.util.LinkedHashSet; 19 | import java.util.Set; 20 | import java.util.concurrent.ConcurrentHashMap; 21 | import java.util.concurrent.ConcurrentMap; 22 | 23 | @Slf4j 24 | public abstract class AbstractCacheManager implements ICacheManager,InitializingBean,DisposableBean,SmartLifecycle { 25 | 26 | /** 27 | * redis pub/sub 容器 28 | */ 29 | private final RedisMessageListenerContainer container = new RedisMessageListenerContainer(); 30 | 31 | /** 32 | * redis pub/sub 监听器 33 | */ 34 | private final RedisMessageListener messageListener = new RedisMessageListener(); 35 | 36 | 37 | /** 38 | * 缓存容器 39 | * 外层key是cache_name 40 | * 里层key是[一级缓存有效时间-二级缓存有效时间-二级缓存自动刷新时间] 41 | */ 42 | private final ConcurrentMap> cacheContainer = new ConcurrentHashMap<>(16); 43 | 44 | /** 45 | * 缓存名称容器 46 | */ 47 | private volatile Set cacheNames = new LinkedHashSet<>(); 48 | 49 | /** 50 | * CacheManager 容器 51 | */ 52 | protected static Set cacheManagers = new LinkedHashSet<>(); 53 | 54 | /** 55 | * redis 客户端 56 | */ 57 | @Getter 58 | protected RedisTemplate redisTemplate; 59 | 60 | public static Set getCacheManager() { 61 | return cacheManagers; 62 | } 63 | 64 | @Override 65 | public Collection getCache(String name) { 66 | ConcurrentMap cacheMap = this.cacheContainer.get(name); 67 | if (CollectionUtils.isEmpty(cacheMap)) { 68 | return Collections.emptyList(); 69 | } 70 | return cacheMap.values(); 71 | } 72 | 73 | @Override 74 | public Collection getCacheNames() { 75 | return this.cacheNames; 76 | } 77 | 78 | // Lazy cache initialization on access 79 | @Override 80 | public ICache getCache(String name, MultiLayeringCacheSetting multiLayeringCacheSetting) { 81 | // 第一次获取缓存Cache,如果有直接返回,如果没有加锁往容器里里面放Cache 82 | ConcurrentMap cacheMap = this.cacheContainer.get(name); 83 | if (!CollectionUtils.isEmpty(cacheMap)) { 84 | if (cacheMap.size() > 1) { 85 | log.warn("缓存名称为 {} 的缓存,存在两个不同的过期时间配置,请一定注意保证缓存的key唯一性,否则会出现缓存过期时间错乱的情况", name); 86 | } 87 | ICache iCache = cacheMap.get(multiLayeringCacheSetting.getInternalKey()); 88 | if (iCache != null) { 89 | return iCache; 90 | } 91 | } 92 | 93 | // 第二次获取缓存Cache,加锁往容器里里面放Cache 94 | synchronized (this.cacheContainer) { 95 | cacheMap = this.cacheContainer.get(name); 96 | if (!CollectionUtils.isEmpty(cacheMap)) { 97 | // 从容器中获取缓存 98 | ICache iCache = cacheMap.get(multiLayeringCacheSetting.getInternalKey()); 99 | if (iCache != null) { 100 | return iCache; 101 | } 102 | } else { 103 | cacheMap = new ConcurrentHashMap<>(16); 104 | cacheContainer.put(name, cacheMap); 105 | // 更新缓存名称 106 | updateCacheNames(name); 107 | // 创建redis监听 108 | addMessageListener(name); 109 | } 110 | 111 | // 新建一个Cache对象 112 | ICache iCache = getMissingCache(name, multiLayeringCacheSetting); 113 | if (iCache != null) { 114 | // 装饰Cache对象 115 | iCache = decorateCache(iCache); 116 | // 将新的Cache对象放到容器 117 | cacheMap.put(multiLayeringCacheSetting.getInternalKey(), iCache); 118 | //同一缓存名称,缓存的过期时间设置要唯一 119 | if (cacheMap.size() > 1) { 120 | log.warn("缓存名称为 {} 的缓存,存在两个不同的过期时间配置,请一定注意保证缓存的key唯一性,否则会出现缓存过期时间错乱的情况", name); 121 | } 122 | } 123 | 124 | return iCache; 125 | } 126 | } 127 | 128 | /** 129 | * 根据缓存名称在CacheManager中没有找到对应Cache时,通过该方法新建一个对应的Cache实例 130 | * 131 | * @param name 缓存名称 132 | * @param multiLayeringCacheSetting 缓存配置 133 | * @return {@link ICache} 134 | */ 135 | protected abstract ICache getMissingCache(String name, MultiLayeringCacheSetting multiLayeringCacheSetting); 136 | 137 | /** 138 | * 更新缓存名称容器 139 | * 140 | * @param name 需要添加的缓存名称 141 | */ 142 | private void updateCacheNames(String name) { 143 | cacheNames.add(name); 144 | } 145 | 146 | /** 147 | * 获取Cache对象的装饰示例 148 | * 149 | * @param iCache 需要添加到CacheManager的Cache实例 150 | * @return 装饰过后的Cache实例 151 | */ 152 | protected ICache decorateCache(ICache iCache) { 153 | return iCache; 154 | } 155 | 156 | /** 157 | * 添加消息监听 158 | * 159 | * @param name 缓存名称 160 | */ 161 | protected void addMessageListener(String name) { 162 | container.addMessageListener(messageListener, new ChannelTopic(name)); 163 | } 164 | 165 | @Override 166 | public void afterPropertiesSet() throws Exception { 167 | messageListener.setCacheManager(this); 168 | container.setConnectionFactory(getRedisTemplate().getConnectionFactory()); 169 | container.afterPropertiesSet(); 170 | messageListener.afterPropertiesSet(); 171 | } 172 | 173 | @Override 174 | public void destroy() throws Exception { 175 | container.destroy(); 176 | } 177 | 178 | @Override 179 | public boolean isAutoStartup() { 180 | return container.isAutoStartup(); 181 | } 182 | 183 | @Override 184 | public void stop(Runnable callback) { 185 | container.stop(callback); 186 | } 187 | 188 | @Override 189 | public void start() { 190 | container.start(); 191 | } 192 | 193 | @Override 194 | public void stop() { 195 | container.stop(); 196 | } 197 | 198 | @Override 199 | public boolean isRunning() { 200 | return container.isRunning(); 201 | } 202 | 203 | @Override 204 | public int getPhase() { 205 | return container.getPhase(); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /multi-layering-cache-core/src/main/java/org/github/roger/manager/ICacheManager.java: -------------------------------------------------------------------------------- 1 | package org.github.roger.manager; 2 | 3 | 4 | import org.github.roger.cache.ICache; 5 | import org.github.roger.settings.MultiLayeringCacheSetting; 6 | 7 | import java.util.Collection; 8 | 9 | /** 10 | * 缓存管理器 11 | * 允许通过缓存名称来获的对应的 {@link ICache}. 12 | * 13 | */ 14 | public interface ICacheManager { 15 | 16 | /** 17 | * 根据缓存名称返回对应的{@link Collection}. 18 | * 19 | * @param name 缓存的名称 (不能为 {@code null}) 20 | * @return 返回对应名称的Cache, 如果没找到返回 {@code null} 21 | */ 22 | Collection getCache(String name); 23 | 24 | /** 25 | * 根据缓存名称返回对应的{@link ICache},如果没有找到就新建一个并放到容器 26 | * 27 | * @param name 缓存名称 28 | * @param multiLayeringCacheSetting 多级缓存配置 29 | * @return {@link ICache} 30 | */ 31 | ICache getCache(String name, MultiLayeringCacheSetting multiLayeringCacheSetting); 32 | 33 | /** 34 | * 获取所有缓存名称的集合 35 | * 36 | * @return 所有缓存名称的集合 37 | */ 38 | Collection getCacheNames(); 39 | } 40 | -------------------------------------------------------------------------------- /multi-layering-cache-core/src/main/java/org/github/roger/message/RedisPubSubMessage.java: -------------------------------------------------------------------------------- 1 | package org.github.roger.message; 2 | 3 | import lombok.Data; 4 | import org.github.roger.enumeration.RedisPubSubMessageType; 5 | 6 | import java.io.Serializable; 7 | 8 | @Data 9 | public class RedisPubSubMessage implements Serializable { 10 | 11 | /** 12 | * 缓存名称 13 | */ 14 | private String cacheName; 15 | 16 | /** 17 | * 缓存key 18 | */ 19 | private Object key; 20 | 21 | /** 22 | * 消息类型 23 | */ 24 | private RedisPubSubMessageType messageType; 25 | 26 | } 27 | -------------------------------------------------------------------------------- /multi-layering-cache-core/src/main/java/org/github/roger/serializer/FastJsonRedisSerializer.java: -------------------------------------------------------------------------------- 1 | package org.github.roger.serializer; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.alibaba.fastjson.JSONArray; 5 | import com.alibaba.fastjson.parser.ParserConfig; 6 | import com.alibaba.fastjson.serializer.SerializerFeature; 7 | import org.github.roger.enumeration.Type; 8 | import org.github.roger.exception.SerializationException; 9 | import org.github.roger.support.NullValue; 10 | import org.github.roger.utils.SerializationUtils; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | import org.springframework.data.redis.serializer.RedisSerializer; 14 | 15 | import java.nio.charset.Charset; 16 | 17 | 18 | public class FastJsonRedisSerializer implements RedisSerializer { 19 | private Logger logger = LoggerFactory.getLogger(FastJsonRedisSerializer.class); 20 | 21 | private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); 22 | 23 | private Class clazz; 24 | 25 | /** 26 | * 允许所有包的序列化和反序列化,不推荐 27 | * 28 | * @param clazz Class 29 | */ 30 | @Deprecated 31 | public FastJsonRedisSerializer(Class clazz) { 32 | super(); 33 | this.clazz = clazz; 34 | try { 35 | ParserConfig.getGlobalInstance().setAutoTypeSupport(true); 36 | } catch (Throwable e) { 37 | logger.warn("fastjson 版本太低,反序列化有被攻击的风险", e); 38 | } 39 | logger.warn("fastjson 反序列化有被攻击的风险,推荐使用白名单的方式,详情参考:https://www.jianshu.com/p/a92ecc33fd0d"); 40 | } 41 | 42 | /** 43 | * 指定小范围包的序列化和反序列化,具体原因可以参考: 44 | *

https://www.jianshu.com/p/a92ecc33fd0d

45 | * 46 | * @param clazz clazz 47 | * @param packages 白名单包名,如:"com.xxx." 48 | */ 49 | public FastJsonRedisSerializer(Class clazz, String... packages) { 50 | super(); 51 | this.clazz = clazz; 52 | try { 53 | ParserConfig.getGlobalInstance().addAccept("org.github.roger."); 54 | if (packages != null && packages.length > 0) { 55 | for (String packageName : packages) { 56 | ParserConfig.getGlobalInstance().addAccept(packageName); 57 | } 58 | } 59 | } catch (Throwable e) { 60 | logger.warn("fastjson 版本太低,反序列化有被攻击的风险", e); 61 | } 62 | } 63 | 64 | @Override 65 | public byte[] serialize(T t) throws SerializationException { 66 | try { 67 | return JSON.toJSONString(new FastJsonSerializerWrapper(t), SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET); 68 | } catch (Exception e) { 69 | throw new SerializationException(String.format("FastJsonRedisSerializer 序列化异常: %s, 【JSON:%s】", 70 | e.getMessage(), JSON.toJSONString(t)), e); 71 | 72 | } 73 | 74 | } 75 | 76 | @Override 77 | public T deserialize(byte[] bytes) throws SerializationException { 78 | if (SerializationUtils.isEmpty(bytes)) { 79 | return null; 80 | } 81 | 82 | String str = new String(bytes, DEFAULT_CHARSET); 83 | try { 84 | FastJsonSerializerWrapper wrapper = JSON.parseObject(str, FastJsonSerializerWrapper.class); 85 | switch (Type.parse(wrapper.getType())) { 86 | case STRING: 87 | return (T) wrapper.getContent(); 88 | case OBJECT: 89 | case SET: 90 | 91 | if (wrapper.getContent() instanceof NullValue) { 92 | return null; 93 | } 94 | 95 | return (T) wrapper.getContent(); 96 | 97 | case LIST: 98 | 99 | return (T) ((JSONArray) wrapper.getContent()).toJavaList(clazz); 100 | 101 | case NULL: 102 | 103 | return null; 104 | default: 105 | throw new SerializationException("不支持反序列化的对象类型: " + wrapper.getType()); 106 | } 107 | } catch (Exception e) { 108 | throw new SerializationException(String.format("FastJsonRedisSerializer 反序列化异常: %s, 【JSON:%s】", 109 | e.getMessage(), str), e); 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /multi-layering-cache-core/src/main/java/org/github/roger/serializer/FastJsonSerializerWrapper.java: -------------------------------------------------------------------------------- 1 | package org.github.roger.serializer; 2 | 3 | import lombok.Data; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import org.github.roger.enumeration.Type; 7 | import org.github.roger.exception.SerializationException; 8 | 9 | import java.util.List; 10 | import java.util.Set; 11 | 12 | /** 13 | * 序列化包装类 14 | * 15 | */ 16 | @NoArgsConstructor 17 | @Data 18 | public class FastJsonSerializerWrapper { 19 | 20 | private Object content; 21 | 22 | private String type; 23 | 24 | public FastJsonSerializerWrapper(Object content) { 25 | this.content = content; 26 | 27 | if (content == null) { 28 | this.type = Type.NULL.name(); 29 | return; 30 | } 31 | 32 | if (content instanceof String || content instanceof Integer 33 | || content instanceof Long || content instanceof Double 34 | || content instanceof Float || content instanceof Boolean 35 | || content instanceof Byte || content instanceof Character 36 | || content instanceof Short) { 37 | 38 | this.type = Type.STRING.name(); 39 | return; 40 | } 41 | 42 | if (content instanceof List) { 43 | this.type = Type.LIST.name(); 44 | return; 45 | } 46 | 47 | if (content instanceof Set) { 48 | this.type = Type.SET.name(); 49 | return; 50 | } 51 | 52 | if (content.getClass().isArray()) { 53 | throw new SerializationException("FastJsonRedisSerializer 序列化不支持枚数组型"); 54 | } 55 | 56 | if (content.getClass().isEnum()) { 57 | throw new SerializationException("FastJsonRedisSerializer 序列化不支持枚举类型"); 58 | } 59 | 60 | this.type = Type.OBJECT.name(); 61 | } 62 | } -------------------------------------------------------------------------------- /multi-layering-cache-core/src/main/java/org/github/roger/serializer/KryoRedisSerializer.java: -------------------------------------------------------------------------------- 1 | package org.github.roger.serializer; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.esotericsoftware.kryo.Kryo; 5 | import com.esotericsoftware.kryo.io.Input; 6 | import com.esotericsoftware.kryo.io.Output; 7 | import org.github.roger.exception.SerializationException; 8 | import org.github.roger.support.NullValue; 9 | import org.github.roger.utils.SerializationUtils; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | import org.springframework.data.redis.serializer.RedisSerializer; 13 | 14 | import java.io.ByteArrayOutputStream; 15 | 16 | /** 17 | * @param T 18 | */ 19 | public class KryoRedisSerializer implements RedisSerializer { 20 | Logger logger = LoggerFactory.getLogger(KryoRedisSerializer.class); 21 | private static final ThreadLocal kryos = ThreadLocal.withInitial(Kryo::new); 22 | 23 | private Class clazz; 24 | 25 | public KryoRedisSerializer(Class clazz) { 26 | super(); 27 | this.clazz = clazz; 28 | } 29 | 30 | @Override 31 | public byte[] serialize(T t) throws SerializationException { 32 | if (t == null) { 33 | return SerializationUtils.EMPTY_ARRAY; 34 | } 35 | 36 | Kryo kryo = kryos.get(); 37 | // 设置成false 序列化速度更快,但是遇到循环应用序列化器会报栈内存溢出 38 | kryo.setReferences(false); 39 | kryo.register(clazz); 40 | 41 | try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); 42 | Output output = new Output(baos)) { 43 | kryo.writeClassAndObject(output, t); 44 | output.flush(); 45 | return baos.toByteArray(); 46 | } catch (Exception e) { 47 | throw new SerializationException(String.format("KryoRedisSerializer 序列化异常: %s, 【JSON:%s】", 48 | e.getMessage(), JSON.toJSONString(t)), e); 49 | } finally { 50 | kryos.remove(); 51 | } 52 | } 53 | 54 | @Override 55 | public T deserialize(byte[] bytes) throws SerializationException { 56 | if (SerializationUtils.isEmpty(bytes)) { 57 | return null; 58 | } 59 | 60 | Kryo kryo = kryos.get(); 61 | // 设置成false 序列化速度更快,但是遇到循环应用序列化器会报栈内存溢出 62 | kryo.setReferences(false); 63 | kryo.register(clazz); 64 | 65 | try (Input input = new Input(bytes)) { 66 | 67 | Object result = kryo.readClassAndObject(input); 68 | if (result instanceof NullValue) { 69 | return null; 70 | } 71 | return (T) result; 72 | } catch (Exception e) { 73 | throw new SerializationException(String.format("FastJsonRedisSerializer 反序列化异常: %s, 【JSON:%s】", 74 | e.getMessage(), JSON.toJSONString(bytes)), e); 75 | } finally { 76 | kryos.remove(); 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /multi-layering-cache-core/src/main/java/org/github/roger/serializer/StringRedisSerializer.java: -------------------------------------------------------------------------------- 1 | package org.github.roger.serializer; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import org.springframework.data.redis.serializer.RedisSerializer; 5 | import org.springframework.util.Assert; 6 | 7 | import java.nio.charset.Charset; 8 | 9 | /** 10 | * 必须重写序列化器,否则@Cacheable注解的key会报类型转换错误 11 | * 12 | */ 13 | public class StringRedisSerializer implements RedisSerializer { 14 | 15 | private final Charset charset; 16 | 17 | private final String target = "\""; 18 | 19 | private final String replacement = ""; 20 | 21 | public StringRedisSerializer() { 22 | this(Charset.forName("UTF8")); 23 | } 24 | 25 | public StringRedisSerializer(Charset charset) { 26 | Assert.notNull(charset, "Charset must not be null!"); 27 | this.charset = charset; 28 | } 29 | 30 | @Override 31 | public String deserialize(byte[] bytes) { 32 | return (bytes == null ? null : new String(bytes, charset)); 33 | } 34 | 35 | @Override 36 | public byte[] serialize(Object object) { 37 | String string = JSON.toJSONString(object); 38 | if (string == null) { 39 | return null; 40 | } 41 | string = string.replace(target, replacement); 42 | return string.getBytes(charset); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /multi-layering-cache-core/src/main/java/org/github/roger/settings/FirstCacheSetting.java: -------------------------------------------------------------------------------- 1 | package org.github.roger.settings; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import lombok.ToString; 7 | import org.github.roger.enumeration.ExpireMode; 8 | 9 | import java.io.Serializable; 10 | import java.util.concurrent.TimeUnit; 11 | 12 | @Data 13 | @NoArgsConstructor 14 | @AllArgsConstructor 15 | @ToString 16 | public class FirstCacheSetting implements Serializable { 17 | 18 | /** 19 | * 缓存初始Size 20 | */ 21 | private int initialCapacity = 10; 22 | 23 | /** 24 | * 缓存最大Size 25 | */ 26 | private int maximumSize = 500; 27 | 28 | /** 29 | * 缓存有效时间 30 | */ 31 | private int expireTime = 0; 32 | 33 | /** 34 | * 缓存时间单位 35 | */ 36 | private TimeUnit timeUnit = TimeUnit.MILLISECONDS; 37 | 38 | /** 39 | * 缓存失效模式{@link ExpireMode} 40 | */ 41 | private ExpireMode expireMode = ExpireMode.WRITE; 42 | 43 | public boolean isAllowNullValues() { 44 | return false; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /multi-layering-cache-core/src/main/java/org/github/roger/settings/MultiLayeringCacheSetting.java: -------------------------------------------------------------------------------- 1 | package org.github.roger.settings; 2 | 3 | import com.alibaba.fastjson.annotation.JSONField; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.io.Serializable; 8 | 9 | @Data 10 | @NoArgsConstructor 11 | public class MultiLayeringCacheSetting implements Serializable { 12 | private static final String SPLIT = "-"; 13 | /** 14 | * 内部缓存名,由[一级缓存有效时间-二级缓存有效时间-二级缓存自动刷新时间]组成 15 | */ 16 | private String internalKey; 17 | 18 | boolean useFirstCache = true; 19 | 20 | private FirstCacheSetting firstCacheSetting; 21 | 22 | private SecondaryCacheSetting secondaryCacheSetting; 23 | 24 | public MultiLayeringCacheSetting(FirstCacheSetting firstCacheSetting, SecondaryCacheSetting secondaryCacheSetting) { 25 | this.firstCacheSetting = firstCacheSetting; 26 | this.secondaryCacheSetting = secondaryCacheSetting; 27 | internalKey(); 28 | } 29 | 30 | @JSONField(serialize = false, deserialize = false) 31 | private void internalKey() { 32 | // 一级缓存有效时间-二级缓存有效时间-二级缓存自动刷新时间 33 | StringBuilder sb = new StringBuilder(); 34 | if (firstCacheSetting != null) { 35 | sb.append(firstCacheSetting.getTimeUnit().toMillis(firstCacheSetting.getExpireTime())); 36 | } 37 | sb.append(SPLIT); 38 | if (secondaryCacheSetting != null) { 39 | sb.append(secondaryCacheSetting.getTimeUnit().toMillis(secondaryCacheSetting.getExpiration())); 40 | sb.append(SPLIT); 41 | sb.append(secondaryCacheSetting.getTimeUnit().toMillis(secondaryCacheSetting.getPreloadTime())); 42 | } 43 | internalKey = sb.toString(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /multi-layering-cache-core/src/main/java/org/github/roger/settings/SecondaryCacheSetting.java: -------------------------------------------------------------------------------- 1 | package org.github.roger.settings; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import lombok.ToString; 7 | import org.github.roger.enumeration.ExpireMode; 8 | 9 | import java.io.Serializable; 10 | import java.util.concurrent.TimeUnit; 11 | 12 | @Data 13 | @NoArgsConstructor 14 | @AllArgsConstructor 15 | @ToString 16 | public class SecondaryCacheSetting implements Serializable { 17 | 18 | /** 19 | * 缓存有效时间 20 | */ 21 | private long expiration = 0; 22 | 23 | /** 24 | * 缓存主动在失效前强制刷新缓存的时间 25 | */ 26 | private long preloadTime = 0; 27 | 28 | /** 29 | * 时间单位 {@link TimeUnit} 30 | */ 31 | private TimeUnit timeUnit = TimeUnit.MICROSECONDS; 32 | 33 | /** 34 | * 是否强制刷新(走数据库),默认是false 35 | */ 36 | private boolean forceRefresh = false; 37 | 38 | /** 39 | * 是否使用缓存名称作为 redis key 前缀 40 | */ 41 | private boolean usePrefix = true; 42 | 43 | /** 44 | * 是否允许存NULL值 45 | */ 46 | boolean allowNullValue = false; 47 | 48 | /** 49 | * 非空值和null值之间的时间倍率,默认是1。allowNullValue=true才有效 50 | * 51 | * 如配置缓存的有效时间是200秒,倍率这设置成10, 52 | * 那么当缓存value为null时,缓存的有效时间将是20秒,非空时为200秒 53 | */ 54 | int magnification = 1; 55 | } 56 | -------------------------------------------------------------------------------- /multi-layering-cache-core/src/main/java/org/github/roger/support/AwaitThreadContainer.java: -------------------------------------------------------------------------------- 1 | package org.github.roger.support; 2 | 3 | import org.springframework.util.CollectionUtils; 4 | 5 | import java.util.Comparator; 6 | import java.util.Map; 7 | import java.util.Set; 8 | import java.util.concurrent.ConcurrentHashMap; 9 | import java.util.concurrent.ConcurrentSkipListSet; 10 | import java.util.concurrent.TimeUnit; 11 | import java.util.concurrent.locks.LockSupport; 12 | 13 | /** 14 | * 等待线程容器 15 | */ 16 | public class AwaitThreadContainer { 17 | private final Map> threadMap = new ConcurrentHashMap<>(); 18 | 19 | /** 20 | * 线程等待,最大等待100毫秒 21 | * @param key 缓存Key 22 | * @param milliseconds 等待时间 23 | * @throws InterruptedException {@link InterruptedException} 24 | */ 25 | public final void await(String key, long milliseconds) throws InterruptedException { 26 | // 测试当前线程是否已经被中断 27 | if (Thread.interrupted()) { 28 | throw new InterruptedException(); 29 | } 30 | Set threadSet = threadMap.get(key); 31 | // 判断线程容器是否是null,如果是就新创建一个 32 | if (threadSet == null) { 33 | threadSet = new ConcurrentSkipListSet<>(Comparator.comparing(Thread::toString)); 34 | threadMap.put(key, threadSet); 35 | } 36 | // 将线程放到容器 37 | threadSet.add(Thread.currentThread()); 38 | // 阻塞一定的时间 39 | LockSupport.parkNanos(this, TimeUnit.MILLISECONDS.toNanos(milliseconds)); 40 | } 41 | 42 | /** 43 | * 线程唤醒 44 | * @param key key 45 | */ 46 | public final void signalAll(String key) { 47 | Set threadSet = threadMap.get(key); 48 | // 判断key所对应的等待线程容器是否是null 49 | if (!CollectionUtils.isEmpty(threadSet)) { 50 | for (Thread thread : threadSet) { 51 | LockSupport.unpark(thread); 52 | } 53 | // 清空等待线程容器 54 | threadSet.clear(); 55 | } 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /multi-layering-cache-core/src/main/java/org/github/roger/support/NullValue.java: -------------------------------------------------------------------------------- 1 | package org.github.roger.support; 2 | 3 | import java.io.Serializable; 4 | 5 | public class NullValue implements Serializable { 6 | 7 | public static final NullValue INSTANCE = new NullValue(); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /multi-layering-cache-core/src/main/java/org/github/roger/utils/RedisCacheKey.java: -------------------------------------------------------------------------------- 1 | package org.github.roger.utils; 2 | 3 | import org.springframework.data.redis.serializer.RedisSerializer; 4 | import org.springframework.data.redis.serializer.StringRedisSerializer; 5 | import org.springframework.util.Assert; 6 | import org.springframework.util.StringUtils; 7 | 8 | import java.util.Arrays; 9 | 10 | public class RedisCacheKey { 11 | 12 | /** 13 | * 前缀序列化器 14 | */ 15 | private final RedisSerializer prefixSerializer1 = new StringRedisSerializer(); 16 | 17 | /** 18 | * 缓存key 19 | */ 20 | private final Object keyElement; 21 | 22 | /** 23 | * 缓存名称 24 | */ 25 | private String cacheName; 26 | 27 | /** 28 | * 是否使用缓存前缀 29 | */ 30 | private boolean usePrefix = true; 31 | 32 | /** 33 | * RedisTemplate 的key序列化器 34 | */ 35 | private final RedisSerializer serializer; 36 | 37 | /** 38 | * @param keyElement 缓存key 39 | * @param serializer RedisSerializer 40 | */ 41 | public RedisCacheKey(Object keyElement, RedisSerializer serializer) { 42 | 43 | Assert.notNull(keyElement, "缓存key不能为NULL"); 44 | Assert.notNull(serializer, "key的序列化器不能为NULL"); 45 | this.keyElement = keyElement; 46 | this.serializer = serializer; 47 | } 48 | 49 | /** 50 | * 获取缓存key 51 | * 52 | * @return String 53 | */ 54 | public String getKey() { 55 | 56 | return new String(getKeyBytes()); 57 | } 58 | 59 | /** 60 | * 获取key的byte数组 61 | * 62 | * @return byte[] 63 | */ 64 | public byte[] getKeyBytes() { 65 | 66 | byte[] rawKey = serializeKeyElement(); 67 | if (!usePrefix) { 68 | return rawKey; 69 | } 70 | byte[] prefix = getPrefix(); 71 | 72 | byte[] prefixedKey = Arrays.copyOf(prefix, prefix.length + rawKey.length); 73 | System.arraycopy(rawKey, 0, prefixedKey, prefix.length, rawKey.length); 74 | 75 | return prefixedKey; 76 | } 77 | 78 | private byte[] serializeKeyElement() { 79 | 80 | if (serializer == null && keyElement instanceof byte[]) { 81 | return (byte[]) keyElement; 82 | } 83 | 84 | return serializer.serialize(keyElement); 85 | } 86 | 87 | /** 88 | * 获取缓存前缀,默认缓存前缀是":",是否使用缓存名称作为前缀 89 | * 90 | * @return byte[] 91 | */ 92 | public byte[] getPrefix() { 93 | return prefixSerializer1.serialize((StringUtils.isEmpty(cacheName) ? "" : cacheName.concat(":"))); 94 | } 95 | 96 | /** 97 | * 设置缓存名称 98 | * 99 | * @param cacheName cacheName 100 | * @return RedisCacheKey 101 | */ 102 | public RedisCacheKey cacheName(String cacheName) { 103 | this.cacheName = cacheName; 104 | return this; 105 | } 106 | 107 | /** 108 | * 设置是否使用缓存前缀,默认使用 109 | * 110 | * @param usePrefix usePrefix 111 | * @return RedisCacheKey 112 | */ 113 | public RedisCacheKey usePrefix(boolean usePrefix) { 114 | this.usePrefix = usePrefix; 115 | return this; 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /multi-layering-cache-core/src/main/java/org/github/roger/utils/SerializationUtils.java: -------------------------------------------------------------------------------- 1 | package org.github.roger.utils; 2 | 3 | /** 4 | * 序列化工具类 5 | * 6 | */ 7 | public abstract class SerializationUtils { 8 | 9 | public static final byte[] EMPTY_ARRAY = new byte[0]; 10 | 11 | public static boolean isEmpty(byte[] data) { 12 | return (data == null || data.length == 0); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /multi-layering-cache-core/src/test/java/org/github/roger/cache/config/ICacheManagerConfig.java: -------------------------------------------------------------------------------- 1 | package org.github.roger.cache.config; 2 | 3 | import org.github.roger.MultiLayeringCacheManager; 4 | import org.github.roger.manager.ICacheManager; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.context.annotation.Import; 8 | import org.springframework.data.redis.core.RedisTemplate; 9 | 10 | @Configuration 11 | @Import({RedisConfig.class}) 12 | public class ICacheManagerConfig { 13 | 14 | 15 | @Bean 16 | public ICacheManager cacheManager(RedisTemplate redisTemplate) { 17 | MultiLayeringCacheManager layeringCacheManager = new MultiLayeringCacheManager(redisTemplate); 18 | 19 | return layeringCacheManager; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /multi-layering-cache-core/src/test/java/org/github/roger/cache/config/RedisConfig.java: -------------------------------------------------------------------------------- 1 | package org.github.roger.cache.config; 2 | 3 | import io.lettuce.core.resource.ClientResources; 4 | import io.lettuce.core.resource.DefaultClientResources; 5 | import org.github.roger.serializer.FastJsonRedisSerializer; 6 | import org.github.roger.serializer.KryoRedisSerializer; 7 | import org.github.roger.serializer.StringRedisSerializer; 8 | import org.springframework.beans.factory.annotation.Value; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | import org.springframework.context.annotation.PropertySource; 12 | import org.springframework.data.redis.connection.RedisConnectionFactory; 13 | import org.springframework.data.redis.connection.RedisPassword; 14 | import org.springframework.data.redis.connection.RedisStandaloneConfiguration; 15 | import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration; 16 | import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration.LettuceClientConfigurationBuilder; 17 | import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; 18 | import org.springframework.data.redis.core.RedisTemplate; 19 | 20 | @Configuration 21 | @PropertySource({"classpath:application.properties"}) 22 | public class RedisConfig { 23 | 24 | @Value("${spring.redis.database:0}") 25 | private int database; 26 | 27 | @Value("${spring.redis.host:127.0.0.1}") 28 | private String host; 29 | 30 | @Value("${spring.redis.password:}") 31 | private String password; 32 | 33 | @Value("${spring.redis.port:6379}") 34 | private int port; 35 | 36 | @Value("${spring.redis.pool.max-idle:200}") 37 | private int maxIdle; 38 | 39 | @Value("${spring.redis.pool.min-idle:10}") 40 | private int minIdle; 41 | 42 | @Value("${spring.redis.pool.max-active:80}") 43 | private int maxActive; 44 | 45 | @Value("${spring.redis.pool.max-wait:-1}") 46 | private int maxWait; 47 | 48 | 49 | // @Bean 50 | // public JedisConnectionFactory redisConnectionFactory() { 51 | // JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); 52 | // jedisPoolConfig.setMinIdle(minIdle); 53 | // jedisPoolConfig.setMaxIdle(maxIdle); 54 | // jedisPoolConfig.setMaxTotal(maxActive); 55 | // jedisPoolConfig.setMaxWaitMillis(maxWait); 56 | // 57 | // JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(jedisPoolConfig); 58 | // jedisConnectionFactory.setDatabase(database); 59 | // jedisConnectionFactory.setHostName(host); 60 | // jedisConnectionFactory.setPassword(password); 61 | // jedisConnectionFactory.setPort(port); 62 | // jedisConnectionFactory.setUsePool(true); 63 | // return jedisConnectionFactory; 64 | // } 65 | 66 | @Bean 67 | public LettuceConnectionFactory redisConnectionFactory() { 68 | ClientResources clientResources = DefaultClientResources.create(); 69 | LettuceClientConfigurationBuilder builder = LettuceClientConfiguration.builder(); 70 | builder.clientResources(clientResources); 71 | LettuceClientConfiguration configuration = builder.build(); 72 | 73 | RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(); 74 | config.setHostName(host); 75 | config.setPort(port); 76 | config.setPassword(RedisPassword.of(password)); 77 | config.setDatabase(database); 78 | return new LettuceConnectionFactory(config, configuration); 79 | } 80 | 81 | @Bean 82 | public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { 83 | RedisTemplate redisTemplate = new RedisTemplate(); 84 | redisTemplate.setConnectionFactory(redisConnectionFactory); 85 | 86 | FastJsonRedisSerializer fastJsonRedisSerializer = new FastJsonRedisSerializer(Object.class, "com.xxx."); 87 | KryoRedisSerializer kryoRedisSerializer = new KryoRedisSerializer(Object.class); 88 | 89 | // 设置值(value)的序列化采用FastJsonRedisSerializer。 90 | redisTemplate.setValueSerializer(kryoRedisSerializer); 91 | redisTemplate.setHashValueSerializer(kryoRedisSerializer); 92 | // 设置键(key)的序列化采用StringRedisSerializer。 93 | redisTemplate.setKeySerializer(new StringRedisSerializer()); 94 | redisTemplate.setHashKeySerializer(new StringRedisSerializer()); 95 | 96 | redisTemplate.afterPropertiesSet(); 97 | return redisTemplate; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /multi-layering-cache-core/src/test/java/org/github/roger/cache/core/CacheCoreTest.java: -------------------------------------------------------------------------------- 1 | package org.github.roger.cache.core; 2 | 3 | import com.github.benmanes.caffeine.cache.Cache; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.github.roger.MultiLayeringCache; 6 | import org.github.roger.cache.ICache; 7 | import org.github.roger.cache.config.ICacheManagerConfig; 8 | import org.github.roger.cache.redis.RedisCache; 9 | import org.github.roger.enumeration.ExpireMode; 10 | import org.github.roger.manager.ICacheManager; 11 | import org.github.roger.settings.FirstCacheSetting; 12 | import org.github.roger.settings.MultiLayeringCacheSetting; 13 | import org.github.roger.settings.SecondaryCacheSetting; 14 | import org.github.roger.utils.RedisCacheKey; 15 | import org.junit.Assert; 16 | import org.junit.Before; 17 | import org.junit.Test; 18 | import org.junit.runner.RunWith; 19 | import org.slf4j.Logger; 20 | import org.slf4j.LoggerFactory; 21 | import org.springframework.beans.factory.annotation.Autowired; 22 | import org.springframework.data.redis.core.RedisTemplate; 23 | import org.springframework.test.context.ContextConfiguration; 24 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 25 | 26 | import java.util.Collection; 27 | import java.util.concurrent.TimeUnit; 28 | 29 | // SpringJUnit4ClassRunner再Junit环境下提供Spring TestContext Framework的功能。 30 | @RunWith(SpringJUnit4ClassRunner.class) 31 | // @ContextConfiguration用来加载配置ApplicationContext,其中classes用来加载配置类 32 | @ContextConfiguration(classes = {ICacheManagerConfig.class}) 33 | @Slf4j 34 | public class CacheCoreTest { 35 | 36 | @Autowired 37 | private ICacheManager cacheManager; 38 | 39 | @Autowired 40 | private RedisTemplate redisTemplate; 41 | 42 | private MultiLayeringCacheSetting layeringCacheSetting1; 43 | private MultiLayeringCacheSetting layeringCacheSetting2; 44 | private MultiLayeringCacheSetting layeringCacheSetting4; 45 | private MultiLayeringCacheSetting layeringCacheSetting5; 46 | 47 | @Before 48 | public void before() { 49 | // 测试 CacheManager getCache方法 50 | FirstCacheSetting firstCacheSetting1 = new FirstCacheSetting(10, 1000, 4, TimeUnit.SECONDS, ExpireMode.WRITE); 51 | SecondaryCacheSetting secondaryCacheSetting1 = new SecondaryCacheSetting(10, 4, TimeUnit.SECONDS, true, true, true, 1); 52 | layeringCacheSetting1 = new MultiLayeringCacheSetting(firstCacheSetting1, secondaryCacheSetting1); 53 | 54 | // 二级缓存可以缓存null,时间倍率是1 55 | FirstCacheSetting firstCacheSetting2 = new FirstCacheSetting(10, 1000, 5, TimeUnit.SECONDS, ExpireMode.WRITE); 56 | SecondaryCacheSetting secondaryCacheSetting2 = new SecondaryCacheSetting(3000, 14, TimeUnit.SECONDS, true, true, true, 1); 57 | layeringCacheSetting2 = new MultiLayeringCacheSetting(firstCacheSetting2, secondaryCacheSetting2); 58 | 59 | // 二级缓存可以缓存null,时间倍率是10 60 | FirstCacheSetting firstCacheSetting4 = new FirstCacheSetting(10, 1000, 5, TimeUnit.SECONDS, ExpireMode.WRITE); 61 | SecondaryCacheSetting secondaryCacheSetting4 = new SecondaryCacheSetting(100, 70, TimeUnit.SECONDS, true, true, true, 10); 62 | layeringCacheSetting4 = new MultiLayeringCacheSetting(firstCacheSetting4, secondaryCacheSetting4); 63 | 64 | 65 | // 二级缓存不可以缓存null 66 | FirstCacheSetting firstCacheSetting5 = new FirstCacheSetting(10, 1000, 5, TimeUnit.SECONDS, ExpireMode.WRITE); 67 | SecondaryCacheSetting secondaryCacheSetting5 = new SecondaryCacheSetting(10, 7, TimeUnit.SECONDS, true, false, true, 1); 68 | layeringCacheSetting5 = new MultiLayeringCacheSetting(firstCacheSetting5, secondaryCacheSetting5); 69 | 70 | } 71 | 72 | @Test 73 | public void testGetCache() { 74 | String cacheName = "cache:name"; 75 | ICache cache1 = cacheManager.getCache(cacheName, layeringCacheSetting1); 76 | ICache cache2 = cacheManager.getCache(cacheName, layeringCacheSetting1); 77 | Assert.assertEquals(cache1, cache2); 78 | 79 | ICache cache3 = cacheManager.getCache(cacheName, layeringCacheSetting2); 80 | Collection caches = cacheManager.getCache(cacheName); 81 | Assert.assertTrue(caches.size() == 2); 82 | Assert.assertNotEquals(cache1, cache3); 83 | } 84 | 85 | @Test 86 | public void testCacheExpiration() { 87 | //一级缓存的有效时间是在写入之后,4秒 88 | String cacheName = "cache:name"; 89 | String cacheKey1 = "cache:key1"; 90 | MultiLayeringCache cache1 = (MultiLayeringCache) cacheManager.getCache(cacheName, layeringCacheSetting1); 91 | cache1.get(cacheKey1, () -> initCache(String.class)); 92 | // 测试一级缓存值及过期时间 93 | String str1 = cache1.getFirstCache().get(cacheKey1, String.class); 94 | String st2 = cache1.getFirstCache().get(cacheKey1, () -> initCache(String.class)); 95 | log.debug("========================:{}", str1); 96 | Assert.assertTrue(str1.equals(st2)); 97 | Assert.assertTrue(str1.equals(initCache(String.class))); 98 | sleep(5); 99 | Assert.assertNull(cache1.getFirstCache().get(cacheKey1, String.class)); 100 | // 看日志是不是走了二级缓存 101 | cache1.get(cacheKey1, () -> initCache(String.class)); 102 | log.debug("***********************"); 103 | log.debug("***********************"); 104 | log.debug("***********************"); 105 | //二级缓存的有效时间是10秒,在失效前 4秒强制刷新缓存 106 | str1 = cache1.getSecondCache().get(cacheKey1, String.class); 107 | st2 = cache1.getSecondCache().get(cacheKey1, () -> initCache(String.class)); 108 | Assert.assertTrue(st2.equals(str1)); 109 | Assert.assertTrue(str1.equals(initCache(String.class))); 110 | sleep(5); 111 | // 看日志是不是走了自动刷新 112 | RedisCacheKey redisCacheKey = ((RedisCache) cache1.getSecondCache()).getRedisCacheKey(cacheKey1); 113 | cache1.get(cacheKey1, () -> initCache(String.class)); 114 | sleep(6); 115 | Long ttl = redisTemplate.getExpire(redisCacheKey.getKey()); 116 | log.debug("========================ttl 1:{}", ttl); 117 | Assert.assertNotNull(cache1.getSecondCache().get(cacheKey1)); 118 | sleep(5); 119 | ttl = redisTemplate.getExpire(redisCacheKey.getKey()); 120 | log.debug("========================ttl 2:{}", ttl); 121 | Assert.assertNull(cache1.getSecondCache().get(cacheKey1)); 122 | } 123 | 124 | @Test 125 | public void testGetCacheNullUseAllowNullValueTrue() { 126 | log.info("测试二级缓存允许为NULL,NULL值时间倍率是10"); 127 | //定义缓存名称 128 | String cacheName = "cache:name:19_1"; 129 | //定义缓存key 130 | String cacheKey = "cache:key:19_1"; 131 | //根据缓存名称获取对应的多级缓存管理对象 132 | MultiLayeringCache cache = (MultiLayeringCache) cacheManager.getCache(cacheName,layeringCacheSetting4); 133 | //根据缓存key获取缓存值,如果没有则调用相应的方法给缓存key设置缓存值,并返回 134 | cache.get(cacheKey,()->initNullCache()); 135 | // 测试一级缓存值不能缓存NULL 136 | String str1 = cache.getFirstCache().get(cacheKey, String.class); 137 | Cache realCache = (Cache) cache.getFirstCache().getRealCache(); 138 | Assert.assertTrue(str1 == null); 139 | // 如果一级缓存可以保存null值,则此时size = 1 140 | Assert.assertTrue(0 == realCache.asMap().size()); 141 | 142 | // 测试二级缓存可以存NULL值,NULL值时间倍率是10,缓存配置的过期时间是100s 143 | String st2 = cache.getSecondCache().get(cacheKey, String.class); 144 | RedisCacheKey redisCacheKey = ((RedisCache) cache.getSecondCache()).getRedisCacheKey(cacheKey); 145 | Long ttl = redisTemplate.getExpire(redisCacheKey.getKey()); 146 | Assert.assertTrue(redisTemplate.hasKey(redisCacheKey.getKey())); 147 | Assert.assertTrue(st2 == null); 148 | Assert.assertTrue(ttl <= 10); 149 | sleep(5); 150 | st2 = cache.getSecondCache().get(cacheKey, String.class); 151 | Assert.assertTrue(st2 == null); 152 | cache.getSecondCache().get(cacheKey, () -> initNullCache()); 153 | sleep(1); 154 | ttl = redisTemplate.getExpire(redisCacheKey.getKey()); 155 | Assert.assertTrue(ttl <= 10 && ttl > 5); 156 | 157 | st2 = cache.get(cacheKey, String.class); 158 | Assert.assertTrue(st2 == null); 159 | } 160 | 161 | @Test 162 | public void testCacheEvict() throws Exception { 163 | // 测试 缓存过期时间 164 | String cacheName = "cache:name"; 165 | String cacheKey1 = "cache:key2"; 166 | String cacheKey2 = "cache:key3"; 167 | MultiLayeringCache cache1 = (MultiLayeringCache) cacheManager.getCache(cacheName, layeringCacheSetting1); 168 | cache1.get(cacheKey1, () -> initCache(String.class)); 169 | cache1.get(cacheKey2, () -> initCache(String.class)); 170 | // 测试删除方法 171 | cache1.evict(cacheKey1); 172 | Thread.sleep(500); 173 | String str1 = cache1.get(cacheKey1, String.class); 174 | String str2 = cache1.get(cacheKey2, String.class); 175 | Assert.assertNull(str1); 176 | Assert.assertNotNull(str2); 177 | // 测试删除方法 178 | cache1.evict(cacheKey1); 179 | Thread.sleep(500); 180 | str1 = cache1.get(cacheKey1, () -> initCache(String.class)); 181 | str2 = cache1.get(cacheKey2, String.class); 182 | Assert.assertNotNull(str1); 183 | Assert.assertNotNull(str2); 184 | } 185 | 186 | private T initCache(Class t) { 187 | log.debug("加载缓存"); 188 | return (T) "test"; 189 | } 190 | 191 | private T initNullCache() { 192 | log.debug("加载缓存,空值"); 193 | return null; 194 | } 195 | 196 | private void sleep(int time) { 197 | try { 198 | Thread.sleep(time * 1000); 199 | } catch (InterruptedException e) { 200 | log.error(e.getMessage(), e); 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /multi-layering-cache-core/src/test/java/org/github/roger/utils/RedisCacheKeyTest.java: -------------------------------------------------------------------------------- 1 | package org.github.roger.utils; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | public class RedisCacheKeyTest { 8 | 9 | @Test 10 | public void testGetKey() { 11 | } 12 | } -------------------------------------------------------------------------------- /multi-layering-cache-core/src/test/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.redis.pool.max-idle=200 2 | spring.redis.pool.min-idle=10 3 | spring.redis.pool.max-active=80 4 | spring.redis.pool.max-wait=-1 5 | 6 | spring.redis.database=0 7 | #server host 8 | spring.redis.host=127.0.0.1 9 | #server password 10 | spring.redis.password= 11 | #connection port 12 | spring.redis.port=6379 13 | -------------------------------------------------------------------------------- /multi-layering-cache-core/src/test/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | #定义LOG输出级别 2 | log4j.rootLogger=DEBUG,Console,File 3 | #定义日志输出目的地为控制台 4 | log4j.appender.Console=org.apache.log4j.ConsoleAppender 5 | log4j.appender.Console.Target=System.out 6 | #可以灵活地指定日志输出格式,下面一行是指定具体的格式 7 | log4j.appender.Console.layout = org.apache.log4j.PatternLayout 8 | log4j.appender.Console.layout.ConversionPattern=[%c] - %m%n 9 | 10 | #文件大小到达指定尺寸的时候产生一个新的文件 11 | log4j.appender.File = org.apache.log4j.RollingFileAppender 12 | #指定输出目录 13 | log4j.appender.File.File = F:/logs/multi-layering-cache-core.log 14 | #定义文件最大大小 15 | log4j.appender.File.MaxFileSize = 10MB 16 | # 输出所以日志,如果换成DEBUG表示输出DEBUG以上级别日志 17 | log4j.appender.File.Threshold = ALL 18 | log4j.appender.File.layout = org.apache.log4j.PatternLayout 19 | log4j.appender.File.layout.ConversionPattern =[%p] [%d{yyyy-MM-dd HH\:mm\:ss}][%c]%m%n 20 | 21 | -------------------------------------------------------------------------------- /multi-layering-cache-starter/.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | /target/ -------------------------------------------------------------------------------- /multi-layering-cache-starter/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | multi-layering-cache 7 | com.github.roger 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | multi-layering-cache-starter 13 | multi-layering-cache-starter 14 | 多级缓存启动模块 15 | jar 16 | 17 | 18 | 19 | 20 | org.springframework.boot 21 | spring-boot-autoconfigure 22 | 23 | 24 | 25 | com.github.roger 26 | multi-layering-cache-aspecj 27 | 1.0-SNAPSHOT 28 | 29 | 30 | 31 | org.springframework.boot 32 | spring-boot-configuration-processor 33 | true 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /multi-layering-cache-starter/src/main/java/com/github/roger/cache/config/MultiLayeringCacheAutoConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.roger.cache.config; 2 | 3 | import com.github.roger.aspect.MultiLayeringCacheAspect; 4 | import com.github.roger.cache.properties.MultiLayeringCacheProperties; 5 | import org.github.roger.MultiLayeringCacheManager; 6 | import org.github.roger.manager.ICacheManager; 7 | import org.springframework.boot.autoconfigure.AutoConfigureAfter; 8 | import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; 9 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 10 | import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; 11 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 12 | import org.springframework.context.annotation.Bean; 13 | import org.springframework.context.annotation.Configuration; 14 | import org.springframework.context.annotation.EnableAspectJAutoProxy; 15 | import org.springframework.data.redis.core.RedisTemplate; 16 | 17 | @Configuration 18 | //仅仅在当前上下文中存在Xxxx对象时,才会实例化一个Bean 19 | //也就是 只有当RedisTemplate.class 在spring的applicationContext中存在时 这个当前的bean才能够创建 20 | @ConditionalOnBean(RedisTemplate.class) 21 | @AutoConfigureAfter({RedisAutoConfiguration.class}) 22 | @EnableAspectJAutoProxy 23 | @EnableConfigurationProperties({MultiLayeringCacheProperties.class}) 24 | public class MultiLayeringCacheAutoConfig { 25 | 26 | @Bean 27 | //如果项目中自己定义了ICacheManager实例,则这个实例不必创建 28 | @ConditionalOnMissingBean(ICacheManager.class) 29 | public ICacheManager cacheManager(RedisTemplate redisTemplate ,MultiLayeringCacheProperties properties) { 30 | MultiLayeringCacheManager layeringCacheManager = new MultiLayeringCacheManager(redisTemplate); 31 | 32 | return layeringCacheManager; 33 | } 34 | 35 | @Bean//把切面交给Spring 容器管理 36 | public MultiLayeringCacheAspect layeringCacheAspect(){ 37 | return new MultiLayeringCacheAspect(); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /multi-layering-cache-starter/src/main/java/com/github/roger/cache/properties/MultiLayeringCacheProperties.java: -------------------------------------------------------------------------------- 1 | package com.github.roger.cache.properties; 2 | 3 | import lombok.Data; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | 6 | @ConfigurationProperties( prefix = "spring.multi-layering-cache") 7 | @Data 8 | public class MultiLayeringCacheProperties { 9 | 10 | /** 11 | * 命名空间,必须唯一般使用服务名 12 | */ 13 | private String namespace; 14 | } 15 | -------------------------------------------------------------------------------- /multi-layering-cache-starter/src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ 2 | com.github.roger.cache.config.MultiLayeringCacheAutoConfig 3 | -------------------------------------------------------------------------------- /multi-layering-cache.iml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.github.roger 8 | multi-layering-cache 9 | pom 10 | 1.0-SNAPSHOT 11 | 12 | 13 | 14 | multi-layering-cache-core 15 | multi-layering-cache-aspecj 16 | multi-layering-cache-starter 17 | 18 | 19 | 20 | 21 | 22 | UTF-8 23 | 24 | 5.1.4.RELEASE 25 | 1.16.20 26 | 1.7.25 27 | 1.2.56 28 | 2.6.2 29 | 2.1.4.RELEASE 30 | 5.1.2.RELEASE 31 | 4.0.2 32 | 1.9.2 33 | 2.1.0.RELEASE 34 | 35 | 36 | 37 | 38 | 39 | 40 | org.springframework.data 41 | spring-data-redis 42 | ${spring.data.redis.version} 43 | 44 | 45 | 46 | io.lettuce 47 | lettuce-core 48 | ${lettuce.version} 49 | 50 | 51 | 52 | com.github.ben-manes.caffeine 53 | caffeine 54 | ${caffeine.version} 55 | 56 | 57 | 58 | com.alibaba 59 | fastjson 60 | ${fastjson.version} 61 | 62 | 63 | 64 | org.springframework 65 | spring-beans 66 | ${spring.version} 67 | 68 | 69 | 70 | com.esotericsoftware 71 | kryo-shaded 72 | ${kryo.version} 73 | 74 | 75 | 76 | org.springframework.boot 77 | spring-boot-autoconfigure 78 | ${spring.boot.version} 79 | 80 | 81 | 82 | org.springframework.boot 83 | spring-boot-configuration-processor 84 | ${spring.boot.version} 85 | 86 | 87 | 88 | org.springframework.boot 89 | spring-boot-starter-test 90 | ${spring.boot.version} 91 | test 92 | 93 | 94 | 95 | org.aspectj 96 | aspectjweaver 97 | ${aspectj.version} 98 | 99 | 100 | 101 | org.springframework 102 | spring-aop 103 | ${spring.version} 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | org.springframework 113 | spring-core 114 | ${spring.version} 115 | 116 | 117 | 118 | org.projectlombok 119 | lombok 120 | ${lombok.version} 121 | 122 | 123 | 124 | org.slf4j 125 | slf4j-log4j12 126 | ${slf4j.version} 127 | 128 | 129 | 130 | org.slf4j 131 | jul-to-slf4j 132 | ${slf4j.version} 133 | 134 | 135 | 136 | org.slf4j 137 | jcl-over-slf4j 138 | ${slf4j.version} 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | org.apache.maven.plugins 149 | maven-compiler-plugin 150 | 151 | 1.8 152 | 1.8 153 | 154 | 155 | 156 | 157 | 158 | org.apache.maven.plugins 159 | maven-source-plugin 160 | 161 | 162 | attach-sources 163 | 164 | jar 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | --------------------------------------------------------------------------------