10 How to Effectively Read Hot Data Such as Billing Rules Using Distributed Caching

10 How to Effectively Read Hot Data Such as Billing Rules Using Distributed Caching #

The previous chapters focused on the business functions of the member and points modules and introduced common features such as service maintenance, configuration center, circuit breaker, and service invocation. In this chapter, we will move on to the core business module - parking billing, which has two types of data with high exposure rates: available parking spaces before entry and billing rules. These are used by almost every car that enters and exits, and are commonly referred to as hot data: frequently used data. It is obvious that reading from the relational database is not the optimal solution, so we need to introduce caching.

Distributed Cache #

Here we only discuss the software server cache and do not involve the hardware part. As one of the two major “killers” in Internet distributed development (the other being message queues), caching has a wide range of applications. In cases with high concurrency and high performance, caching is almost always present.

img

From the perspective of the integration of applications and caching, it can be divided into local cache and distributed cache.

We often use Tomcat as the application server, where user session storage is actually a cache. It is just a local cache. If you need to implement session applications across multiple Tomcat instances, you need cooperation from other components. In Java, we often use objects like HashMap or ConcurrentHashMap for storage, which are also forms of local cache. The Ehcache and Google Guava Cache components can also implement local caching. Local caching is more common in monolithic applications, with clear advantages such as extremely fast access speed, but also clear disadvantages such as inability to cross instances and limited capacity.

In distributed scenarios, the disadvantages of local caching become more prominent, and distributed caching is more suitable for this role. By separating software applications from caching, multiple applications can share the cache, and capacity expansion is relatively simple. There are two open-source distributed cache products: memcached and Redis. Let’s briefly introduce these two products.

Memcached is an early cache product that only supports basic key-value storage. It has a relatively simple data structure and does not provide persistence. After a failure and restart, data cannot be recovered. It does not have a successful distributed solution on its own and needs to rely on other components to complete one. With the emergence of Redis, it directly outperformed memcached and its market share continues to rise.

Redis provides efficient caching while also supporting persistence, so data can be recovered after a failure. It supports more data types, such as string, list, set, sorted set, hash, etc. Redis itself provides a cluster solution, and it can also be achieved through third-party components such as Twemproxy or Codis, which account for a large proportion in actual product applications. In addition, Redis has a rich set of client resources and supports nearly 50 development languages.

In this case, Redis is used to store hot data. In more complex business scenarios, local caching and distributed caching can be used in combination.

Redis Application #

Redis Installation and Configuration #

Official website: https://redis.io/. The latest version is currently 5.0.7. Redis provides a rich set of data types and functions, which can basically cover daily development and operation needs. Its simple command line interface has a very low learning curve, allowing users to quickly get started. Redis also provides a rich set of language clients for integration into projects.

img

(Image source: Redis official website, https://redis.io/clients)

Next, let’s introduce how to install Redis:

  1. Download the compiled binary installation package. In this example, version 4.0.11 is used.
$ wget http://download.redis.io/releases/redis-4.0.11.tar.gz
$ tar xzf redis-4.0.11.tar.gz
$ cd redis-4.0.11
$ make
  1. Configuration: By default, Redis has weak security settings, with no password configured and a port that is easy to scan. If you want to change the default configuration, you can modify the redis.conf file.

You can modify the default port 6379 #

port 16479

By default, Redis does not run as a background program, so the switch needs to be turned on #

daemonize yes

Enable password development and set a password #

requirepass zxcvbnm,./

  1. Start Redis

When starting, load the configuration file #

appledeMacBook-Air:redis-4.0.11 apple$ src/redis-server redis.conf 59464:C 07 Mar 10:38:15.284 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo 59464:C 07 Mar 10:38:15.285 # Redis version=4.0.11, bits=64, commit=00000000, modified=0, pid=59464, just started 59464:C 07 Mar 10:38:15.285 # Configuration loaded

Command line test #

appledeMacBook-Air:redis-4.0.11 apple$ src/redis-cli -p 16479

You must execute the auth command and enter the password, otherwise the command cannot be used properly #

127.0.0.1:16479> auth zxcvbnm,./ OK 127.0.0.1:16479> dbsize (integer) 51 127.0.0.1:16479>

With this, the installation of the Redis service is complete. Next, you can integrate the caching function into your project. Some may say that operating Redis through the command line is not as intuitive as using a graphical management interface. Active users have already provided corresponding tools for everyone to use, such as Redis Manager.

Integrating Spring Data Redis #

In this practice, we will use the Spring Data Redis component from the Spring Data project family to communicate with the Redis Server. When integrating with a Spring Boot project, we will use the starter style.

Spring Boot Data Redis depends on the Jedis or Lettuce client. It provides a set of client-agnostic APIs based on Spring Boot, which allows you to easily switch from one Redis client to another without modifying your business code.

The billing service corresponds to the parking-charging module. In the pom.xml file, import the corresponding jar. You may wonder why there is no version specified here. It’s because it has already been specified in the spring-boot-dependencies configuration, so there is no need to configure it separately.

<!-- Mouse over for information prompt: The managed version is 2.1.11.RELEASE The artifact is managed in org.springframework.boot:spring-boot-dependencies:2.1.11.RELEASE -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

You can configure it using Java code with the [@Configuration] annotation, or you can use a configuration file. Here, we will use the configuration file approach. Configure the Redis connection in the application.properties file. Here, we specifically specify the database. Redis has 16 databases by default, numbered from 0 to 15. They provide effective data isolation to prevent contamination.

#redis config
spring.redis.database=1
spring.redis.host=localhost
spring.redis.port=16479
#default redis password is empty
spring.redis.password=zxcvbnm,./
spring.redis.timeout=60000
spring.redis.pool.max-active=1000
spring.redis.pool.max-wait=-1
spring.redis.pool.max-idle=10
spring.redis.pool.min-idle=5

Based on the principle of convention over configuration of Spring Boot, after configuring it this way, Redis has already been successfully integrated into the project.

Write the service class RedisServiceImpl.java. By using the RedisTemplate provided by the Spring Boot Data Redis project, you can communicate and interact with Redis. In this example, we will only use the simple key-value format based on string data.

@Slf4j
@Service
public class RedisServiceImpl implements RedisService {

    @Autowired
    RedisTemplate<Object, Object> redisTemplate;

    @Override
    public boolean exist(String chargingrule) {
        ValueOperations<Object, Object> valueOperations = redisTemplate.opsForValue();
        return valueOperations.get(chargingrule) != null ? true : false;
    }

    @Override
    public void cacheObject(String chargingrule, String jsonString) {
        redisTemplate.opsForValue().set(chargingrule, jsonString);
        log.info("chargingRule cached!");

}

}

}

Redis supports more diverse data types compared to memcached. The RedisTemplate API also provides corresponding operation methods, as follows:

img

Load data into cache #

When the project is first started, how do we write the database into the cache? It is recommended to load the cache when the project starts, and then flush the cache after the data changes. Spring Boot provides two ways to load the cache when the project starts: ApplicationRunner and CommandLineRunner. Both execute the run method after the Spring container is initialized, and the most obvious difference between them is the parameter.

In this example, the ApplicationRunner method is used.

Initialize the billing rule cache:

@Component
@Order(value = 1) //order is the loading order, the smaller the number, the earlier it loads. If there are dependencies, it is recommended to arrange them in order
public class StartUpApplicationRunner implements ApplicationRunner {

    @Autowired
    RedisService redisService;

    @Autowired
    ChargingRuleService ruleService;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        List<ChargingRule> rules = ruleService.list();
        //ParkingConstant is a constant class in the project
        if (!redisService.exist(ParkingConstant.cache.chargingRule)) {
            redisService.cacheObject(ParkingConstant.cache.chargingRule, JSONObject.toJSONString(rules));
        }
    }
}

After the project starts, check if there is any data in the cache using the redis client.

appledeMacBook-Air:redis-4.0.11 apple$ src/redis-cli -p 16479
127.0.0.1:16479> auth zxcvbnm,./
OK
127.0.0.1:16479> select 1
OK
127.0.0.1:16479[1]> keys *
1) "\xac\xed\x00\x05t\x00\aruleKey"

The Key value is preceded by a bunch of gibberish like \xac\xed\x00\x05t\x00\a, which is unicode encoding. Because the default serialization method of redisTemplate is jdkSerializeable, it stores binary bytecode when storing, but this does not affect the data. Here, we need to change the serialization method so that we can read it normally.

@Component
public class RedisConfig {

    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);

        ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);

jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// Reset the value serialization method
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
// Reset the key serialization method. The default serialization method of StringRedisTemplate is StringRedisSerializer
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}

Clear the billing rules and use flushdb (use with caution, as it will clear all data under the current database. Another flush command will clear all databases, so be even more cautious) to restart the project and reload the billing rules.

img

appledeMacBook-Air:redis-4.0.11 apple$ src/redis-cli -p 16479
127.0.0.1:16479> auth zxcvbnm,./
OK
127.0.0.1:16479> select 1
OK
127.0.0.1:16479[1]> keys *
1) "ruleKey"
127.0.0.1:16479[1]> get ruleKey
"\"[{\\\"createBy\\\":\\\"admin\\\",\\\"createDate\\\":1577467568000,\\\"end\\\":30,\\\"fee\\\":0.0,\\\"id\\\":\\\"41ed927623cf4a0bb5354b10100da992\\\",\\\"remark\\\":\\\"30分钟内免费\\\",\\\"start\\\":0,\\\"state\\\":1,\\\"updateDate\\\":1577467568000,\\\"version\\\":0},{\\\"createBy\\\":\\\"admin\\\",\\\"createDate\\\":1577467572000,\\\"end\\\":120,\\\"fee\\\":5.0,\\\"id\\\":\\\"41ed927623cf4a0bb5354b10100da993\\\",\\\"remark\\\":\\\"2小时内,5元\\\",\\\"start\\\":31,\\\"state\\\":1,\\\"updateDate\\\":1577467572000,\\\"version\\\":0},{\\\"createBy\\\":\\\"admin\\\",\\\"createDate\\\":1577468046000,\\\"end\\\":720,\\\"fee\\\":10.0,\\\"id\\\":\\\"4edb0820241041e5a0f08d01992de4c0\\\",\\\"remark\\\":\\\"2小时以上12小时以内,10元\\\",\\\"start\\\":121,\\\"state\\\":1,\\\"updateDate\\\":1577468046000,\\\"version\\\":0},{\\\"createBy\\\":\\\"admin\\\",\\\"createDate\\\":1577475337000,\\\"end\\\":1440,\\\"fee\\\":20.0,\\\"id\\\":\\\"7616fb412e824dcda41ed9367feadfda\\\",\\\"remark\\\":\\\"12小时至24小时,20元\\\",\\\"start\\\":721,\\\"state\\\":1,\\\"updateDate\\\":1577475337000,\\\"version\\\":0}]\""

At this point, the key is displayed correctly, but the value corresponding to the key still shows Unicode encoding. You can add the _—raw_ parameter in the command line to view Chinese characters. The complete command line: src/redis-cli -p 16479 —raw, and Chinese characters will be displayed normally in the client.

img

Calculate fees using cached billing rules #

When a vehicle exits, the parking time needs to be calculated. Based on the long-term parking time, the specific billing rule is matched to calculate the fee, and then the payment record is written.

/**
 * @param stayMintues
 * @return
 */
private float caluateFee(long stayMintues) {
    String ruleStr = (String) redisService.getkey(ParkingConstant.cache.chargingRule);
    JSONArray array = JSONObject.parseArray(ruleStr);
    List<ChargingRule> rules = JSONObject.parseArray(array.toJSONString(), ChargingRule.class);
    float fee = 0;
    for (ChargingRule chargingRule : rules) {
        if (chargingRule.getStart() <= stayMintues && chargingRule.getEnd() > stayMintues) {
            fee = chargingRule.getFee();
            break;
        }
    }
    return fee;
}

Since the transaction pressure for parking fees is not very high, this is just an example application, and the difference between reading from the database and reading from the cache is not significant. Imagine the scenario of deducting fees from mobile phones. If you still read the data from the relational database and then calculate the fees, the difference would be significant.

As a distributed cache, Redis is separate from the application, so any project that can establish a valid connection with Redis can access the data in the cache at will. Of course, Redis provides basic functionality as a cache, as well as many other operations, such as data sharding, distributed locks, transactions, memory optimization, message subscription/publishing, etc., to address the needs of different business scenarios.

A question for further consideration #

How to design popular product ranking lists commonly seen in e-commerce websites, such as daily bestsellers, weekly bestsellers, monthly bestsellers, and yearly bestsellers, by using Redis.