Cache (Redis) tool class, including solutions for cache breakdown, cache penetration, and generation of globally unique ids
/**
* Cache (Redis) tool class, including solutions for cache breakdown, cache penetration, and generation of globally unique IDs
* */
public class CacheManipulate {<!-- -->
//Generate the variables required for global id
public static final long BEGIN_TIMESTEMP =
LocalDateTime.of(2000,1,1,0,0).toEpochSecond(ZoneOffset.UTC);
public static final int MOVE_BIT = 32;
//Inject redis
private final StringRedisTemplate stringRedisTemplate;
//Create a thread pool
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
public CacheManipulate(StringRedisTemplate stringRedisTemplate) {<!-- -->
this.stringRedisTemplate = stringRedisTemplate;
}
//Get the lock
private boolean getLock(String key){<!-- -->
try {<!-- -->
Boolean valid = stringRedisTemplate.opsForValue().setIfAbsent(key,"1",10,TimeUnit.SECONDS);
return BooleanUtil.isTrue(valid);
} catch (Exception e) {<!-- -->
throw new RuntimeException(e);
}
}
//Unlock
private void unlock(String key){<!-- -->
try {<!-- -->
stringRedisTemplate.delete(key);
} catch (Exception e) {<!-- -->
throw new RuntimeException(e);
}
}
/**
* Set specific expiration time
* */
public void set(String key, Object val, Long time, TimeUnit unit){<!-- -->
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(val),time,unit);
}
/**
* Set logical expiration time, do not delete directly
*/
public void setLogicalExpire(String key, Object val, Long time, TimeUnit unit){<!-- -->
RedisData redisData = new RedisData();
redisData.setData(val);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData),time,unit);
}
/**
* To solve cache penetration, the value does not exist in the database and a null value needs to be set.
* */
public<T,R> R quaryWithExpire(String keyPrefix, T id, Class<R> type,
Function<T,R> dbRollBack,Long time,TimeUnit unit){<!-- -->
R res;
String key = keyPrefix + id.toString();
//1. Query whether it exists in redis
String json = stringRedisTemplate.opsForValue().get(key);
if(!StrUtil.isBlank(json)){<!-- -->
res = JSONUtil.toBean(json,type);
return res;
}
//isBlank function will also return true when json is null
if(json == null){<!-- -->
return null;
}
//Query database
res = dbRollBack.apply(id);
if(res==null){<!-- -->
//Cache empty value
this.set(key,"",time,unit);
}else{<!-- -->
//Rebuild cache
this.set(key,res,time,unit);
}
return res;
}
/**
* Solve cache breakdown, a large number of hotspot data expire at the same time, need to rebuild the cache, logical expiration time
* */
public<T,R> R quaryWithLogicalExpire(String keyPrefix, T id, Class<R> type,
Function<T,R> dbRollBack,Long time,TimeUnit unit) {<!-- -->
R res=null;
String key = keyPrefix + id.toString();
//1. Query whether it exists in redis
res = isExistKey(key,type);
if(res != null)return res;
//Expired to rebuild the cache
//Get the lock
String lockKey = RedisLockConstants.GLOBAL_LOCK_KEY + id.toString();
Boolean isLock = getLock(lockKey);
if(isLock){<!-- -->
//Double verification prevents multiple rebuilds
res = isExistKey(key,type);
if(res != null)return res;
//Start a thread to rebuild the cache
Future<R> future = CACHE_REBUILD_EXECUTOR.submit(()->{<!-- -->
R r;
try {<!-- -->
//Query database
r = dbRollBack.apply(id);
if(r==null){<!-- -->
//Cache empty values, maybe not needed?
this.setLogicalExpire(key,"",time,unit);
}else{<!-- -->
//Rebuild cache
this.setLogicalExpire(key,r,time,unit);
}
} catch (Exception e) {<!-- -->
throw new RuntimeException(e);
}finally {<!-- -->
unlock(lockKey);
}
return r;
});
try {<!-- -->
res = future.get();
return res; // return result
} catch (InterruptedException | ExecutionException e) {<!-- -->
// Handle exceptions
throw new RuntimeException(e);
}
}
return res;
}
/**
* Determine whether the key exists in the cache
* */
public <R> R isExistKey(String key,Class<R> type){<!-- -->
R res = null;
String json = stringRedisTemplate.opsForValue().get(key);
if(!StrUtil.isBlank(json)){<!-- -->
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
LocalDateTime expireTime = redisData.getExpireTime();
res = JSONUtil.toBean((JSONObject) redisData.getData(),type);
if(!expireTime.isAfter(LocalDateTime.now())){<!-- -->
//Expired returns null
res=null;
}
}
//isBlank function will also return true when json is null
if(json == null){<!-- -->
res=null;
}
return res;
}
/**
* Generate a globally unique id
* @param prefixKey
* @return id
* */
public long nextId(String prefixKey){<!-- -->
//Generate the first 31 digits of the timestamp prefix
LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long timeStemp = nowSecond-BEGIN_TIMESTEMP;
//The 32-digit self-increasing suffix serial number after production, the date is the year, month and day. The same service has a different key every day. At the same time, the number of services per day, month and year can be retrieved based on the key.
String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
long count =stringRedisTemplate.opsForValue().increment("icr" + prefixKey + ":" + date);
//Bit operation constructs global id
long id = (timeStemp<<MOVE_BIT)|count;
return id;
}
}