Four powerful tools of microservice architecture

Time:2020-12-5

summary

With the development of Internet applications, from single application architecture to SOA and today’s microservices, with the continuous upgrading and evolution of microservices, the stability between services and services has become more and more important. The main reason why distributed systems are complex is that distributed systems need to consider the delay and unreliability of networks. One of the most important characteristics of microservices is the need to guarantee services The most important premise to ensure idempotency is to control concurrency with distributed locks. At the same time, caching, degradation and current limiting are three powerful tools to protect the stability of microservice system.

With the continuous development of business, more and more subsystems are divided according to the business domain. Each business system needs cache, current limiting, distributed lock and idempotent tool components. The distributed tools component (not yet open source) formally contains the basic functional components required by the above-mentioned distributed system.

The distributed tools component provides two springboot starters based on tail and redis, which are very simple to use.

Take using redis as an example, application.properties Add the following configuration

redis.extend.hostName=127.0.0.1
redis.extend.port=6379
redis.extend.password=pwdcode
redis.extend.timeout=10000

redis.idempotent.enabled=true

In the following section, we will focus on the usage of cache, current limiting, distributed lock and idempotent.

cache

The use of cache can be said to be everywhere. In terms of the access path of application requests, users, browsers, reverse proxy, web server, application, database, and so on, almost every link is filled with the use of cache. The most straightforward explanation of cache is the “space for time” algorithm. Caching is to temporarily store some data in some places, which may be memory or hard disk. In short, the goal is to avoid some time-consuming operations. Our common time-consuming operations, such as database query, some data calculation results, or to reduce the pressure on the server. In fact, reducing the pressure is also due to query or calculation. Although it takes a short time, the operation is very frequent and the accumulation is very long, resulting in serious queuing and other situations, and the server can not resist.

The distributed tools component provides a cacheengine interface, which has different implementations based on TAIR and redis. The specific definition of cacheengine is as follows:

public String get(String key);

    /**
     *Get the object corresponding to the specified key. The exception will also return null
     * 
     * @param key
     * @param clazz
     * @return
     */
    public <T> T get(String key, Class<T> clz);

    /**
     *Store cache data, ignore expiration time
     * 
     * @param key
     * @param value
     * @return
     */
    public <T extends Serializable> boolean put(String key, T value);

    /**
     *Store cache data
     * 
     * @param key
     * @param value
     * @param expiredTime
     * @param unit
     * @return
     */
    public <T extends Serializable> boolean put(String key, T value, int expiredTime, TimeUnit unit);

    /**
     *Delete cache data based on key
     * 
     * @param key
     * @return
     */
    public boolean invalid(String key);

The get method queries the key, puts stores the cache data, and invalid deletes the cache data.

Current limiting

In the distributed system, especially in some scenarios of second killing and high concurrency, some current limiting measures are needed to ensure the high availability of the system. Generally speaking, the purpose of current limiting is to protect the system by limiting the concurrent access / request or the request within a time window. Once the limit rate is reached, it can refuse service (direct to the error page or tell the resource is not available), queue or Waiting (e.g. seckill, comment, order), demotion (return to backing data or default data, e.g. item details page, stock is available by default).

Some common current limiting algorithms include fixed window, sliding window, leaky bucket and token bucket. At present, the distributed tools component only implements fixed window algorithm based on counter. The specific usage is as follows:

/**
     *Specify the expiration time auto increment counter, the default is + 1 per time, non sliding window
     * 
     *@ param key counter auto increment key
     *@ param expireTime expiration time
     *@ param unit time unit
     * @return
     */
    public long incrCount(String key, int expireTime, TimeUnit unit);

    /**
     *Specifies the expiration time auto increment counter. Ratethreshold returns true if it exceeds the maximum value per unit time, otherwise it returns false
     * 
     *@ param key current limiting key
     *@ param ratethreshold
     *@ param expireTime fixed window time
     *Unit time
     * @return
     */
    public boolean rateLimit(final String key, final int rateThreshold, int expireTime, TimeUnit unit);

The ratelimit method based on cacheengine can realize current limiting. ExpireTime can only set fixed window time and non sliding window time.
In addition, the distributed tools component provides a template ratelimittemplate, which can simplify the ease of use of current limiting, and can directly call the execute method of ratelimittemplate to handle the current limiting problem.

/**
     *@ param limitkey current limiting key
     *@ param resultsupplier callback method
     *@ param ratethreshold
     *@ param limittime limit time
     *@ param blockduration blocking period
     *Unit time
     *@ param errcodeenum specifies the current limiting error code
     * @return
     */
    public <T> T execute(String limitKey, Supplier<T> resultSupplier, long rateThreshold, long limitTime,
                         long blockDuration, TimeUnit unit, ErrCodeEnum errCodeEnum) {
        boolean blocked = tryAcquire(limitKey, rateThreshold, limitTime, blockDuration, unit);
        if (errCodeEnum != null) {
            AssertUtils.assertTrue(blocked, errCodeEnum);
        } else {
            AssertUtils.assertTrue(blocked, ExceptionEnumType.ACQUIRE_LOCK_FAIL);
        }

        return resultSupplier.get();
    }

In addition, the distributed tools component also provides the usage of annotation @ ratelimit. The specific annotation ratelimit is defined as follows:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface RateLimit {

    /**
     *Current limiting key
     */
    String limitKey();

    /**
     *The number of times that can be accessed. The default value is max_ VALUE
     */
    long limitCount() default Long.MAX_VALUE;

    /**
     *Time period
     */
    long timeRange();

    /**
     *Blocking period
     */
    long blockDuration();

    /**
     *Time unit. The default is seconds
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;
}

The code of current restriction based on annotation is as follows:

@RateLimit(limitKey = "#key", limitCount = 5, timeRange = 2, blockDuration = 3, timeUnit = TimeUnit.MINUTES)
public String testLimit2(String key) {
    ..........
    return key;
}

Any method adding the above annotation has certain current limiting capability (the specific method needs to be within the interception range specified by spring AOP). The above code indicates that the parameter key is used as the current limiting key, and the number of requests is not more than 5 times every 2 minutes. If the limit is exceeded, it will be blocked for 3 minutes.

Distributed lock

In Java single process, we can control concurrent access to resources in multi-threaded environment by using synchronized keyword and reentrantlock reentrant lock. Generally, local locking can not meet our needs. We are more faced with the scenario of cross process lock in distributed system, which is called distributed lock. Distributed lock is usually implemented by storing the lock mark in the memory, but the memory is not the memory allocated by a process, but public memory such as redis and TAIR. As for the implementation of locking with databases and files, it is the same as that of a single machine, as long as the marks can be mutually exclusive. The main reason why the distributed lock is more complex than that of the single process is that the delay and unreliability of the network need to be considered in the distributed system.

The distributed locks provided by the distributed tools component should have the following features:
Mutual exclusion:Like local locks, they are mutually exclusive, but distributed locks need to ensure the mutual exclusion of different threads in different node processes.
Reentrant ability:If the same thread on the same node obtains the lock, it can also acquire the lock again.
Lock timeout:Like the local lock, it supports lock timeout to prevent deadlock. The asynchronous heartbeat demon thread refreshes the expiration time to prevent deadlock in special scenarios (such as FGC deadlock timeout).
High performance, high availability:Locking and unlocking need high performance, and also need to ensure high availability to prevent distributed lock failure, which can increase degradation.
Supports blocking and non blocking:Like reentrantlock, lock, trylock and trylock (long timeout) are supported.
Fair lock and unfair lock (not supported): Fair locks are obtained in the order of request locking, while unfair locks are unordered. At present, the distributed locks provided by the distributed tools component do not support this feature.

The distributed lock provided by the distributed tools component is very simple to use. It provides a distributed lock template: distributedlocktemplate, which can directly call the static methods provided by the template (as follows:

/**
     *Distributed lock processing template executor
     * 
     *@ param lockkey distributed lock key
     *@ param resultsupplier distributed lock handling callback
     *@ param waittime lock waiting time
     *Unit time
     *Special error code returned by enerrum @ coderum
     * @return
     */
    public static <T> T execute(String lockKey, Supplier<T> resultSupplier, long waitTime, TimeUnit unit,
                                ErrCodeEnum errCodeEnum) {
        AssertUtils.assertTrue(StringUtils.isNotBlank(lockKey), ExceptionEnumType.PARAMETER_ILLEGALL);
        boolean locked = false;
        Lock lock = DistributedReentrantLock.newLock(lockKey);
        try {
            locked = waitTime > 0 ? lock.tryLock(waitTime, unit) : lock.tryLock();
        } catch (InterruptedException e) {
            throw new RuntimeException(String.format("lock error,lockResource:%s", lockKey), e);
        }
        if (errCodeEnum != null) {
            AssertUtils.assertTrue(locked, errCodeEnum);
        } else {
            AssertUtils.assertTrue(locked, ExceptionEnumType.ACQUIRE_LOCK_FAIL);
        }
        try {
            return resultSupplier.get();
        } finally {
            lock.unlock();
        }
    }

idempotent

Idempotency design is very important in the design of distributed system, especially in complex microservices. A set of system contains multiple subsystem services, and one subsystem service often calls another service, and the service invocation service is nothing more than RPC communication or restful. Network delay or interruption in distributed system can not be avoided, which usually leads to services The call layer of the. The interface with this property always adheres to the idea that when the interface is called abnormally and repeatedly tried, it will always cause the loss that the system can not bear, so it must be prevented from happening.

Idempotent usually has two dimensions:
1. Idempotency in spatial dimension, that is, the range of idempotent objects, whether it is an individual or an organization, a certain transaction or a certain type of transaction.
2. The idempotent in time dimension, that is, the guarantee time of idempotent, is several hours, days or permanent.

There are many operations in the actual system, no matter how many operations, they should produce the same effect or return the same results. The following application scenarios are also common ones:
1. The front end repeatedly submits the request, and the request data is the same, the background needs to return the same result corresponding to the request.
2. When a payment request is initiated, the payment center should only deduct money from the user’s account once, and only once in case of network interruption or system abnormality.
3. Send a message, and send a message with the same content to the user only once.
4. Create a business order. Only one business request can be created at a time. If you try to create multiple business orders, there will be a big problem.
5. Message idempotent processing based on msgid

Before formally using the idempotent provided by the distributed tools component, let’s take a look at the design of the idempotent component of distributed tools.

Four powerful tools of microservice architecture

  • Idempotent key extraction ability: obtain unique idempotent key

    Idempotent key extraction supports 2 annotations: idempotent txid and idempotent txidgetter. Add the above two annotations in any method to extract the relevant idempotent key. The precondition is that the idempotent annotation needs to be added to the relevant idempotent method.

If only idempotent template is used for business processing, the relevant idempotent key should be set and its uniqueness should be guaranteed.

  • Distributed lock service capability: provide global locking and unlocking capabilities

    Distributed tools idempotent components need to use their own distributed lock function to ensure its concurrency and uniqueness. Distributed tools provides distributed locks to provide reliable and stable locking and unlocking capabilities.

  • High performance write and query capabilities: query and store idempotent results

Distributed tools idempotent component provides storage implementation based on TAIR and redis, and supports user-defined primary and secondary storage through spring dependency injection into idempotent service. It is suggested that distributed tools idempotent storage results store tail MDB in the first level, LDB or tablestore in the second level. The first level storage ensures its high performance, and the second level storage ensures its reliability.

Secondary storage parallel query will return the fastest idempotent result of the query.

Parallel asynchronous writing of secondary storage further improves performance.

  • High availability of idempotent write and query capabilities: idempotent storage exception, does not affect the normal business process, increase fault tolerance

Distributed tools idempotent component supports secondary storage. In order to ensure its high availability, after all, the probability of secondary storage failure is too low, which will not lead to business unavailability. If secondary storage fails at the same time, certain fault tolerance is made in the business, and the specific idempotent method will be implemented.

The write and query processing of primary storage and secondary storage are isolated, and any exception of primary storage will not affect the overall business execution.

After understanding the idempotency of the distributed tools component, let’s take a look at how to use idempotent components. First, we will learn about idempotent annotations provided by common API. The specific usage of idempotent annotations is as follows:

Four powerful tools of microservice architecture

Priority of idempotent interceptor to obtain idempotent ID:

  1. First, judge whether the spelkey property of idempotent is empty. If not, idempotent ID will be generated according to the spring expression defined by spelkey.
  2. Secondly, judge whether the parameter contains the idempotent txid annotation. If there is an idempotent txid, the parameter value will be directly obtained to generate idempotent ID.
  3. Thirdly, whether the parameter object attribute contains the idempotent txid annotation is obtained through reflection. If the object attribute contains the idempotent txid annotation, the parameter object attribute will be obtained to generate idempotent ID.
  4. Finally, if the idempotent ID is not obtained in the above three cases, whether the method of the parameter object defines the idempotent txidgetter annotation is further obtained by reflection. If the annotation is included, the idempotent ID is generated by reflection.

Code usage example:

    @Idempotent(spelKey = "#request.requestId", firstLevelExpireDate = 7,secondLevelExpireDate = 30)
    public void execute(BizFlowRequest request) {
       ..................
    }

For example, the above code indicates that the requestid is obtained from the request as an idempotent key. The primary storage is valid for 7 days, and the secondary storage is valid for 30 days.

In addition to using idempotent annotations, idempotent components also provide a general idempotent template, idempotent template, which must be set before using idempotent templates tair.idempotent.enabled=true perhaps redis.idempotent.enabled=true , the default is false. At the same time, you need to specify the first level storage of idempotent results, and the storage of idempotent results is optional.
The method to use idempotent template idempotent template is as follows:

/**
     *Idempotent template processor
     *
     *@ param request idempotent request information
     *@ param executesupplier idempotent processing callback function
     *@ param resultpreprocessconsumer idempotent result callback function can preprocess the result
     *The @ param ifresultneedidempotent needs to determine whether idempotency is required according to the results in addition to exceptions. This parameter can be provided in scenarios where idempotency is required
     * @return
     */
    public R execute(IdempotentRequest<P> request, Supplier<R> executeSupplier,
                     Consumer<IdempotentResult<P, R>> resultPreprocessConsumer, Predicate<R> ifResultNeedIdempotence) {

      ........
    }

request:
Idempotent parameter idempotent request assembly, idempotent parameter and idempotent unique ID can be set

executeSupplier:
Specific idempotent method logic, such as payment and single interface, can be processed through jdk8 functional interface supplier callback.

resultBiConsumer:
Idempotent returns the processing of the result. This parameter can be null. If it is null, the default processing is adopted. According to the idempotent result, if the exception error code is successful and unretrievable, the result will be returned directly. If it fails, the exception code can be retried, and the processing will be retried.
If the value of this parameter is not empty, you can set resultstatus (resultstatus includes three states, including success, failure can be retried, and failure can not be retried) for idempotent results.


Author: middleware brother

Read the original

This article is the original content of yunqi community, which can not be reproduced without permission.