目前参与的项目中有一个对外接口,客户通过apikey请求接口,项目中需要判断apikey是否存在我们系统中,为提升apikey判断速度,决定用caffeine将apikey->User缓存在本地。
大致校验流程如下:
具体代码如下:
CacheConfig.java
定义userCache和errorApikeyCache两个缓存,其中userCache用来存apikey->User数据,errorApikeyCache用来存无效的apikey(设置一定过期时间),防止客户用相同无效apikey重复请求接口
package com.xxx;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.CacheWriter;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.RemovalCause;
import com.xxx.UserModel;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 用户本地缓存查询服务
*
* @author xxx on 2020/7/9
*/
@Slf4j
@Configuration
public class CacheConfig {
/**
* apikey -> UserDO
* @return
*/
@Bean("userCache")
public Cache<String, UserModel> userCache() {
Cache<String, UserModel> userCache = Caffeine.newBuilder()
// 数量上限
.maximumSize(10000)
// 缓存写入/删除监控
.writer(new CacheWriter<Object, Object>() {
@Override
public void write(Object key, Object value) { //此方法是同步阻塞的
log.info("[Caffeine][add]key=" + key);
}
@Override
public void delete(Object key, Object value, RemovalCause cause) {
log.info("[Caffeine][remove]key=" + key);
}
})
.build();
return userCache;
}
/**
* apikey -> UserDO
* @return
*/
@Bean("errorApikeyCache")
public Cache<String, Integer> errorApikeyCache() {
Cache<String, Integer> cache = Caffeine.newBuilder()
// 数量上限
.maximumSize(500)
// 过期机制,1分钟
.expireAfterAccess(1, TimeUnit.MINUTES)
// 缓存写入/删除监控
.writer(new CacheWriter<Object, Object>() {
@Override
public void write(Object key, Object value) { //此方法是同步阻塞的
log.info("[Caffeine][add]key=" + key);
}
@Override
public void delete(Object key, Object value, RemovalCause cause) {
log.info("[Caffeine][remove]key=" + key);
}
})
.build();
return cache;
}
}
UserLocalCacheService.java
package com.xxx;
import com.xxx.UserModel;
/**
* 用户本地缓存服务
*
* @author xxx on 2020/7/9
*/
public interface UserLocalCacheService {
/**
* 根据apikey获取用户
* @param apikey
* @return
*/
UserModel getUserByApikey(String apikey);
}
UserLocalCacheServiceImpl.java
package com.xxx;
import com.github.benmanes.caffeine.cache.Cache;
import com.xxx..Click;
import com.xxx.Pagination;
import com.xxx.Range;
import com.xxx.ActiveUserQuery;
import com.xxx.ActiveUserClient;
import com.xxx.UserModel;
import com.xxx.UserModelQuery;
import com.xxx.UserModelSyncClientService;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
/**
* 用户本地缓存服务
*
* @author xxx on 2020/7/9
*/
@Service
@Slf4j
public class UserLocalCacheServiceImpl implements UserLocalCacheService {
@Resource
Cache<String, UserModel> userCache;
@Autowired
Cache<String, Integer> errorApikeyCache;
@Resource
private UserModelSyncClientService userModelSyncClientService;
@Resource
ActiveUserClient activeUserClient;
private final static int ACTIVE_USER_PAGE_SIZE = 500;
private static AtomicBoolean refreshing = new AtomicBoolean(false);
private static long lastModified = 0;
private static int pageSize = 100;
/**
* 锁对象
*/
private static Object lockObj = new Object();
/**
* 根据apikey获取user信息
* @param apikey
* @return
*/
@Override
public UserModel getUserByApikey(String apikey) {
if (StringUtils.isBlank(apikey)) {
return null;
}
//先判断apikey是否在errorApikey中
if (errorApikeyCache.getIfPresent(apikey) != null) {
log.debug("apikey hit errCache, {}", apikey);
return null;
}
UserModel userModel = userCache.getIfPresent(apikey);
if (userModel != null) {
return userModel;
} else {
synchronized (lockObj) {
userModel = userCache.getIfPresent(apikey);
//双重检测
if (userModel == null) {
Click<UserModel> click = userModelSyncClientService.getUserModelByApikey(apikey);
if (!click.succ()) {
log.error("useModel query error");
return null;
}
userModel = click.getData();
if (userModel == null) {
errorApikeyCache.put(apikey, 1);
log.warn("apikey not exist, {}", apikey);
} else {
userCache.put(userModel.getApiKey(), userModel);
log.info("sid:" + userModel.getUserId() + " refreshed by query");
}
}
}
}
return userModel;
}
/**
* 类加载的时候,加载缓存
*/
@PostConstruct
private void initUserCache() {
lastModified = System.currentTimeMillis();
//获取初始化用户
initActiveUser();
}
/**
* 获取初始化用户
*/
private void initActiveUser() {
if (refreshing.get()) {
log.info("Is in refreshing, skip this round.");
return;
}
refreshing.set(true);
List<UserModel> allUsers = new ArrayList<UserModel>();
long s0 = System.currentTimeMillis();
ActiveUserQuery activeUserQuery = new ActiveUserQuery();
activeUserQuery.setPageNo(1);
activeUserQuery.setPageSize(10000);
Click<List<Long>> activeUserClick = activeUserClient.queryList(activeUserQuery);
if (!activeUserClick.succ()) {
log.error("active user query error");
refreshing.set(false);
return;
}
List<Long> userIdList = activeUserClick.getData();
if (CollectionUtils.isEmpty(userIdList)) {
log.warn("active user empty");
refreshing.set(false);
return;
}
int listSize = userIdList.size();
log.info("active user count: " + userIdList.size());
int toIndex = ACTIVE_USER_PAGE_SIZE;
Collection<Long> newList;
List<UserModel> userModelList;
for (int i = 0; i < listSize; i += ACTIVE_USER_PAGE_SIZE) {
if (i + ACTIVE_USER_PAGE_SIZE > listSize) {
toIndex = listSize - i;
}
newList = userIdList.subList(i, i + toIndex);
userModelList = userModelSyncClientService.selectUserModelByIds(newList)
.touch();
log.info("get active user i: " + i + ", " + System.currentTimeMillis());
allUsers.addAll(userModelList);
}
long s1 = System.currentTimeMillis();
log.info("total active user count: " + allUsers.size() + ", query user cost: " + (s1 - s0));
//给缓存赋值
updateCache(allUsers, true);
refreshing.set(false);
long s2 = System.currentTimeMillis();
log.info("init active user finish, " + s2 + ", cost: " + (s2 - s0));
}
/**
* 给缓存赋值
* @param userModelList
* @param initUser
*/
private void updateCache(List<UserModel> userModelList, Boolean initUser) {
if (CollectionUtils.isEmpty(userModelList)) {
return;
}
for (UserModel userModel : userModelList) {
// 如果是初始化数据的时候直接存在缓存中
if (initUser) {
userCache.put(userModel.getApiKey(), userModel);
log.info("sid:" + userModel.getUserId() + " refreshed");
} else {
// 如果是定时任务更新数据,则判断用户是否在缓存中,只更新在缓存中的数据;
// 如果不在缓存中,说明用户没有调用接口,只是单纯的修改信息,则不用更新缓存,下次访问接口没命中缓存会加到缓存中
if (userCache.getIfPresent(userModel.getApiKey()) != null) {
userCache.put(userModel.getApiKey(), userModel);
log.info("sid:" + userModel.getUserId() + " refreshed");
}
}
}
log.info("current user cache size: " + userCache.asMap().size());
}
/**
* 每一分钟刷新一次用户缓存
*/
@Scheduled(cron = "0 0/1 * * * ?")
public void refreshUserCache() {
if (refreshing.get()) {
log.info("Is in refreshing, skip this round.");
return;
}
refreshing.set(true);
long s0 = System.currentTimeMillis();
log.info("start refresh user cache, s0: {}", s0);
try {
UserModelQuery query = new UserModelQuery();
Range<Date> dateRange = new Range<>();
dateRange.set$gte(new Date(lastModified - 60000));
query.setModifiedRange(dateRange);
Pagination<UserModel> page = new Pagination<>(0, pageSize);
query.setPage(page);
Pagination<UserModel> userModelPagination = userModelSyncClientService
.selectByQuery4Page(query)
.touch();
if (userModelPagination == null) {
log.info("refreshUserCache error");
refreshing.set(false);
return;
}
List<UserModel> userModelList = userModelPagination.getRecords();
if (CollectionUtils.isEmpty(userModelList)) {
log.info("refreshUserCache, empty");
refreshing.set(false);
return;
}
updateCache(userModelList, false);
while (userModelList.size() == pageSize) {
page.setPageNo(page.getPageNo() + 1);
userModelPagination = userModelSyncClientService.selectByQuery4Page(query)
.touch();
if (userModelPagination != null && CollectionUtils
.isNotEmpty(userModelPagination.getRecords())) {
userModelList = userModelPagination.getRecords();
updateCache(userModelList, false);
} else {
break;
}
}
lastModified = s0;
refreshing.set(false);
} catch (Exception e) {
log.error("refresh user cache error, {}", e);
refreshing.set(false);
}
long s1 = System.currentTimeMillis();
log.info("start refresh user cache, s1: {}, cost: {}", s1, s1 - s0);
}
}
这里面有几个地方说明一下:
1、项目启动的时候调用initActiveUser方法初始化系统中的活跃用户到本地缓存(线上用户较多,不用将所有用户缓存到本地)
2、系统每一分钟查询更新用户表中近一分钟有变更的用户(通过数据库中gmt_modified字段)
3、当apikey从userCache中没有找到用户信息的时候,为防止请求量过大的时候都去查询数据库,给数据库造成压力,采用双重检测