The Lua script

introduce

Lua is a lightweight scripting language written in C, similar to stored procedures for data. Redis. IO /commands/ev . Benefits of executing Redis commands using Lua scripts:

  1. Sending multiple commands at a time reduces network overhead.
  2. Redis will execute the entire script as a whole without being interrupted by other requests, keeping it atomic.
  3. For complex combined commands, we can put them in files and realize command reuse.

grammar

Syntax for Lua scripts:

eval lua-script key-num [key1 key2 key3 ...] [value1 value2 value3...]Copy the code
  • Eval stands for executing Lua commands.
  • Lua-script stands for lua script content.
  • Key-num specifies the number of keys in a parameter. Note that in Redis, the key starts with 1. If there is no key parameter, write 0.
  • [key1 key2 key3 …] Key is passed to Lua as an argument, or it can be left blank, but it must correspond to the number of key-num.
  • [value1 value2 value3…] These parameters are passed to the Lua language and are optional.

Syntax for executing redis in lua-script:

redis.call(command,key[param1,param2...] )Copy the code
  • Command is a command, including set, get, and del.
  • Key is the key being operated on.
  • param1,param2… Represents the parameter given to key.

Now that we’ve learned the above two grammars, let’s look at some examples.

eval "return 'Hello World'" 0
eval "return redis.call('set','jackxu','shuaige')" 0
eval "return redis.call('set',KEYS[1],ARGV[1])" 1 jackxu shuaige
Copy the code

  • The first is to return a Hello word without using the redis command, so the key argument is 0.
  • The second one uses the redis command, set jackxu shuaige, but the keys are fixed, so the number of keys passed in is still zero.
  • The third one is set jackxu shuaige again, but the key and the value are passed in as arguments, so the 1 that follows is the number of keys, jackxu is the key, shuaige is the value.

In the case of long Lua scripts, if the entire script needs to be passed to the Redis server each time the script is invoked, there will be a large network overhead. To solve this problem, Redis can cache Lua scripts and generate SHA1 digest codes, which can then be used to execute Lua scripts directly. The syntax is as follows:

Script load "return 'Hello World'" ## Execute the cached script evalsha via the digest code "470877a599ac74fbfda41caa908de682c5fc7d4b" 0Copy the code

Java code

The methods described above are executed on the client side of Redis, but we usually use them in code. Let’s look at how they are used in code.

The first example is a simpler implementation of the above syntax in Jedis, written the same way.

    public static void main(String[] args) {
        Jedis jedis = new Jedis("39.103.144.86".6379);
        jedis.eval("return redis.call('set',KEYS[1],ARGV[1])".1."jackxu"."shuaige");
        System.out.println(jedis.get("jackxu"));
    }
Copy the code

The second example is about limiting traffic, limiting access Y times in X seconds. I wrote 1 and 200 parameters here, which means limiting access 200 times in 1 second. In fact, this is QPS200. Because the Lua script here is very long, it takes time to send each time to the server, so we use the above second method of writing a summary to achieve.

    /** * limit traffic, limit access Y times in X seconds */
    public static void limit(a) {
        Jedis jedis = new Jedis("39.103.144.86".6379);
        // Set the expiration time for the key only the first time
        String lua = "local num = redis.call('incr', KEYS[1])\n" +
                "if tonumber(num) == 1 then\n" +
                "\tredis.call('expire', KEYS[1], ARGV[1])\n" +
                "\treturn 1\n" +
                "elseif tonumber(num) > tonumber(ARGV[2]) then\n" +
                "\treturn 0\n" +
                "else \n" +
                "\treturn 1\n" +
                "end\n";
        Object result = jedis.evalsha(jedis.scriptLoad(lua), Arrays.asList("limit.key"), Arrays.asList("1"."200"));
        System.out.println(result);
    }
Copy the code

Source: com/XHJ jedis/LuaTest. Java

Script timeout

Redis command execution itself is single-threaded. If this thread is executing Lua script, Lua script execution times out or falls into an infinite loop, which will cause other commands to enter the waiting state. The following commands

eval 'while(true) do end' 0
Copy the code

There is a configuration in Redis Config

lua-time-limit 5000
Copy the code

There is a timeout period for script execution, which is 5 seconds by default. After 5 seconds, other clients do not wait, but return “BUSY” error. But that doesn’t work either, as the while loop is stuck, and none of the other commands can be executed. At this point we need to terminate the Lua script.

Termination has two commands, script kill and shutdown nosave. Commands like the above can be terminated with script kill, but the following commands cannot be terminated.

eval "redis.call('set','jack','aaa') while true do end" 0
Copy the code

If the current Lua script is used to modify Redis data (set, del, etc.), it cannot be terminated by script kill, and an UNKILLABLE error will be returned. Because scripts are supposed to run atomically, terminating a part of the script violates the goal of atomicity. In this case, the shutdown nosave command is used to stop the reids. The shutdown nosave command is used to stop the reids.

application

Lua scripts are very important. We must use Lua scripts to ensure atomicity when implementing distributed locks. For more information about distributed locks, please refer to my other article “Introducing several common distributed lock writing methods”.

You can also use Redis INCr to limit the flow by placing a custom annotation + section on top of the method.

Why are Reids so fast

IO /topics/benc… “, let’s test it out

CD /jackxu/redis-6.2.2/ SRC Redis-benchmark -T set, lpush-n 100,000-qCopy the code

Set and LPUSH are 9.4W and 9W respectively, not much different from the 10W QPS claimed on the official website. This one is a 1-core 2G cloud server, and it will be higher if the configuration and network are good.Why can resist such a large QPS, summed up in three points:

  1. Pure memory structure
  2. Request processing single thread
  3. Multiplexing mechanism

Let’s take a look at each of them.

memory

You know redis is stored in memory, memory is fast, fast, time order one.

Single thread

We often say that Redis is single-threaded, but this is not strictly true. Redis single threading refers to the fact that the network request module uses one thread, that is, one thread to handle all network requests, while other modules that use multiple threads still use multiple threads. Since version 4.0, threads have been introduced to handle other things, such as requesting dirty data, releasing useless connections, and deleting large keys.

What is the advantage of making the main thread for processing requests single-threaded?

  1. There are no costs associated with creating and destroying threads
  2. The CPU consumption caused by context switching is avoided
  3. Avoid contention issues between threads, such as locking and releasing deadlocks

The official explanation is that single threads are sufficient in Redis and CPU is not the bottleneck of Redis. Redis bottlenecks are most likely machine memory or network bandwidth. Since single-threading is easy to implement and you don’t have to deal with concurrency, it makes sense to adopt a single-threaded solution.

Note that since processing requests is single-threaded, do not run long commands in production, such as keys, flushall, and flushdb, which will block requests.

multiplexing

First of all, what’s causing the problem?

Since the process’s execution is linear (that is, sequential), when we call low-speed system I/O(read, write, accept, and so on), the process may block and cannot perform other operations on the call. Blocking is normal. When the client and server communicate, the server requests read(sockfd1,bud,bufsize), and the client process does not send data, the read(blocking call) will block until the client writes (sockfd,but,size) sends data. This is fine when one client is communicating with the server. When multiple clients are communicating with the server, if the server blocks one client sockfd1, the server still cannot process the data from the other client when it reaches the socket sockfd2, and still blocks at read(sockfd1,…). On. At this point, the problem appears, can not handle the service of another customer in time, this time to use I/O multiplexing to solve!

When multiple clients are connected, sockfd1, sockfD2, sockfd3.. Sockfdn listens for all n clients at the same time. When one of them sends a message, it returns from the SELECT block. Then it calls read to read the sockFD that received the message, and loops back to the SELECT block. This way, you won’t be unable to process messages from the other customer because they are blocked on one. For a more detailed explanation, please refer to my article on the Five IO Models of Linux.

Here is a diagram of the model in Redis:

Redis developed its own network Event handler based on Reactor model, which is called File Event Handler. File event handler consists of Socket, IO multiplexer, file event dispatcher (Dispather) and event handler.

The I/O multiplexer listens on multiple sockets at the same time. When the socket being listened on is ready to perform accept, read, write, or close operations, the corresponding operation is specifiedFile eventsIt’s going to produce.IO multiplexing proceduresPushes all event-generating sockets into oneThe queueAnd thenOrderly at a timeOnly one socket is sent to the file event dispatcher, which calls the corresponding socket based on the event type generated by the socketEvent handlerProcess.

All the capabilities of I/O multiplexing in Redis are implemented by wrapping the common LIBRARIES of I/O multiplexing functions such as Select, Epoll, EVport, and KQueue.

  • Evport is supported by the Solaris kernel;
  • Epoll is supported by the Linux kernel.
  • Kqueue is supported by the Mac kernel;
  • Select is provided by POSIX and is supported by common operating systems (guarantee scheme).

Event handlers fall into three categories:

  • Connection response handler: used to process client connection requests;
  • Command request processor: used to execute the commands passed from the client, such as the common SET, Lpush, etc.
  • Command reply processor: used to return the execution results of client commands, such as set and get commands.

Multithreaded I/O

We know that Redis 6.0 version introduced multi-threading, here said multi-threading, in fact, is the Redis single thread to do these two things from the client to read data, write data back to the client (also known as network I/O), processing into multi-threading, but the Redis command is executed in the main thread serial execution, So there is no thread concurrency safety issue.

Here are the differences between single-threaded AND multithreaded I/O:

Expiry policies

We know that the memory of Redis is limited and it is impossible for you to keep setting in it. One day, it will be full. At this time, we need to delete expired keys to make more space. There are three types of expiration policies:

Immediate expiration (active elimination)

Each Key that is set to expire needs to create a timer that will be cleared immediately when it expires. This policy can immediately clear expired data and is memory friendly; However, it takes up a lot of CPU resources to process expired data, which affects the response time and throughput of the cache.

Inert expiration (passive obsolescence)

Only when a Key is accessed, the system checks whether the Key has expired. This policy maximizes CPU savings and is very memory unfriendly. In extreme cases, a large number of expired keys may not be accessed again and thus will not be cleared, occupying a large amount of memory.

All queries call expireIfNeeded to determine whether it is out of date: db.c line 1299

expireIfNeeded(redisDb *db,robj *key)
Copy the code

Regular date

At certain intervals, a certain number of keys in the expires dictionary of a certain number of databases are scanned and expired keys are cleared. This strategy is a compromise of the first two schemes. By adjusting the interval of periodic scan and the time limit of each scan, CPU and memory resources can be used to achieve the optimal balance in different situations.

Summary: Redis uses both lazy expiration and periodic expiration strategies. It does not purge expired keys in real time.

Elimination strategy

If none of the keys are set to expire, Redis will need to use a obsolescence strategy to determine which data is to be cleaned up to ensure that new data is stored when Redis reaches its maximum memory limit.

To set the maximum memory, in the redis.conf configuration,

# maxmemory <bytes>
Copy the code

If maxmemory is not specified or set to 0, a 32-bit system uses a maximum of 3GB memory, and a 64-bit system does not limit the memory.

Redis >config set maxmemory 2GBCopy the code

There are eight elimination strategies

  • Volatile – lRU: Expulsion of the oldest unused key from the key set with expiration time first, fallback to noeviction if there are no keys to remove.
  • Allkeys-lru: Expels the longest unused key through the LRU algorithm.
  • Volatile – lFU: Expels the least frequently used key from all keys with expiration time configured.
  • Allkeys-lfu: removes the least frequently used key from allkeys.
  • Volatile -random: Random expulsion from the set of expired keys.
  • Allkeys-random: deletes allkeys randomly.
  • Volatile – TTL: Expel keys that are about to expire from keys configured with expiration time, if not, fallback to noeviction.
  • Noeviction: Error will be returned when memory usage exceeds configuration, no key will be expelled, redis will only respond to read operations.

Volatile – LRU,volatile-random,volatile- TTL equals noeviction if TTL is not set or no key that matches the prerequisite is deprecated.

The configuration is also in redis.conf

# maxmemory-policy noeviction
Copy the code

Or dynamically modify

redis>config set maxmemory-policy volatile-lru
Copy the code

You are advised to use volatile- lRU to delete the least recently used keys while ensuring normal services.

The LRU algorithm in Redis does not use traditional hash linked lists because it requires additional data structure storage and consumes memory. The accuracy of the algorithm is adjusted by random sampling in Redis. By configuring Maxmemory_samples (5 by default), m keys are randomly selected from the database and the cached data corresponding to the keys with the lowest popularity is eliminated. Therefore, the larger the value of sampling parameter M is, the more accurate the cache data to be eliminated can be found, but it also consumes more CPU calculation and reduces the execution efficiency.

See three different bands formed by three different dots:

  • Light gray bands are objects that have been recycled (eliminated by the LRU algorithm)
  • Gray bands are objects that have not been collected
  • The green band is the newly added object

Starting from Redis 3.0, the performance of the algorithm is improved, making it closer to the real LRU algorithm. The trick is to maintain a pool of reclaim candidate keys. With the same 5 sampling points, Redis 3.0 performs better than Redis 2.8 and is close to the traditional LRU algorithm when the sample is 10. Reference connection:

Redis. IO/switchable viewer/lru -…

Github.com/redis/redis…

Persistence mechanism

We know that Redis is so fast because the data is stored in memory, but there is a risk of memory, that is, power failure or downtime will lead to the loss of data in memory. To prevent data loss after a restart, Redis provides two persistence schemes: Redis DataBase (RDB snapshot) and AOF (Append Only File). Persistence is one of the main differences between Redis and Memcache.

RDB

RDB is the default Redis persistence scheme (AOF is preferred if AOF is enabled). When certain conditions are met, data in the memory is written to disks and a snapshot file dump. RDB is generated. The Redis restart restores data by loading the dump. RDB file.

Trigger mode includes automatic trigger and manual trigger.

Automatic trigger:

A) Configure trigger rules in the redis.conf file

# rule save 900 1 #900 seconds at least one key is changed (including adding) Save 300 10 #300 seconds at least 10 keys are changed save 60 10000 #60 seconds at least 10000 keys are changed # file path dir RDB # Whether to compress RDB files with LZF rdbCompression yes # Enable rdbchecksum yesCopy the code

The above three rules are non-conflicting and will be triggered if any one of them is met. If the RDB scheme is not needed, comment save or configure it as an empty string “”. You can use the lastsave command to view the time when the last snapshot was successfully created.

B) Shutdown is triggered to ensure the normal shutdown of the server.

C) flushall, RDB file is null.

Manual trigger:

If we need to restart the service or migrate data, we need to manually trigger the RDB snapshot save at this point. Redis provides two commands, save and bgSave.

Save blocks the current Redis server while creating a snapshot. Redis cannot process other commands and can block for a long time if there is a large amount of data in memory.

Bgsave works by forking a child process to persist asynchronously in the background, and the main process can also respond to client requests. It does not record the data generated after the fork, and the blocking occurs only during the fork phase, usually for a short time.

AOF

AOF records each write operation in the form of a log and appends it to a file. When enabled, commands that change Redis data are written to AOF files. When Redis restarts, write instructions are executed from front to back according to the log file contents to complete data recovery.

AOF configuration:

Appendonly no # filename appendfilename "appendonly.aof" # aof persistent policy appendfsync everysecCopy the code

Due to the caching mechanism of the operating system, AOF data is not actually written to the hard disk, but enters the hard disk cache of the system. So we need to enable the flush cache policy.

  • Appendfsync everysec :(default) performs fsync once per second, which may result in the loss of data for 1s, but this is both safe and efficient.
  • Appendfsync no: Fsync is not executed and data is synchronized to the disk through the operating system. This speed is fast but is not secure.
  • Appendfsync always: Fsync is performed on each write to ensure that data is synchronized to disk, but this is inefficient.

I know that as more commands are executed, AOF files get bigger and bigger. Redis has added a rewriting mechanism. When the size of AOF files exceeds a set threshold, Redis starts to compress the contents of AOF files, keeping only the smallest instruction set that can recover data. This can be overridden using the command bgrewriteaof. AOF file rewriting is not to organize the original file, but directly read the server’s existing key and value pairs, and then replace the previous multiple commands that recorded this key and value pair with one command, and produce a new file to replace the original AOF file.

# aOF file growth ratio, Parameter description value auto-aof-rewrite-percentage percentage 100 # aof-rewrite-min-size 64mb auto-aof-rewrite-min-size 64mb Default no(indicates that synchronization is required) no-appendfsync-on-rewrite no # indicates that redis will ignore the last possible problem instruction when resuming, default yes aof-load-truncated yesCopy the code

Redis executes the write command during the rewrite:

Compare the two schemes

  1. Redis enables RDB persistence by default. If a specified number of write operations are performed within a specified period of time, data in the memory is written to the disk.
  2. RDB persistence is suitable for large-scale data recovery but has poor data consistency and integrity.
  3. Redis needs to manually enable AOF persistence. By default, write operation logs are appended to AOF files every second.
  4. The data integrity of AOF is higher than that of RDB, but the record content is too much, which will affect the efficiency of data recovery.
  5. Redis provides a slimming mechanism for rewriting large AOF files.
  6. If you only intend to use Redis for caching, you can turn persistence off.
  7. If you plan to use Redis persistence. It is recommended that both RDB and AOF be enabled. In fact, RDB is more suitable for data backup, leave a back-up. AOF is broken, RDB is broken.
  8. AOF files are loaded first to recover the original data, because AOF files usually hold more complete data sets than RDB files.

Source code:Github.com/xuhaoj/redi…Thanks for watching!