Distributed cache problem

1 Cache invalidation problem

Let's first solve the problem of cache failure in the case of large concurrent reads;

1.1 Cache penetration

1.1.1 Problem description

  • Cache penetration refers to querying a data that must not exist. Since the cache is a miss, the database will be queried, but the database does not have such a record. We did not write the null of this query into the cache, which will lead to this non-existent data. Every time data is requested, it must be queried at the storage layer, which loses the meaning of caching.
  • When the traffic is heavy, the DB may hang up. If someone uses a key that does not exist to frequently attack our application, this is a vulnerability.

1.1.2 Solution:

(1) Cache null values: If the data returned by a query is empty (whether the data does not exist or not), we still cache the empty result (null). more than five minutes.

(2) Set an accessible list (white list):
use the bitmaps type to define an accessible list, and the list id is used as the offset of the bitmaps. Each visit is compared with the id in the bitmap. If the access id is not in the bitmaps, Intercept and do not allow access.

(3) Using Bloom filter: (Bloom Filter (Bloom Filter) was proposed by Bloom in 1970. It is actually a very long binary vector (bitmap) and a series of random mapping functions (hash function ).
The Bloom filter can be used to retrieve whether an element is in a set. Its advantage is that the space efficiency and query time are far more than the general algorithm. The disadvantage is that there is a certain rate of misidentification and difficulty in deletion.) Will
all The data that may exist is hashed into a sufficiently large bitmaps, and a data that must not exist will be intercepted by this bitmaps, thereby avoiding the query pressure on the underlying storage system.

(4) Real-time monitoring: When it is found that the hit rate of Redis begins to drop rapidly, it is necessary to check the access objects and the accessed data, and cooperate with the operation and maintenance personnel to set a blacklist to restrict services

1.2 Cache Avalanche

1.2.1 Problem description

  • Cache avalanche means that we use the same expiration time when we set the cache, causing the cache to fail at a certain moment at the same time, all requests are forwarded to the DB, and the instantaneous pressure on the DB is too heavy for an avalanche.

1.2.2 Solution:

(1) Build a multi-level cache architecture: nginx cache + redis cache + other caches (ehcache, etc.)

(2) Use locks or queues:
Use locks or queues to ensure that there will not be a large number of threads reading and writing to the database at one time, so as to avoid a large number of concurrent requests falling on the underlying storage system when failure occurs. Not suitable for high concurrency

(3) Set the expiration flag to update the cache:
record whether the cache data is expired (set the advance amount), if it expires, it will trigger to notify another thread to update the actual key cache in the background.

(4) Distribute the cache expiration time:
add a random value based on the original expiration time, such as 1-5 minutes random, so that the repetition rate of each cache expiration time will be reduced, and it will be difficult to cause collective invalidation event.

1.3 Cache breakdown

1.3.1 Problem description

  • For some keys with an expiration time set, if these keys may be accessed with high concurrency at some point in time, it is a very "hot" data.
  • For "hot" data keys, a problem needs to be considered: if the key fails before a large number of requests come in at the same time, all data queries for this key will fall to the DB, which is called cache breakdown.

1.3.2 Solutions

(1) Pre-set popular data: Before redis peak access, store some popular data in redis in advance, and increase the duration of these popular data keys

(2) Real-time adjustment: monitor which data is popular on site, and adjust the key expiration time in real time

(3) Use locking: (Cache query is performed before and after the lock is obtained, and the database query is performed if it cannot be found). Local locks are also useful in distributed situations. It is best to use distributed locks.
<1> It means that when the cache fails (judging that the value taken out is empty), it is not to load db immediately.
<2> First use some operations of the cache tool with a successful operation return value (such as SETNX of Redis) to set a mutex key
<3> When the operation returns successfully, then perform the load db operation, and reset the cache, and finally Delete the mutex key;
<4> When the operation fails, it proves that there is a thread loading db, and the current thread sleeps for a period of time before retrying the entire get cache method.

2 Distributed lock solution

SET key value [EX seconds] [PX milliseconds] [NX|XX]
set product_lock 1 EX 2 NX
TTL product_lock


		
String uuid = UUID.randomUUID().toString();
Boolean lockFlag = redisTemplate.opsForValue().setIfAbsent("product_lock", uuid, 300, TimeUnit.SECONDS);
/* 此段代码加锁有问题:存在死锁问题
Boolean lockFlag = redisTemplate.opsForValue().setIfAbsent("product_lock", "1");
*/
if(lockFlag) {
    
    
	// redisTemplate.expire("product_lock", 30, TimeUnit.SECONDS);
	// TODO 执行业务逻辑
	/* 此段代码解锁有问题:存在死锁问题
	String lockValue = redisTemplate.opsForValue().get("product_lock");
	if(uuid.equals(lockValue)) {
		redisTemplate.delete("product_lock");
	}
	*/
	String script = "if redis.call('get',KEYS[1]) == ARGV[1] "
			+"then "
			+"	return redis.call('del',KEYS[1]) "
			+"else "
			+"	 return 0 "
			+"end ";
	Integer execute = redisTemplate.execute(new DefaultRedisScript<Integer>(script,Integer.class),Arrays.asList("product_lock"),uuid);
	// TODO
}

2.1 redisson

@Configuration
public class RedissonConfig {
    
    
	@Bean
	public RedissonClient redissonClient() {
    
    
		Config config = new Config();
		config.useSingleServer().setAddress("redis://192.168.6.6:6379").setPassword("Dky@Nqi2021");
		RedissonClient redisson = Redisson.create(config);
		return redisson;
	}
}
@Controller
public class MichaelController {
    
    
	@Autowired
	private RedissonClient redissonClient;

	@GetMapping("/hello")
	@ResponseBody
	public String hello() {
    
    
		// 1、获取一把锁,只要锁的名字一样,就是同一把锁
		RLock rLock = redissonClient.getLock("my-first-lock");
		/// 2、加锁
		rLock.lock();// 阻塞式等待(默认加的锁是30s时间)
		//1)锁的自动续期,如果业务超长,运行期间自动给锁续上新的30s。不用担心业务时间长,锁自动过期被删除。
		//2)加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30s以后会自动删除。
		try {
    
    
			System.out.println("加锁成功,执行业务..." + Thread.currentThread().getId());
			Thread.sleep(10000);// 睡10秒
		} catch (Exception e) {
    
    
			// TODO: handle exception
		} finally {
    
    
			// 3、解锁
			System.out.println("释放锁..." + Thread.currentThread().getId());
			rLock.unlock();
		}
		return "hello";
	}
}
==>同时执行两个请求,效果如下<==
// 加锁成功,执行业务...78
// 释放锁...78
// 加锁成功,执行业务...79
// 释放锁...79

Guess you like

Origin blog.csdn.net/Michael_lcf/article/details/122253041