解决方案描述:
使用互斥锁重建缓存
在高并发场景下,为了避免大量的请求同时到达存储层查询数据、重建缓存,可以使用互斥锁控制,如根据 key 去缓存层查询数据,当缓存层为命中时,对 key 加锁,然后从存储层查询数据,将数据写入缓存层,最后释放锁。若其他线程发现获取锁失败,则让线程休眠一段时间后重试。对于锁的类型,如果是在单机环境下可以使用 Java 并发包下的 Lock,如果是在分布式环境下,可以使用分布式锁(Redis 中的 SETNX 方法)。这里演示第二种分布式环境下的场景。
重点第一个方法
package com.ruoyi.common.core.redis;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
@SuppressWarnings(value = {"unchecked", "rawtypes"})
@Component
public class RedisCache {
@Autowired
public RedisTemplate redisTemplate;
public int setNx(String key, int value, int timeSecond,TimeUnit timeUnit) {
Boolean result = redisTemplate.opsForValue().setIfAbsent(key, value, timeSecond, timeUnit);
if (result != null) {
return result.booleanValue() == true ? 1 : 0;
} else {
return 0;
}
}
public void setCacheObject(final String key, final T value) {
redisTemplate.opsForValue().set(key, value);
}
public void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) {
redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
}
public boolean expire(final String key, final long timeout) {
return expire(key, timeout, TimeUnit.SECONDS);
}
public boolean expire(final String key, final long timeout, final TimeUnit unit) {
return redisTemplate.expire(key, timeout, unit);
}
public T getCacheObject(final String key) {
ValueOperations operation = redisTemplate.opsForValue();
return operation.get(key);
}
public boolean deleteObject(final String key) {
return redisTemplate.delete(key);
}
public long deleteObject(final Collection collection) {
return redisTemplate.delete(collection);
}
public long setCacheList(final String key, final List dataList) {
Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
return count == null ? 0 : count;
}
public List getCacheList(final String key) {
return redisTemplate.opsForList().range(key, 0, -1);
}
public BoundSetOperations setCacheSet(final String key, final Set dataSet) {
BoundSetOperations setOperation = redisTemplate.boundSetOps(key);
Iterator it = dataSet.iterator();
while (it.hasNext()) {
setOperation.add(it.next());
}
return setOperation;
}
public Set getCacheSet(final String key) {
return redisTemplate.opsForSet().members(key);
}
public void setCacheMap(final String key, final Map dataMap) {
if (dataMap != null) {
redisTemplate.opsForHash().putAll(key, dataMap);
}
}
public Map getCacheMap(final String key) {
return redisTemplate.opsForHash().entries(key);
}
public void setCacheMapValue(final String key, final String hKey, final T value) {
redisTemplate.opsForHash().put(key, hKey, value);
}
public T getCacheMapValue(final String key, final String hKey) {
HashOperations opsForHash = redisTemplate.opsForHash();
return opsForHash.get(key, hKey);
}
public List getMultiCacheMapValue(final String key, final Collection
测试:
package com.ruoyi.web.controller;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.redis.RedisCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
@RestController
@RequestMapping("/testRedis")
public class TestRedisLockController {
@Autowired
private RedisCache redis;
//过期值
private final Integer expireTime=10;
@GetMapping("/setUserList")
public AjaxResult setRedis() {
redis.setCacheObject("userList","张伟",expireTime,TimeUnit.SECONDS);
return AjaxResult.success();
}
@GetMapping("/getUserList")
public AjaxResult getUserList() throws InterruptedException {
String userList = get("userList");
return AjaxResult.success(userList);
}
@GetMapping("/deleteUserList")
public AjaxResult deleteRedis() {
redis.deleteObject("userList");
return AjaxResult.success();
}
public String get(String key) throws InterruptedException {
String value = redis.getCacheObject(key);
if (value == null) { //代表缓存值过期
//Setnx(SET if Not eXists) 命令在指定的 key 不存在时,为 key 设置指定的值。设置成功返回1,设置失败返回0
//设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
if (redis.setNx("key_mutex", 1, 3 * 60,TimeUnit.SECONDS) == 1) { //代表设置成功
String s = "吕子乔";//这里懒省事,写成固定值
value = s;//从数据库取出最新值
redis.setCacheObject(key, value, expireTime, TimeUnit.SECONDS); //将值重新赋值到redis缓存中,并将过期时间重新设置
redis.deleteObject("key_mutex"); //使用完后,热点key已经成为未过期的key,将key_mutex值删除
return value;
} else { //若是设置不成功,证明这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
Thread.currentThread().sleep(5);
get(key); //重试
}
}
return value;
}
}