问题(需求):
项目中一些常见问题
1.多节点用户会话保存
2.订单大规模定时取消
3.产品服务访问慢
4.分布式任务重复执行
5.高并发提交:设备状态上报接口
接口高并发
/**
* 接到上报
* 1
* @param params
* @return
*/
@Override
public int save(CarAccessQueryParams params){
CarAccess carAccessExit = getOneCarAccessByCarnum(params.getCarnum());
CarAccess carAccessParam = CarAccessQueryParams.convertToEntity(params);
String key = String.format("app:%s",params.getCarnum());
Long count= redisCache.increment(key);
if(count == 1) {
redisCache.expire(key, 60);
if(carAccessExit==null){
carAccessDao.insert(carAccessParam);
}else {
carAccessDao.updateByCarnum(carAccessParam);
}
}else if((count>1)&&(carAccessParam.getPicUrl()!=null)){//<1分钟
if(carAccessExit!=null){
carAccessDao.updateByCarnum(carAccessParam);
}else{
try {
TimeUnit.SECONDS.sleep(1);
}catch (Exception e){
logger.error(String.format("carAccess save延时失败==%s"),e.getMessage());
}
carAccessDao.updateByCarnum(carAccessParam);
}
}
return 1;
}
分布式会话
@Configuration
@EnableRedisHttpSession(redisNamespace = "{app}",maxInactiveIntervalInSeconds=1800)
public class RedisConfig {
}
延时队列
public abstract class AbstractRedisDelayQueue {
protected Logger logger = LoggerFactory.getLogger(AbstractRedisDelayQueue.class);
@Autowired
protected RedisTemplate redisTemplate;
/**
* 插入任务id
* @param taskId 任务id(队列内唯一)
* @param time 延时时间(单位 :秒)
* @return 是否插入成功
*/
public boolean addTaskId(String taskId, Integer time) {
Calendar instance = Calendar.getInstance();
instance.add(Calendar.SECOND, time);
long delaySeconds = instance.getTimeInMillis() / 1000;
try {
String key = setDelayQueueName();
boolean zadd = redisTemplate.opsForZSet().add(key, taskId, delaySeconds);
return zadd;
} catch (Exception e) {
logger.warn("AbstractRedisDelayQueue addTaskId异常===",e);
e.printStackTrace();
return false;
}
}
private void startDelayQueue() {
logger.info("AbstractRedisDelayQueue---init----QueueName==> {}", setDelayQueueName());
while (true) {
try {
long now = System.currentTimeMillis() / 1000;
// 获取当前时间前的任务列表
Set<Object> taskIds = redisTemplate.opsForZSet().rangeByScoreWithScores(setDelayQueueName(), 0, now);
// 如果不为空则遍历判断其是否满足取消要求
if (!CollectionUtils.isEmpty(taskIds)) {
for (Object obj : taskIds) {
JSONObject jsonObject = JSONObject.parseObject(JSON.toJSONString(obj));
String taskId = jsonObject.get("value").toString();
Long num = redisTemplate.opsForZSet().remove(setDelayQueueName(), taskId);
// 如果移除成功, 则取消订单
if (null != num && num > 0) {
invoke(taskId);
}
}
}
} catch (Exception e) {
logger.error("处理延时任务发生异常,异常原因为 {}", e);
} finally {
// 间隔一分钟执行一次
try {
TimeUnit.SECONDS.sleep(60L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 最终执行的任务方法
* @param taskId 任务id
*/
public abstract void invoke(String taskId);
/**
* 要实现延时队列的名字
* @return
*/
public abstract String setDelayQueueName();
/**
* 项目启动时执行初始化
*/
@PostConstruct
public void init(){
ExecutorService singleThreadExecutor = new ThreadPoolExecutor(1, 1, 300L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(16),
new BasicThreadFactory.Builder().namingPattern("start-delay-queue-thread-pool-%d").daemon(true).build(), new ThreadPoolExecutor.AbortPolicy());
singleThreadExecutor.execute(()-> {
logger.info("startDelayQueue--- {}", Thread.currentThread().getName());
startDelayQueue();
});
singleThreadExecutor.shutdown();
}
public class CancelOrderDelayQueue extends AbstractRedisDelayQueue {
@Autowired
private OrderServiceFacade orderServiceFacade;
@Override
public void invoke(String taskId) {
log.info("超时未支付自动取消======================" + taskId);
OrderResponse orderResponse = orderServiceFacade.queryOrderByOrderNo(taskId);
if(Objects.nonNull(orderResponse)){
Optional.ofNullable(orderResponse.getMain()).ifPresent(main -> {
if(TradeStatusEnum.CREATE.equals(main.getOrderStatus())){
orderServiceFacade.cancleOrderByOrderNo(OrderConstant.ORDER_TYPE_MAIN,main.getOrderNo(),"超时自动关闭");
}
});
}
}
@Override
public String setDelayQueueName() {
return RedisKeyConstant.CANCEL_ORDER_DELAY_QUEUE_NAME;
}
}
分布式锁
@Scheduled(cron = "0 0 10 * * ?")
public void remindFollowCustomerNumber(){
try{
log.info("RemindTask remindUnFollow {}",new Date());
if (!redisCache.setNX(DsopConstants.REMIND_FLAG_FOLLOW_CUSTOMER_NUMBER,DsopConstants.REMIND_FLAG_VALUE,DsopConstants.REMIND_FLAG_TIMEOUT, TimeUnit.SECONDS)){
log.info("任务已执行");
return;
}
List<FollowRemindNumberDTO> customerInfos=customerFollowService.queryRemindCustomerNumber();
customerInfos.forEach(c->{
wxPushService.pushRemindCustomerNumber(c.getManager(),c.getManagerPhone(),c.getCustomeraNumber(),c.getCustomerbNumber());
});
}catch (Exception e){
log.error("RemindTask remind异常:{}",e.getMessage());
}finally {
redisCache.del(DsopConstants.REMIND_FLAG_FOLLOW_CUSTOMER_NUMBER);
}
}
reids提效工具springcache
@Override
@Cacheable(value = "newProductDetailList",key = "#root.target.getFormatKey(#p0)",cacheManager = "cacheManager5Minute")
public Page<ItemResponse> quertNewProductDetailListCache(ItemOnsaleRequest itemOnsaleRequest) {
CommResponse<Page<ItemResponse>> onsale = productFeign.getOnsale(itemOnsaleRequest);
if(onsale.getCode()!=0){
throw new BusinessException(onsale.getDesc());
}
return onsale.getData();
}
5.常见场景
5.1.分布式锁
5.2.缓存
5.3.统一会话
5.4.token认证
6.问题的解决方案代码示例
分布式缓存之缓存击穿+解决⽅案
简介:分布式缓存必考题之缓存击穿+解决⽅案
缓存击穿 (某个热点key缓存失效了)
缓存中没有但数据库中有的数据,假如是热点数据,那key在缓存过期的⼀刻,同时有⼤量的请求,这些请求都会击穿到DB,造成瞬时DB请求量⼤、压⼒增⼤。和缓存雪崩的区别在于这⾥针对某⼀key缓存,后者则是很多key。
预防
设置热点数据不过期
定时任务定时更新缓存
设置互斥锁
SpringCache解决⽅案
缓存的同步 sync
sync 可以指示底层将缓存锁住,使只有⼀个线程可以进⼊计算,⽽其他线程堵塞,直到返回结果更新到缓存中
@Cacheable(value = {“product”},key ="#root.args[0]", cacheManager =
“customCacheManagr”,sync=true)
分布式缓存缓存雪崩+解决⽅案
简介:分布式缓存之缓存雪崩+解决⽅案
缓存雪崩 (多个热点key都过期)
⼤量的key设置了相同的过期时间,导致在缓存在同⼀时刻全部失效,造成瞬时DB请求量⼤、压⼒骤增,引起雪崩
预防
存数据的过期时间设置随机,防⽌同⼀时间⼤量数据过期现象发⽣
设置热点数据永远不过期,定时任务定时更新
SpringCache解决⽅案
设置差别的过时时间
⽐如CacheManager配置多个过期时间维度
配置⽂件 time-to-live
cache:
#使⽤的缓存类型
type: redis
#过时时间
redis:
time-to-live: 3600000
开启前缀,默以为true
use-key-prefix: true
键的前缀,默认就是缓存名cacheNames
key-prefix: MIDDLE_CACHE
是否缓存空结果,防⽌缓存穿透,默以为true
cache-null-values: true
cache-null-values: true
分布式缓存之缓存穿透
+解决⽅案
简介:分布式缓存之缓存穿透+解决⽅案
缓存穿透(查询不存在数据)查询⼀个不存在的数据,由于缓存是不命中的,并且出于容错考虑,如发起为id为“-1”不存在的数据
如果从存储层查不到数据则不写⼊缓存这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。存在⼤量查询不存在的数据,可能DB就挂掉了,这也是⿊客利⽤不存在的key频繁攻击应⽤的⼀种⽅式。
预防
接⼝层增加校验,数据合理性校验
缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,设置短点的过期时间,防⽌同个key被⼀直攻击
SpringCache解决⽅案
空结果也缓存,默认不配置condition或者unless就⾏
cache:
#使⽤的缓存类型
type: redis
#过时时间
redis:
time-to-live: 3600000
开启前缀,默以为true
use-key-prefix: true
键的前缀,默认就是缓存名cacheNames
key-prefix: MIDDLE_CACHE
是否缓存空结果,防⽌缓存穿透,默以为true
@Override
@Cacheable(value = "mallPlantList", key = "0", condition = "#root.target.getFormatKey(#p0) != null", cacheManager = "cacheManager5Minute",unless="#result.size()==0")
public List<PlantDTO> getListAll() {
PlantQueryForm plantQueryForm = new PlantQueryForm();
plantQueryForm.setSize(50);
CommResponse<Page<PlantDTO>> search = plantFeign.search(plantQueryForm);
if(search.getCode()!=0){
throw new BusinessException(search.getDesc());
}
Page<PlantDTO> data = search.getData();
if(data != null){
List<PlantDTO> records = data.getRecords();
return records;
}
return new ArrayList<>();
}
常用工具:
1.RedisDesktopManager
2.ApacheJMeter