feat: 完成Caffeine缓存管理
parent
f6eaf6b05f
commit
0f0883a781
|
@ -29,6 +29,7 @@
|
||||||
<groupId>com.github.ben-manes.caffeine</groupId>
|
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||||
<artifactId>caffeine</artifactId>
|
<artifactId>caffeine</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
package com.muyu.common.caffeine.bean;
|
||||||
|
|
||||||
|
|
||||||
|
import com.github.benmanes.caffeine.cache.Cache;
|
||||||
|
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||||
|
import org.springframework.cache.CacheManager;
|
||||||
|
import org.springframework.cache.caffeine.CaffeineCache;
|
||||||
|
import org.springframework.cache.support.SimpleCacheManager;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author: 胡杨
|
||||||
|
* @Name: CaffeineCacheConfig
|
||||||
|
* @Description: Caffeine管理器
|
||||||
|
* @CreatedDate: 2024/9/26 上午11:52
|
||||||
|
* @FilePath: com.muyu.common.caffeine.config
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class CaffeineManager {
|
||||||
|
/**
|
||||||
|
* 创建缓存管理器
|
||||||
|
* @return 缓存管理器实例
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public CacheManager cacheManager() {
|
||||||
|
SimpleCacheManager cacheManager = new SimpleCacheManager();
|
||||||
|
cacheManager.setCaches(getCaches());
|
||||||
|
return cacheManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private ArrayList<CaffeineCache> getCaches() {
|
||||||
|
ArrayList<CaffeineCache> caches = new ArrayList<>();
|
||||||
|
// 故障信息缓存
|
||||||
|
caches.add(new CaffeineCache("fault", Caffeine.newBuilder().build()));
|
||||||
|
// 围栏信息缓存
|
||||||
|
caches.add(new CaffeineCache("fence", Caffeine.newBuilder().build()));
|
||||||
|
// 预警策略规则缓存
|
||||||
|
caches.add(new CaffeineCache("warming", Caffeine.newBuilder().build()));
|
||||||
|
return caches;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -1,30 +0,0 @@
|
||||||
package com.muyu.common.caffeine.bean;
|
|
||||||
|
|
||||||
|
|
||||||
import org.springframework.cache.support.SimpleCacheManager;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @Author: 胡杨
|
|
||||||
* @Name: CaffeineCacheConfig
|
|
||||||
* @Description: Caffeine管理器
|
|
||||||
* @CreatedDate: 2024/9/26 上午11:52
|
|
||||||
* @FilePath: com.muyu.common.caffeine.config
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
public class CaffeineManagerBean {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建缓存管理器
|
|
||||||
* @return 缓存管理器实例
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
public SimpleCacheManager simpleCacheManager() {
|
|
||||||
return new SimpleCacheManager();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
|
@ -8,8 +8,9 @@ import com.muyu.common.redis.service.RedisService;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.ObjectUtils;
|
import org.apache.commons.lang3.ObjectUtils;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.cache.CacheManager;
|
||||||
import org.springframework.cache.caffeine.CaffeineCache;
|
import org.springframework.cache.caffeine.CaffeineCache;
|
||||||
import org.springframework.cache.support.SimpleCacheManager;
|
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@ -30,74 +31,73 @@ public class CaffeineCacheUtils {
|
||||||
@Resource
|
@Resource
|
||||||
private RedisService redisService;
|
private RedisService redisService;
|
||||||
@Resource
|
@Resource
|
||||||
private SimpleCacheManager simpleCacheManager;
|
private CacheManager cacheManager;
|
||||||
@Resource
|
|
||||||
private RedisTemplate<String,Object> redisTemplate;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 车辆上线 - 新增缓存
|
|
||||||
*/
|
|
||||||
public void addCarCache(String vin) {
|
|
||||||
ArrayList<CaffeineCache> caches = new ArrayList<>();
|
|
||||||
// 从Redis中获取缓存信息
|
|
||||||
Collection<String> keys = redisTemplate.keys(CaffeineContent.CAR_VIN_KEY + vin);
|
|
||||||
keys.forEach(key -> {
|
|
||||||
Object string = redisTemplate.opsForValue().get(key);
|
|
||||||
Cache<Object , Object> cache = Caffeine.newBuilder().build();
|
|
||||||
cache.put(key, string);
|
|
||||||
// 全部存储到 CaffeineCache集合
|
|
||||||
caches.add(new CaffeineCache(vin, cache));
|
|
||||||
log.info("存储缓存,vin:{}, key:{}, value:{}", vin, key, string);
|
|
||||||
});
|
|
||||||
simpleCacheManager.setCaches(caches);
|
|
||||||
log.info("车辆编码:{},本地缓存完成...",vin);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * 车辆上线 - 新增缓存
|
||||||
|
// */
|
||||||
|
// public void addCarCache(String vin) {
|
||||||
|
//
|
||||||
|
// ArrayList<CaffeineCache> caches = new ArrayList<>();
|
||||||
|
// // 从Redis中获取缓存信息
|
||||||
|
// Collection<String> keys = redisTemplate.keys(CaffeineContent.CAR_VIN_KEY + vin);
|
||||||
|
// keys.forEach(key -> {
|
||||||
|
// Object string = redisTemplate.opsForValue().get(key);
|
||||||
|
// Cache<Object , Object> cache = Caffeine.newBuilder().build();
|
||||||
|
// cache.put(key, string);
|
||||||
|
// // 全部存储到 CaffeineCache集合
|
||||||
|
// caches.add(new CaffeineCache(vin, cache));
|
||||||
|
// log.info("存储缓存,vin:{}, key:{}, value:{}", vin, key, string);
|
||||||
|
// });
|
||||||
|
// simpleCacheManager.setCaches(caches);
|
||||||
|
// log.info("车辆编码:{},本地缓存完成...",vin);
|
||||||
|
// }
|
||||||
|
//
|
||||||
/**
|
/**
|
||||||
* 车辆下线 - 删除缓存
|
* 车辆下线 - 删除缓存
|
||||||
*/
|
*/
|
||||||
public void deleteCarCache(String vin) {
|
public void deleteCarCache(String cacheName) {
|
||||||
if (!hasCarVinCache(vin)) {
|
if (!hasCarVinCache(cacheName,null)) {
|
||||||
log.warn("车辆编码:{},本地缓存不存在该车辆信息...", vin);
|
log.warn("车辆编码:{},本地缓存不存在该车辆信息...", cacheName);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
simpleCacheManager.getCache(vin).invalidate();
|
cacheManager.getCache(cacheName).invalidate();
|
||||||
log.info("车辆编码:{},本地缓存删除完成...", vin);
|
log.info("车辆编码:{},本地缓存删除完成...", cacheName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取车辆信息缓存
|
* 获取车辆信息缓存
|
||||||
*/
|
*/
|
||||||
public Object getCarCache(String vin, String key) {
|
public Object getCarCache(String cacheName, String key) {
|
||||||
if (!hasCarVinKeyCache(vin, key)){
|
if (!hasCarVinCache(cacheName, key)){
|
||||||
log.warn("车辆编码:{},本地缓存不存在该车辆信息...",vin);
|
log.warn("车辆编码:{},本地缓存不存在该车辆信息...",cacheName);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return simpleCacheManager.getCache(vin).get(key).get();
|
return cacheManager.getCache(cacheName).get(key).get();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取车辆信息缓存
|
* 获取车辆信息缓存
|
||||||
*/
|
*/
|
||||||
public <T> T getCarCache(String vin, String key, Class<T> type) {
|
public <T> T getCarCache(String cacheName, String key, Class<T> type) {
|
||||||
if (!hasCarVinKeyCache(vin,key)){
|
if (!hasCarVinCache(cacheName,key)){
|
||||||
log.warn("车辆编码:{},本地缓存不存在该车辆信息...",vin);
|
log.warn("车辆编码:{},本地缓存不存在该车辆信息...",cacheName);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return simpleCacheManager.getCache(vin).get(key, type);
|
return cacheManager.getCache(cacheName).get(key, type);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 判断缓存存在与否
|
* 判断缓存存在与否
|
||||||
*/
|
*/
|
||||||
public Boolean hasCarVinCache(String vin) {
|
public Boolean hasCarVinCache(String cacheName,String key) {
|
||||||
return ObjectUtils.isNotEmpty(simpleCacheManager.getCache(vin));
|
boolean notEmpty = ObjectUtils.isNotEmpty(cacheManager.getCache(cacheName));
|
||||||
|
if (notEmpty && StringUtils.isNotEmpty(key)){
|
||||||
|
return ObjectUtils.isNotEmpty(cacheManager.getCache(cacheName).get(key).get());
|
||||||
|
}
|
||||||
|
return notEmpty;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断缓存的Key存在与否
|
|
||||||
*/
|
|
||||||
public Boolean hasCarVinKeyCache(String vin,String key) {
|
|
||||||
return hasCarVinCache(vin) && ObjectUtils.isNotEmpty(simpleCacheManager.getCache(vin).get(key).get());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
com.muyu.common.caffeine.utils.CaffeineCacheUtils
|
com.muyu.common.caffeine.utils.CaffeineCacheUtils
|
||||||
com.muyu.common.caffeine.bean.CaffeineManagerBean
|
com.muyu.common.caffeine.bean.CaffeineManager
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package com.muyu.data.processing.controller;
|
package com.muyu.data.processing.controller;
|
||||||
|
|
||||||
|
|
||||||
import com.muyu.common.caffeine.utils.CaffeineCacheUtils;
|
|
||||||
import com.muyu.common.core.utils.uuid.UUID;
|
import com.muyu.common.core.utils.uuid.UUID;
|
||||||
import com.muyu.common.iotdb.config.IotDBConfig;
|
import com.muyu.common.iotdb.config.IotDBConfig;
|
||||||
import com.muyu.common.kafka.constants.KafkaConstants;
|
import com.muyu.common.kafka.constants.KafkaConstants;
|
||||||
|
@ -10,6 +9,8 @@ import jakarta.annotation.Resource;
|
||||||
import org.apache.kafka.clients.producer.KafkaProducer;
|
import org.apache.kafka.clients.producer.KafkaProducer;
|
||||||
import org.apache.kafka.clients.producer.ProducerRecord;
|
import org.apache.kafka.clients.producer.ProducerRecord;
|
||||||
import org.springframework.amqp.rabbit.core.RabbitTemplate;
|
import org.springframework.amqp.rabbit.core.RabbitTemplate;
|
||||||
|
import org.springframework.cache.Cache;
|
||||||
|
import org.springframework.cache.CacheManager;
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
@ -33,9 +34,12 @@ public class TestController {
|
||||||
@Resource
|
@Resource
|
||||||
private IotDBConfig iotDBConfig;
|
private IotDBConfig iotDBConfig;
|
||||||
@Resource
|
@Resource
|
||||||
private CaffeineCacheUtils caffeineCacheUtils;
|
|
||||||
@Resource
|
|
||||||
private RedisTemplate<String,String> redisTemplate;
|
private RedisTemplate<String,String> redisTemplate;
|
||||||
|
// @Resource
|
||||||
|
// private CaffeineCacheUtils cacheUtils;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private CacheManager cacheManager;
|
||||||
|
|
||||||
@GetMapping("/testKafka")
|
@GetMapping("/testKafka")
|
||||||
public void sendMsg(@RequestParam("msg") String msg) {
|
public void sendMsg(@RequestParam("msg") String msg) {
|
||||||
|
@ -74,7 +78,10 @@ public class TestController {
|
||||||
|
|
||||||
@GetMapping("/testRabbit/GoOnline")
|
@GetMapping("/testRabbit/GoOnline")
|
||||||
public void testRabbitGoOnline(@RequestParam("msg") String msg) {
|
public void testRabbitGoOnline(@RequestParam("msg") String msg) {
|
||||||
rabbitTemplate.convertAndSend(RabbitConstants.GO_ONLINE_QUEUE, msg);
|
rabbitTemplate.convertAndSend(RabbitConstants.GO_ONLINE_QUEUE, msg, message -> {
|
||||||
|
message.getMessageProperties().setMessageId(UUID.randomUUID().toString().replace("-",""));
|
||||||
|
return message;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/testRabbit/Downline")
|
@GetMapping("/testRabbit/Downline")
|
||||||
|
@ -97,7 +104,38 @@ public class TestController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/testGetCache")
|
@GetMapping("/testGetCache")
|
||||||
public void testGetCache(@RequestParam("vin") String vin,@RequestParam("key") String key) {
|
public void testGetCache(@RequestParam("cacheName") String cacheName,@RequestParam("key") String key) {
|
||||||
System.out.println(caffeineCacheUtils.getCarCache(vin,key));
|
Cache cache = cacheManager.getCache(cacheName);
|
||||||
|
if (cache != null) {
|
||||||
|
String v = cache.get(key,String.class);
|
||||||
|
log.info("缓存值为: {}",v);
|
||||||
|
}else {
|
||||||
|
log.info("无缓存");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/textSetCache")
|
||||||
|
public void textSetCache(
|
||||||
|
@RequestParam("cacheName") String cacheName,
|
||||||
|
@RequestParam("key") String key,
|
||||||
|
@RequestParam("value") String value) {
|
||||||
|
Cache cache = cacheManager.getCache(cacheName);
|
||||||
|
if (cache != null){
|
||||||
|
cache.put(key, value);
|
||||||
|
log.info("设置缓存成功");
|
||||||
|
}else {
|
||||||
|
log.info("无缓存");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/testDelCache")
|
||||||
|
public void testDelCache(@RequestParam("cacheName") String cacheName) {
|
||||||
|
Cache cache = cacheManager.getCache(cacheName);
|
||||||
|
if (cache != null) {
|
||||||
|
cache.invalidate();
|
||||||
|
log.info("删除缓存成功");
|
||||||
|
}else{
|
||||||
|
log.info("无缓存");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,4 +18,5 @@ import lombok.*;
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class Temporary1 {
|
public class Temporary1 {
|
||||||
|
private String test;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,4 +18,5 @@ import lombok.*;
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class Temporary2 {
|
public class Temporary2 {
|
||||||
|
private String test;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package com.muyu.data.processing.rebbit;
|
package com.muyu.data.processing.rebbit;
|
||||||
|
|
||||||
|
|
||||||
import com.muyu.common.caffeine.utils.CaffeineCacheUtils;
|
|
||||||
import com.muyu.common.rabbit.constants.RabbitConstants;
|
import com.muyu.common.rabbit.constants.RabbitConstants;
|
||||||
import com.rabbitmq.client.Channel;
|
import com.rabbitmq.client.Channel;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
|
@ -31,7 +30,6 @@ import java.util.HashSet;
|
||||||
@Component
|
@Component
|
||||||
@Setter
|
@Setter
|
||||||
public class DownlineRabbitConsumer {
|
public class DownlineRabbitConsumer {
|
||||||
private CaffeineCacheUtils caffeineCacheUtils;
|
|
||||||
@Resource
|
@Resource
|
||||||
private RedisTemplate<String,String> redisTemplate;
|
private RedisTemplate<String,String> redisTemplate;
|
||||||
@Resource
|
@Resource
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
package com.muyu.data.processing.rebbit;
|
package com.muyu.data.processing.rebbit;
|
||||||
|
|
||||||
|
|
||||||
import com.github.benmanes.caffeine.cache.Cache;
|
|
||||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
|
||||||
import com.muyu.common.caffeine.constents.CaffeineContent;
|
|
||||||
import com.muyu.common.caffeine.utils.CaffeineCacheUtils;
|
|
||||||
import com.muyu.common.rabbit.constants.RabbitConstants;
|
import com.muyu.common.rabbit.constants.RabbitConstants;
|
||||||
import com.rabbitmq.client.Channel;
|
import com.rabbitmq.client.Channel;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
|
@ -13,14 +9,10 @@ import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.amqp.core.Message;
|
import org.springframework.amqp.core.Message;
|
||||||
import org.springframework.amqp.rabbit.annotation.Queue;
|
import org.springframework.amqp.rabbit.annotation.Queue;
|
||||||
import org.springframework.amqp.rabbit.annotation.RabbitListener;
|
import org.springframework.amqp.rabbit.annotation.RabbitListener;
|
||||||
import org.springframework.cache.caffeine.CaffeineCache;
|
|
||||||
import org.springframework.cache.support.SimpleCacheManager;
|
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collection;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 上线事件监听
|
* 上线事件监听
|
||||||
|
@ -34,11 +26,12 @@ import java.util.Collection;
|
||||||
@Component
|
@Component
|
||||||
@Setter
|
@Setter
|
||||||
public class GoOnlineRabbitConsumer {
|
public class GoOnlineRabbitConsumer {
|
||||||
private CaffeineCacheUtils caffeineCacheUtils;
|
|
||||||
@Resource
|
@Resource
|
||||||
private RedisTemplate<String,String> redisTemplate;
|
private RedisTemplate<String,String> redisTemplate;
|
||||||
|
// @Resource
|
||||||
|
// private SimpleCacheManager simpleCacheManager;
|
||||||
@Resource
|
@Resource
|
||||||
private SimpleCacheManager simpleCacheManager;
|
private CacheUtils<String,String> cacheUtils;
|
||||||
|
|
||||||
|
|
||||||
@RabbitListener(queuesToDeclare = {@Queue(RabbitConstants.GO_ONLINE_QUEUE)})
|
@RabbitListener(queuesToDeclare = {@Queue(RabbitConstants.GO_ONLINE_QUEUE)})
|
||||||
|
@ -69,18 +62,18 @@ public class GoOnlineRabbitConsumer {
|
||||||
* 车辆上线 - 新增缓存
|
* 车辆上线 - 新增缓存
|
||||||
*/
|
*/
|
||||||
public void addCarCache(String vin) {
|
public void addCarCache(String vin) {
|
||||||
ArrayList<CaffeineCache> caches = new ArrayList<>();
|
|
||||||
// 从Redis中获取缓存信息
|
// 从Redis中获取缓存信息
|
||||||
Collection<String> keys = redisTemplate.keys(CaffeineContent.CAR_VIN_KEY + vin);
|
// ArrayList<CaffeineCache> caches = new ArrayList<>();
|
||||||
keys.forEach(key -> {
|
// Cache<Object , Object> cache = Caffeine.newBuilder().build();
|
||||||
Object string = redisTemplate.opsForValue().get(key);
|
// Collection<String> keys = redisTemplate.keys(vin+":*");
|
||||||
Cache<Object , Object> cache = Caffeine.newBuilder().build();
|
// keys.forEach(key -> {
|
||||||
cache.put(key, string);
|
// String value = redisTemplate.opsForValue().get(key);
|
||||||
// 全部存储到 CaffeineCache集合
|
// cache.put(key, value);
|
||||||
caches.add(new CaffeineCache(vin, cache));
|
// // 全部存储到 CaffeineCache集合
|
||||||
log.info("存储缓存,vin:{}, key:{}, value:{}", vin, key, string);
|
// caches.add(new CaffeineCache(key, cache));
|
||||||
});
|
// log.info("存储缓存,vin:{}, key:{}, value:{}", vin, key, value);
|
||||||
simpleCacheManager.setCaches(caches);
|
// });
|
||||||
log.info("车辆编码:{},本地缓存完成...",vin);
|
// simpleCacheManager.setCaches(caches);
|
||||||
|
// log.info("车辆编码:{},本地缓存完成...",vin);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue