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

/**
 * 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;
    }
}