Basic introduction
We know , Operating the database frequently will reduce the system performance of the server , Therefore, frequent access to 、 The updated data is stored in the cache .Halo The project also introduces a caching mechanism , And set a variety of implementation methods , Such as custom cache 、Redis、LevelDB etc. , Let's analyze the implementation process of caching mechanism .
Custom cache
Because the data exists in the form of key value pairs in the cache , And different types of cache systems define similar storage and read operations , Therefore, this article only introduces the default custom cache in the project . Custom cache refers to the cache written by the author , With ConcurrentHashMap As a container , The data is stored in the memory of the server . Before introducing custom caching , Let's take a look first Halo System diagram of cache :
I use Halo 1.4.13 Version is not set Redis cache , The above is from 1.5.2 edition .
You can see , The author's design idea is to define general operation methods in the upper abstract classes and interfaces , And the specific cache container 、 Data storage and reading methods are defined in each implementation class . If you want to modify the type of cache , Just configure the class HaloProperties Revision in China cache Value of field :
@Bean
@ConditionalOnMissingBean
AbstractStringCacheStore stringCacheStore() {
AbstractStringCacheStore stringCacheStore;
// according to cache Select the specific cache type according to the value of the field
switch (haloProperties.getCache()) {
case "level":
stringCacheStore = new LevelCacheStore(this.haloProperties);
break;
case "redis":
stringCacheStore = new RedisCacheStore(stringRedisTemplate);
break;
case "memory":
default:
stringCacheStore = new InMemoryCacheStore();
break;
}
log.info("Halo cache store load impl : [{}]", stringCacheStore.getClass());
return stringCacheStore;
}
The above code comes from 1.5.2 edition .
cache The default value of the field is "memory", Therefore, the implementation class of cache is InMemoryCacheStore( Custom cache ):
public class InMemoryCacheStore extends AbstractStringCacheStore {
/**
* Cleaner schedule period. (ms)
*/
private static final long PERIOD = 60 * 1000;
/**
* Cache container.
*/
public static final ConcurrentHashMap<String, CacheWrapper<String>> CACHE_CONTAINER =
new ConcurrentHashMap<>();
private final Timer timer;
/**
* Lock.
*/
private final Lock lock = new ReentrantLock();
public InMemoryCacheStore() {
// Run a cache store cleaner
timer = new Timer();
// Every time 60s Clear an expired key
timer.scheduleAtFixedRate(new CacheExpiryCleaner(), 0, PERIOD);
}
// Omitted code
}
InMemoryCacheStore The meaning of member variable is as follows :
- CACHE_CONTAINER yes InMemoryCacheStore Cache container for , The type is ConcurrentHashMap. Use ConcurrentHashMap To ensure thread safety , Because the data related to cache lock will be stored in the cache ( The following describes ), Whenever a user accesses a service in the background , New data will enter the cache , This data may come from different threads , therefore CACHE_CONTAINER You need to consider the case of multiple threads operating at the same time .
<!-- 2. - We are
Halo Open source project learning ( Four ): Publish articles and pages
Mentioned in the article ,Halo Set up authorization mechanism for private articles , When the client obtains the authorization of the article , The server adds the client's to the cache sessionId. Because different users may apply for authorization at the same time , therefore CACHE_CONTAINER You need to consider the case of multiple threads operating at the same time . --> - timer Responsible for performing periodic tasks , The frequency of task execution is PERIOD, Default is one minute , The processing logic of periodic tasks is to clear the expired data in the cache key.
- lock yes ReentrantLock Type of exclusive lock , Related to cache lock .
Data in cache
The data stored in the cache includes :
- Option information in system settings , In fact, that is options Data stored in tables .
- Logged in users ( Blogger ) Of token.
- The name of the client that has been authorized by the article sessionId.
- Cache lock related data .
In the previous post , We introduced token and sessionId Storage and acquisition of , Therefore, this article will not repeat this part , See Halo Open source project learning ( 3、 ... and ): Sign up and log in
and Halo Open source project learning ( Four ): Publish articles and pages
. Cache lock will be introduced in the next section , In this section, we first look at Halo How to save options Information .
First of all, I need to understand options When is the information stored in the cache , actually , The program will be released after startup ApplicationStartedEvent event , It is defined in the project to be responsible for listening ApplicationStartedEvent Event listener StartedListener(listener It's a bag ), The listener will execute after the event is published initThemes Method , Here is initThemes Partial code snippet in method :
private void initThemes() {
// Whether the blog has initialized
Boolean isInstalled = optionService
.getByPropertyOrDefault(PrimaryProperties.IS_INSTALLED, Boolean.class, false);
// Omitted code
}
This method is called getByPropertyOrDefault Method to query the installation status of the blog from the cache , We from getByPropertyOrDefault Method start , Search down the call chain , You can trace OptionProvideService Interface getByKey Method :
default Optional<Object> getByKey(@NonNull String key) {
Assert.hasText(key, "Option key must not be blank");
// If val = listOptions().get(key) Not empty , return value by val Of Optional object , Otherwise return to value Empty Optional object
return Optional.ofNullable(listOptions().get(key));
}
You can see , The point is this listOptions Method , The method in OptionServiceImpl Definition in class :
public Map<String, Object> listOptions() {
// Get options from cache
// From the cache CACHE_CONTAINER In order to get "options" This key Corresponding data , And convert the data into Map object
return cacheStore.getAny(OPTIONS_KEY, Map.class).orElseGet(() -> {
// The first call needs to start from options Get all... From the table Option object
List<Option> options = listAll();
// all Option Object's key aggregate
Set<String> keys = ServiceUtils.fetchProperty(options, Option::getKey);
/*
* options The records stored in the table are actually user-defined Option Options , When users modify blog settings , It will update automatically options surface ,
* Halo For some options in value Set a certain type , for example EmailProperties In this class HOST by String type , and
* SSL_PORT Then for Integer type , because Option Class value All are String type , Therefore, some value Translate into
* Set the type
*/
Map<String, Object> userDefinedOptionMap =
ServiceUtils.convertToMap(options, Option::getKey, option -> {
String key = option.getKey();
PropertyEnum propertyEnum = propertyEnumMap.get(key);
if (propertyEnum == null) {
return option.getValue();
}
// Yes value Do type conversion
return PropertyEnum.convertTo(option.getValue(), propertyEnum);
});
Map<String, Object> result = new HashMap<>(userDefinedOptionMap);
// Add default property
/*
* Some of the options are Halo The default setting is , for example EmailProperties Medium SSL_PORT, When not set by the user , It will also be set to the default 465,
* Again , You also need to set the default "465" Turn into Integer Type of 465
*/
propertyEnumMap.keySet()
.stream()
.filter(key -> !keys.contains(key))
.forEach(key -> {
PropertyEnum propertyEnum = propertyEnumMap.get(key);
if (StringUtils.isBlank(propertyEnum.defaultValue())) {
return;
}
// Yes value Type conversion and save result
result.put(key,
PropertyEnum.convertTo(propertyEnum.defaultValue(), propertyEnum));
});
// Cache the result
// Add all options to the cache
cacheStore.putAny(OPTIONS_KEY, result);
return result;
});
}
The server method starts with CACHE_CONTAINER In order to get "options" This key Corresponding data , The data is then converted to Map Object of type . Because the first query CACHE_CONTAINER in did not "options" Corresponding value, Therefore, initialization is required :
- First of all, from the options Get all... From the table Option object , And store these objects in Map in . among key and value Are all Option Object key and value, but value You also need to do a type conversion , Because in Option Class value Defined as String type . for example ,"is_installed" Corresponding value by "true", In order to be able to use value, The string needs to be "true" Turn it into Boolean Type of true. Combined with the context , We found that the procedure was based on PrimaryProperties class ( Inherit PropertyEnum Enumeration class of ) Enumeration objects defined in
IS_INSTALLED("is_installed", Boolean.class, "false")
To confirm the target type Boolean Of . - options The options in the table are user-defined options , besides ,Halo Some default options are also set in , These options are in PropertyEnum Defined in subclasses of , for example EmailProperties Class
SSL_PORT("email_ssl_port", Integer.class, "465")
, Their corresponding key by "email_ssl_port",value by "465". The server will also send these key - value Yes, to Map, Also on value Do type conversion .
The above is listOptions Method processing logic , We go back to getByKey Method , You can find , Get listOptions Method Map After the object , The server can be based on the specified key( Such as "is_installed") Get the corresponding attribute value ( Such as true). When the user modifies the system settings of the blog in the administrator background , The server will update according to the user's configuration options surface , And publish OptionUpdatedEvent event , After that, the listener responsible for processing the event will send the... In the cache "options" Delete , Perform the initialization operation according to the above steps next time ( See FreemarkerConfigAwareListener Medium onOptionUpdate Method ).
Cache expiration processing
It is very important to cache the expired knowledge , After the data has expired , It usually needs to be removed from the cache . From the above cacheStore.putAny(OPTIONS_KEY, result) In the method we know , Before the server stores the data in the cache , It will be encapsulated into CacheWrapper object :
class CacheWrapper<V> implements Serializable {
/**
* Cache data
*/
private V data;
/**
* Expired time.
*/
private Date expireAt;
/**
* Create time.
*/
private Date createAt;
}
among data Is the data that needs to be stored ,createAt and expireAt They are the creation time and expiration time of the data .Halo In the project ,"options" There is no expiration time , Only when the data is updated , The listener will delete the old data . It should be noted that ,token and sessionId There are expiration dates , For those with expiration time key, There are also corresponding treatment methods in the project . With token For example , After intercepting the user's request, the interceptor will confirm the user's identity , That is, whether there is in the query cache token The corresponding user id, The underlying call of this query operation is get Method ( stay AbstractCacheStore Definition in class ):
public Optional<V> get(K key) {
Assert.notNull(key, "Cache key must not be blank");
return getInternal(key).map(cacheWrapper -> {
// Check expiration
// Be overdue
if (cacheWrapper.getExpireAt() != null
&& cacheWrapper.getExpireAt().before(run.halo.app.utils.DateUtils.now())) {
// Expired then delete it
log.warn("Cache key: [{}] has been expired", key);
// Delete the key
delete(key);
// Return null
return null;
}
// Cache data returned without expiration
return cacheWrapper.getData();
});
}
Server get to key Corresponding CacheWrapper After the object , Will check the expiration time , If the data has expired , Then delete it directly and return to null. in addition , As mentioned above ,timer(InMemoryCacheStore Member variables of ) The periodic task is also responsible for deleting expired data , Here is timer The method of executing periodic tasks :
private class CacheExpiryCleaner extends TimerTask {
@Override
public void run() {
CACHE_CONTAINER.keySet().forEach(key -> {
if (!InMemoryCacheStore.this.get(key).isPresent()) {
log.debug("Deleted the cache: [{}] for expiration", key);
}
});
}
}
so , Periodic tasks are also performed by calling get Method to delete expired data .
Buffer lock
Halo The cache lock in the project is also an interesting module , Its function is to limit the frequency of users calling a function , It can be considered as locking the requested method . Cache locks mainly use custom annotations @CacheLock and AOP To achieve ,@CacheLock Notes are defined as follows :
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface CacheLock {
@AliasFor("value")
String prefix() default "";
@AliasFor("prefix")
String value() default "";
long expired() default 5;
TimeUnit timeUnit() default TimeUnit.SECONDS;
String delimiter() default ":";
boolean autoDelete() default true;
boolean traceRequest() default false;
}
The meaning of each member variable is :
- prefix: Used to build cacheLockKey( A string ) The prefix of .
- value: Same as prefix.
- expired: Duration of cache lock .
- timeUnit: Unit of duration .
- delimiter: Separator , structure cacheLockKey When using .
- autoDelete: Whether to delete cache lock automatically .
- traceRequest: Whether to track the requested IP, If it is , So build cacheLockKey Will add the user's IP.
The use of cache lock is to add... To the method that needs to be locked @CacheLock annotation , And then through Spring Of AOP Lock the method before it is executed , After the method is executed, cancel the lock . The facet class in the project is CacheLockInterceptor, Responsible for adding / The logic of unlocking is as follows :
Around("@annotation(run.halo.app.cache.lock.CacheLock)")
public Object interceptCacheLock(ProceedingJoinPoint joinPoint) throws Throwable {
// Get method signature
// Get method signature
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
log.debug("Starting locking: [{}]", methodSignature.toString());
// On the method CacheLock annotation
// Get cache lock
CacheLock cacheLock = methodSignature.getMethod().getAnnotation(CacheLock.class);
// To construct a cache lock key
// Build cache lock key
String cacheLockKey = buildCacheLockKey(cacheLock, joinPoint);
System.out.println(cacheLockKey);
log.debug("Built lock key: [{}]", cacheLockKey);
try {
// Get from cache
Boolean cacheResult = cacheStore
.putIfAbsent(cacheLockKey, CACHE_LOCK_VALUE, cacheLock.expired(),
cacheLock.timeUnit());
if (cacheResult == null) {
throw new ServiceException("Unknown reason of cache " + cacheLockKey)
.setErrorData(cacheLockKey);
}
if (!cacheResult) {
throw new FrequentAccessException(" Too many visits , Please try again later !").setErrorData(cacheLockKey);
}
// Methods of performing annotation modification
// Proceed the method
return joinPoint.proceed();
} finally {
// After method execution , Whether to delete cache lock automatically
// Delete the cache
if (cacheLock.autoDelete()) {
cacheStore.delete(cacheLockKey);
log.debug("Deleted the cache lock: [{}]", cacheLock);
}
}
}
@Around("@annotation(run.halo.app.cache.lock.CacheLock)")
Express , If the requested method is @CacheLock To modify , Then the server will not execute this method , But to perform interceptCacheLock Method :
- On the method CacheLock Annotate and build cacheLockKey.
- Check to see if there is cacheLockKey, If there is , So throw an exception , Remind users to visit too often . If it doesn't exist , It will be cacheLockKey Store in cache ( The effective time is expired), And execute the requested method .
- If CacheLock In the annotations autoDelete by true, Then delete immediately after the method execution cacheLockKey.
Principle and of cache lock Redis Of setnx + expire
be similar , If key Already exists , You can't add... Again . Here's the build cacheLockKey The logic of :
private String buildCacheLockKey(@NonNull CacheLock cacheLock,
@NonNull ProceedingJoinPoint joinPoint) {
Assert.notNull(cacheLock, "Cache lock must not be null");
Assert.notNull(joinPoint, "Proceeding join point must not be null");
// Get the method
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
// key The prefix of
// Build the cache lock key
StringBuilder cacheKeyBuilder = new StringBuilder(CACHE_LOCK_PREFIX);
// Separator
String delimiter = cacheLock.delimiter();
// If CacheLock The prefix is set in , Then use the prefix directly , Otherwise, use the method name
if (StringUtils.isNotBlank(cacheLock.prefix())) {
cacheKeyBuilder.append(cacheLock.prefix());
} else {
cacheKeyBuilder.append(methodSignature.getMethod().toString());
}
// Extraction quilt CacheParam The value of the variable modified by the annotation
// Handle cache lock key building
Annotation[][] parameterAnnotations = methodSignature.getMethod().getParameterAnnotations();
for (int i = 0; i < parameterAnnotations.length; i++) {
log.debug("Parameter annotation[{}] = {}", i, parameterAnnotations[i]);
for (int j = 0; j < parameterAnnotations[i].length; j++) {
Annotation annotation = parameterAnnotations[i][j];
log.debug("Parameter annotation[{}][{}]: {}", i, j, annotation);
if (annotation instanceof CacheParam) {
// Get current argument
Object arg = joinPoint.getArgs()[i];
log.debug("Cache param args: [{}]", arg);
// Append to the cache key
cacheKeyBuilder.append(delimiter).append(arg.toString());
}
}
}
// Whether to add the requested IP
if (cacheLock.traceRequest()) {
// Append http request info
cacheKeyBuilder.append(delimiter).append(ServletUtils.getRequestIp());
}
return cacheKeyBuilder.toString();
}
You can find ,cacheLockKey The structure of is cache_lock_
+ CacheLock Prefix or method signature set in annotation
+ Separator
+ CacheParam The value of the parameter modified by the annotation
+ Separator
+ Requested IP
, for example :
cache_lock_public void run.halo.app.controller.content.api.PostController.like(java.lang.Integer):1:127.0.0.1
CacheParam Same as CacheLock equally , Are annotations defined to implement cache locks .CacheParam The function of is to make the granularity of the lock accurate to the specific entity , Such as like request :
@PostMapping("{postId:\\d+}/likes")
@ApiOperation("Likes a post")
@CacheLock(autoDelete = false, traceRequest = true)
public void like(@PathVariable("postId") @CacheParam Integer postId) {
postService.increaseLike(postId);
}
Parameters postId By CacheParam modification , according to buildCacheLockKey The logic of the method ,postId It will also be cacheLockKey Part of , What's locked in this way is " by id be equal to postId Like your article " This method , Instead of locking " give the thumbs-up " Method .
Besides ,CacheLock In the annotations traceRequest Parameters are also important , If traceRequest by true, So the requested IP Will be added to cacheLockKey in , At this time, the cache lock only limits the same IP The frequency of requests for a method , Different IP Mutual non-interference . If traceRequest by false, Then the cache lock is a distributed lock , Different IP You cannot access the same function at the same time , For example, when a user likes an article , Other users cannot like this article in a short time .
Finally, let's analyze putIfAbsent Method ( stay interceptCacheLock In the called ), Its functions and functions Redis Of setnx be similar , The specific processing logic of this method can be traced to InMemoryCacheStore Class putInternalIfAbsent Method :
Boolean putInternalIfAbsent(@NonNull String key, @NonNull CacheWrapper<String> cacheWrapper) {
Assert.hasText(key, "Cache key must not be blank");
Assert.notNull(cacheWrapper, "Cache wrapper must not be null");
log.debug("Preparing to put key: [{}], value: [{}]", key, cacheWrapper);
// Lock
lock.lock();
try {
// obtain key Corresponding value
// Get the value before
Optional<String> valueOptional = get(key);
// value Not empty return false
if (valueOptional.isPresent()) {
log.warn("Failed to put the cache, because the key: [{}] has been present already",
key);
return false;
}
// Add to cache value And back to true
// Put the cache wrapper
putInternal(key, cacheWrapper);
log.debug("Put successfully");
return true;
} finally {
// Unlock
lock.unlock();
}
}
In the last section we mentioned , Custom cache InMemoryCacheStore There is one of them. ReentrantLock A member variable of type lock,lock The role of is to ensure putInternalIfAbsent Method thread safety , Because adding... To the cache container cacheLockKey It is executed by multiple threads in parallel . If you do not add lock, So when multiple threads operate on the same cacheLockKey when , Different threads may detect that there is no... In the cache cacheLockKey, therefore putInternalIfAbsent Methods return true, Then multiple threads can execute a method at the same time , add to lock This situation can be avoided after .
Conclusion
About Halo The caching mechanism is introduced here , If there is a mistake in understanding , Welcome criticism ( ̳• ◡ • ̳).