Cache failure problem under high concurrency - cache penetration, cache breakdown, cache avalanche, simple implementation of Redis distributed lock, Redisson implementation of distributed lock

Several issues exposed by the basic usage paradigm of cache

{
	1、先查询缓存
	2、if(缓存没有命中){
		2.1、查询数据库
		2.2、查询结果放入缓存
		2.3、同时return结果
	}
	3、缓存命中直接return缓存数据
}

As follows; using cache to efficiently query ‘three-level classification’ data completely follows the paradigm mentioned above.

    public Map<Long, List<Catalog2VO>> getCatalogJsonBaseMethod() {
    
    
        String key = ProductConstant.RedisKey.INDEX_CATEGORY_JSON;
        // 1、从缓存中获取数据
        String categoryListFromCache = redisTemplate.opsForValue().get(key);
        if (!StringUtils.hasText(categoryListFromCache)) {
    
    
            // 2.1、缓存没有命中,查询数据库
            Map<Long, List<Catalog2VO>> catalogJsonFromDB = getCatalogJsonFromDB();
            // 2.2、将查询结果放入缓存
            redisTemplate.opsForValue().set(key,JSON.toJSONString(catalogJsonFromDB));
            return catalogJsonFromDB;
        }
        // 3、缓存命中便直接return
        return JSON.parseObject(categoryListFromCache, new TypeReference<>() {
    
    });
    }

This paradigm will expose the following problems under high concurrency and distribution, which are also the points that need to be solved and discussed in this chapter.

  • Cache penetration of high concurrency cache invalidation
  • Cache breakdown of high concurrency cache failure
  • High concurrency cache failure cache avalanche
  • Distributed lock under distributed architecture

Cache invalidation problem—cache penetration

Request to query data that 100% does not exist

Assumeid=idooy that this record does not exist in the database at all; according to the request processing logic先查询缓存, but because this is a record that does not exist ( Assuming that it is true), it is impossible for the cache to hit, 缓存不命中接着就会查询数据库;如果没有将这一次请求查询的null写入缓存, which will cause id=idooy this request to go to the database every time, directly losing the meaning of caching< /span>

Risk: Using non-existent data to send a large number of requests, the instantaneous pressure on the database increases, eventually causing the database to crash
Solution: Cache the null result and add a short expiration time; sometimes when querying a fixed value, the request does not need to carry parameters. In this case, cache penetration will not occur

Cache invalidation problem—cache breakdown

A certain Key just expired during a period of high concurrent requests.

For a Key with an expiration time set, if the Key happens to expire during a period of high concurrent access at some time in the future, then the high concurrent request pressure will be directly given to the database
Solution: 加锁; Highly concurrent requests for the same Key ensure that only one request is sent to the database; other requests wait and are finally obtained from the cache; discussed below单机锁 and分布式锁

1. Single machine lock

单机锁It refers to the use of lock exclusivity in a single application or in the same process to ensure that when a certain Key fails during high concurrency, only one request goes to the database for query to avoid cache breakdown.

The code implementation is as follows:

    @Override
    public Map<Long, List<Catalog2VO>> getCatalogJson() {
    
    
        String key = ProductConstant.RedisKey.INDEX_CATEGORY_JSON;
        // 1、从缓存中获取数据
        String categoryListFromCache = redisTemplate.opsForValue().get(key);
        if (!StringUtils.hasText(categoryListFromCache)) {
    
    
            // 2、缓存没有命中,查询数据库,加锁保证数据库只查询一次
            // 因为当前this实例为单例,故可以作为锁资源使用
            synchronized (this) {
    
    
                // 2.1、高并发下必然有N个请求同时等待竞争锁,所以竞争到锁的第一件事就是再查一遍缓存
                String result = redisTemplate.opsForValue().get(key);
                if (StringUtils.hasText(result)) {
    
    
                    return JSON.parseObject(result, new TypeReference<>() {
    
    });
                }
                // 2.2、缓存依旧没有命中的情况下查询数据库
                Map<Long, List<Catalog2VO>> catalogJsonFromDB = getCatalogJsonFromDB();
                // 2.3、将查询结果放入缓存
                redisTemplate.opsForValue().set(key,JSON.toJSONString(catalogJsonFromDB),2,TimeUnit.HOURS);
                return catalogJsonFromDB;
            }
        }
        // 3、缓存命中便直接return
        return JSON.parseObject(categoryListFromCache, new TypeReference<>() {
    
    });
    }

Correct lock granularity

Insert image description here

Incorrect lock granularity cannot guarantee that the number of database queries is unique

Insert image description here
Insert image description here

2. Distributed lock

The essence of the above stand-alone lock is to use a singleton object in the current process as a lock resource; under the distributed deployment of microservice architecture, the same product service may be deployed N multiple times, and each service process is isolated from each other.

Insert image description here
Therefore; local locks can only lock the current process, and distributed locks are required under distributed architecture.

getCatalogJsonData()

    private Map<Long, List<Catalog2VO>> getCatalogJsonData() {
    
    
        String key = ProductConstant.RedisKey.INDEX_CATEGORY_JSON;
        String result = redisTemplate.opsForValue().get(key);
        if (!StringUtils.hasText(result)) {
    
    
            Map<Long, List<Catalog2VO>> catalogJsonFromDB = getCatalogJsonFromDB();
            redisTemplate.opsForValue().set(key,JSON.toJSONString(catalogJsonFromDB),2,TimeUnit.HOURS);
            return catalogJsonFromDB;
        }
        return JSON.parseObject(result, new TypeReference<>() {
    
    
        });
    }

Distributed lock evolution—basic principles

All 'goods and services' can go to one place at the same time to "occupy a pit". If they occupy it, the logic will be executed, otherwise they must wait until the lock is released.
You can go to Redis to "take advantage of the pit", or you can go to the database, or you can go to any place that can be accessed by "goods and services"
Insert image description here

Distributed lock (locking) evolution one: failure to delete lock leads to deadlock

Insert image description here
As shown in the figure above; an exception occurs when executing business logic or the system crashes before deleting the lock (kill -9); which directly results in the lock deletion operation not being executed. Then other requests will not be able to "successfully occupy the lock", causing a deadlock.

Next, set the expiration time for the "lock" to prevent deadlock. Even if the deletion of the lock fails, it will be automatically deleted.

Distributed lock (locking) evolution 2: Set expiration time for ‘lock’ to prevent deadlock

Insert image description here
Therefore, "occupying the lock + setting the expiration time" must ensure atomicity

Distributed lock (locking) evolution three: expiration time and lock-occupying action atomicity must be guaranteed

Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "ok",3,TimeUnit.SECONDS);

Insert image description here

Distributed lock (unlocking) evolution 1: Business logic execution time is greater than the expiration time of the ‘lock’

The execution time of the business logic exceeds the expiration time of the 'lock'; this means that after the execution of the business logic is completed, it is not your own lock that is deleted.
Imagine the following high concurrency scenario, assuming that the expiration time of the ‘lock’ is 10s and the execution time of the business is 15s;

Request No. 1 is executed to the 10th second, and the 'lock' automatically expires; Request No. 2 immediately occupies the lock successfully and executes the business logic.
At 15s①, the business logic is executed and the lock is successfully deleted. Obviously, what No. 1 deletes at this time is not his own lock (his own lock was automatically deleted at the 10th second), but the lock No. 2.
At the same time, at 15 seconds, No. 1 deleted the lock of No. 2; then No. 3 successfully occupied the lock , and so on In case 'lock permanently expires'

Insert image description here
该况下暴露的问题本质就是锁删除了他人的锁; Then use the unique ID to ensure that the thread deletes its own lock.

Distributed lock (unlocking) evolution 2: UUID ensures that you delete your own 'lock'

When occupying the lock, the value is specified as uuid. Everyone will delete it only if it matches their own lock.
Insert image description here
As shown in the figure; the problem is still exposed. get("lock") and equals is established. At this time, the lock has just expired automatically and been deleted. Another thread has successfully occupied the lock. At this time, executing delete to delete the lock will also delete the lock that is not your own.
So the essence of this problem is 删锁的过程不能保证原子性

Distributed lock (unlocking) evolution three: Lua script ensures atomicity of 'lock' deletion

As shown below; the official provides ‘unlocking’ suggestions and Lua steps to ensure the atomicity of the unlocking process.

  • The value of the lock should not be set to a fixed string, but to a large random string that cannot be guessed, called a token.
  • Instead of releasing the lock with DEL, send a script that only deletes the key if the value matches
    Insert image description here

According to official tips; unlocked core business code snippets

// 解锁
redisTemplate.execute(new DefaultRedisScript<>(getLuaScript(),Long.class),Arrays.asList("lock"),uuid);

private String getLuaScript(){
    
    
        return "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
                "then\n" +
                "    return redis.call(\"del\",KEYS[1])\n" +
                "else\n" +
                "    return 0\n" +
                "end";
    }

Insert image description here

3. Automatic renewal of locks

The business execution time is too long; the ‘lock’ has automatically expired before the business logic has been executed. The simplest way is to set the ‘lock’ for a long enough time.
But it is still very difficult to write the code yourself to solve this problem perfectly, so this problem is raised Redisson , the distributed lock it provides will solve all the problems mentioned above; including automatic renewal of locks

4. Redis simply implements the complete code of distributed locks

  • Lock atomicity command; ensure that 'setting expiration time and lock occupation' is an atomic operation
  • Unlock atomicity command; uuid ensures that the lock is deleted; lua script ensures the atomicity of lock deletion
  • Set the expiration time of the 'lock' long enough to ensure that the execution time of the business logic will not exceed the expiration time. This is a simple and crude way to solve the problem of automatic renewal of the 'lock' when it expires.
private Map<Long, List<Catalog2VO>> getCatalogJsonWithRedisLock() {
    
    
        // 所有的请求进来先占坑,即抢占锁
        String uuid = UUID.randomUUID().toString();
        // 原子性命令;保证'设置过期时间和占锁'是原子性操作
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,3,TimeUnit.SECONDS);
        if (lock) {
    
    
            // "占坑"成功,执行业务逻辑
            Map<Long, List<Catalog2VO>> result;
            try {
    
    
                result = getCatalogJsonData();
            } finally {
    
    
                // 解锁:uuid保证删的是自己的锁;lua脚本保证了删锁的原子性
                redisTemplate.execute(new DefaultRedisScript<>(getLuaScript(),Long.class),Arrays.asList("lock"),uuid);
            }
            return result;
        }else {
    
    
            // "占坑"失败,自旋
            try {
    
    
                // 防止栈溢出
                TimeUnit.MILLISECONDS.sleep(200);
            } catch (InterruptedException e) {
    
    
                throw new RuntimeException(e);
            }
            return getCatalogJsonWithRedisLock();
        }
    }

    private String getLuaScript(){
    
    
        return "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
                "then\n" +
                "    return redis.call(\"del\",KEYS[1])\n" +
                "else\n" +
                "    return 0\n" +
                "end";
    }

    private Map<Long, List<Catalog2VO>> getCatalogJsonData() {
    
    
        String key = ProductConstant.RedisKey.INDEX_CATEGORY_JSON;
        String result = redisTemplate.opsForValue().get(key);
        if (!StringUtils.hasText(result)) {
    
    
            Map<Long, List<Catalog2VO>> catalogJsonFromDB = getCatalogJsonFromDB();
            redisTemplate.opsForValue().set(key,JSON.toJSONString(catalogJsonFromDB),2,TimeUnit.HOURS);
            return catalogJsonFromDB;
        }
        return JSON.parseObject(result, new TypeReference<>() {
    
    
        });
    }

Cache invalidation problem—cache avalanche

A large number of keys expire at the same time at a certain time

Assume that a large number of Keys in the cache use the same expiration time, which will directly cause these Keys to expire at the same time at some point in the future; at this time, a large number of requests for these Keys will put pressure on the database, causing the instantaneous pressure on the database to be too high. A crash may occur
Solution: Add a random value to the original expiration time, so that the repetition rate of each cache's expiration time will be very low. Therefore, it is difficult to cause a cache avalanche problem caused by Key failure in a large area at the same time

// 再原有的失效时间基础上添加随机时间片
// 这里没有增加随机时间片,因为Key的数量有限,足以保证失效时间的离散分布
redisTemplate.opsForValue().set(key,JSON.toJSONString(catalogJsonFromDB),2,TimeUnit.HOURS);

Distributed lock—Redisson

The above based on Redissetnx命令 simply implemented a distributed lock, and exposed many problems during the implementation process, and all of them were solved one by one. However, the official recommendation is to use redlock to implement distributed locks

Note:RedlockThe algorithm is slightly more complex to implement, but provides better guarantees and fault tolerance
Insert image description here
There is an implementation for Java hereRedisson
Insert image description here

github-Redisson-distributed locks and synchronizers

Watchdog automatically renews to avoid deadlock

RLock lock = redisson.getLock("anyLock");
// 最常见的使用方法
lock.lock();

As we all know, if the Redisson node responsible for storing this distributed lock goes down and the lock happens to be locked, the lock will be locked. 为了避免锁死的状态的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期. By default, the watchdog check lock timeout is 30 seconds, which can also be specified by modifying Config.lockWatchdogTimeout.

In addition, Redisson also provides the leaseTime parameter through the locking method to specify the locking time. After this time has elapsed, the lock will automatically unlock.

// 加锁以后10秒钟自动解锁
// 无需调用unlock方法手动解锁
lock.lock(10, TimeUnit.SECONDS);

// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
    
    
   try {
    
    
     ...
   } finally {
    
    
       lock.unlock();
   }
}

Redisson configuration method

GitHub—Redisson configuration method
Insert image description here
Single node mode programmatic configuration
Please refer to the official documentation for details. The following code snippet is for single node mode a>

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.24.3</version>
        </dependency>

Redisson client configuration

@Configuration
public class RedissonConfiguration {
    
    


    @Bean
    public RedissonClient redissonClient(){
    
    
        Config config = new Config();
        SingleServerConfig singleServerConfig = config.useSingleServer();
        singleServerConfig.setAddress("redis://IP:port");
        singleServerConfig.setPassword("******");
        return Redisson.create(config);
    }
}

Redisson-lock lock test

1. Will Redisson deadlock?
Test the interface business code and enable two services locally. Among them, service A will be killed during the 3S period of executing business logic, that is, service A will not perform the unlocking action. At this time, service B will access again to test whether service B can still obtain the lock and execute it. Business logic. If it can, it means that Redisson will not have a deadlock problem. lock.unlock()

    @GetMapping("/lock")
    @ResponseBody
    public String hello(){
    
    

        RLock lock = redissonClient.getLock("my-lock");
        lock.lock();
        try {
    
    
            log.info("加锁成功;执行业务逻辑....{}",Thread.currentThread().getId());
            TimeUnit.SECONDS.sleep(5);
            return "success";
        } catch (InterruptedException e) {
    
    
            throw new RuntimeException(e);
        } finally {
    
    
            lock.unlock();
        }
    }

After testing, Redisson does not have a deadlock problem; the reason why a deadlock does not occur is because Redisson’s lock automatically renews
2. Redisson does not have a deadlock problem reason

  • Watchdog mechanism, automatic renewal of locks; if the business execution logic takes too long; Redisson will automatically renew the lock during business execution. Prevent the lock from being automatically deleted if the business execution time is too long
  • Because the lock always has an expiration time, the lock will be deleted automatically even if the lock is not deleted manually.

おすすめ

転載: blog.csdn.net/weixin_43859011/article/details/134721049