首先这是一个操作频繁的自动化定时功能,对比于定时器有着更大的使用空间和性能优化,无论是前端的setTimeout与setInterval 定时器还是后端的TimerTask定时器,在面对短期内的频繁操作都会有着性能和多线程之间的问题,所以这时的队列就起到很重要的作用了,尤其是在于一些消息的推送。下面我使用的是DelayQueue延迟队列和Redis的缓存来实现:
1:
(1) 这里我使用的是maven来管理库,所以第一步我们是导入所要实现功能的jar包,DelayQueue包由于jdk的Util包来提供以及我们所需要的Redis的pom依赖,这里我使用的是StringRedisTemplate来操作redis。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
(2)所有功能都是从底层开始,这里底层已经由队列和缓存封装好了,我们只需要编写业务层和控制层(Rest),需要三个业务层来进行处理所有的逻辑,首先是队列和缓存的业务层,这两个主要实现的是对订单存储和获取以及删除。
DelayServer:队列的业务层,主要用来获取添加到队列中的元素以及对队列的增删。在其中主要有三个变量,启动队列的state,内部接口listener以及队列集合delatQueue。
@Slf4j
@Service
@Getter
@Setter
public class DelayService {
private boolean start ;
private OnDelayedListener listener;
private DelayQueue<DshOrder> delayQueue = new DelayQueue<DshOrder>();
public static interface OnDelayedListener{
public void onDelayedArrived(DshOrder order);
}
public void start(OnDelayedListener listener){
if(start){
return;
}
log.error("DelayService 启动");
start = true;
this.listener = listener;
new Thread(new Runnable(){
public void run(){
try{
while(true){
DshOrder order = delayQueue.take();
if(DelayService.this.listener != null){
DelayService.this.listener.onDelayedArrived(order);
}
}
}catch(Exception e){
e.printStackTrace();
}
}
}).start();
}
public void add(DshOrder order){
delayQueue.put(order);
}
public void remove(String orderId){
DshOrder[] array = delayQueue.toArray(new DshOrder[]{});
if(array == null || array.length <= 0){
return;
}
DshOrder target = null;
for(DshOrder order : array){
if(order.getOrderId() == orderId){
target = order;
break;
}
}
if(target != null){
delayQueue.remove(target);
}
}
}
DshOrder:订单队列中的对象,这里用来存储订单的主键以及根据你业务逻辑需要多久取消时间的规格来,我这里使用的是秒。
@Setter
@Getter
@ApiModel(description = "订单队列对象")
public class DshOrder implements Delayed {
@ApiModelProperty(value = "订单id")
private String orderId;
@ApiModelProperty(value = "超时时间")
private long startTime;
/**
* orderId:订单id
* timeout:自动取消订单的超时时间,秒
* */
public DshOrder(String orderId, int timeout){
this.orderId = orderId;
this.startTime = System.currentTimeMillis() + timeout*1000L;
}
@Override
public int compareTo(Delayed other) {
if (other == this){
return 0;
}
if(other instanceof DshOrder){
DshOrder otherRequest = (DshOrder)other;
long otherStartTime = otherRequest.getStartTime();
return (int)(this.startTime - otherStartTime);
}
return 0;
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(startTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
}
OrderRedisService:Redis业务层,主要用来将订单存入缓存和便利缓存对象到队列中。
public interface OrderRedisService {
/**
* 订单对象加入缓存
*
* @param orderId 订单id
* @param orderObject 订单对象
*/
void saveOrder(String orderId, OrderObject orderObject);
/**
* 获得缓存订单对象
*
* @param orderId 订单id
* @return
*/
String getOrder(String orderId);
/**
* 删除缓存订单对象
*
* @param orderId 订单id
*/
void deleteOrder(String orderId);
/**
* 查询所有需要缓存的订单对象
*
* @return
*/
Set<String> sacn();
/**
* 获得redis键的剩余时间
*
* @param key redis键
* @return 剩余时间
*/
Long getSurplusTime(String key);
}
OrderRedisServiceImpl:Redis业务层实现类,提供订单在缓存中的所有实现。首先保存订单时需要将订单存入缓存中并设置过期时间(这是判断订单是否超时的关键),然后通过id来获取订单在缓存中是否存在,以及删除订单在缓存中存在的主键,其次是一个遍历缓存中所有关于订单的主键方法,这是一个match匹配,前提是在存入订单主键时给定特定的格式才能匹配到所有存在的主键(切记match内的匹配字母,这是你订单主键添加入缓存的开头)。最后,由于缓存存在脏数据或者服务挂了的情况使的当订单主键在Redis中过期但是并没有删除(Redis的主键过期删除存在自动删除和手动删除,所以容易产生脏数据),Redis中提供了TTL命令中的获取主键剩余过期时间的方法,也就是getExpire。
@Slf4j
@Service
public class OrderRedisServiceImpl implements OrderRedisService {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 保存订单并设置过期时间
* @param outTradeId
* @param redisDo
*/
@Override
public void saveOrder(String outTradeId, OrderObject redisDo) {
String key = outTradeId;
//key过期时间为30分钟
redisTemplate.opsForValue().set(key, JsonUtils.obj2Json(redisDo), 600, TimeUnit.SECONDS);
}
/**
* 获取订单
* @param outTradeNo
* @return
*/
@Override
public String getOrder(String outTradeNo) {
String key = outTradeNo;
String message = redisTemplate.opsForValue().get(key);
if(message != null){
return key;
}
return "";
}
/**
* 删除订单
* @param outTradeNo
*/
@Override
public void deleteOrder(String outTradeNo) {
String key = outTradeNo;
redisTemplate.delete(key);
}
/**
* 获取订单中所有的key
* @return
*/
@Override
public Set<String> sacn(){
Set<String> execute = redisTemplate.execute(new RedisCallback<Set<String>>() {
@Override
public Set<String> doInRedis(RedisConnection connection) throws DataAccessException {
Set<String> binaryKeys = new HashSet<>();
Cursor<byte[]> cursor = connection.scan( new ScanOptions.ScanOptionsBuilder().match("order*").count(100).build());
while (cursor.hasNext()) {
binaryKeys.add(new String(cursor.next()));
}
return binaryKeys;
}
});
return execute;
}
@Override
public Long getSurplusTime(String key) {
return redisTemplate.getExpire(key,TimeUnit.SECONDS);
}
}
(3) 队列和缓存的业务层已经写好了,但是考虑到系统队列可能会在系统奔溃后删除了本地的数据,使得服务重启后数据消失,下面我用了监听来在系统启动时,将Redis中的订单加入到队列中,但这些的前提都是在Redis的数据没有被一并清除。
@Slf4j
@Service
public class StartupListener implements ApplicationListener<ContextRefreshedEvent> {
@Autowired
DelayService delayService;
@Autowired
OrderRedisService redisService;
@Autowired
StringRedisTemplate stringRedisTemplate;
@Override
public void onApplicationEvent(ContextRefreshedEvent evt) {
log.error(">>>>>>>>>>>>系统启动完成,onApplicationEvent()");
if (evt.getApplicationContext().getParent() == null) {
return;
}
//自动取消订单
delayService.start(new DelayService.OnDelayedListener(){
@Override
public void onDelayedArrived(final DshOrder order) {
//异步来做
ThreadPoolUtils.execute(new Runnable(){
public void run(){
String orderId = order.getOrderId();
//查库判断是否需要自动取消订单
int surpsTime = redisService.getSurplusTime(orderId).intValue();
log.error("redis键:" + orderId + ";剩余过期时间:"+surpsTime);
if(surpsTime > 0){
log.error("没有需要取消的订单!");
}else{
log.error("自动取消订单,删除队列:"+orderId);
//从队列中删除
delayService.remove(orderId);
//从redis删除
redisService.deleteOrder(orderId);
log.error("自动取消订单,删除redis:"+orderId);
//todo 对订单进行取消订单操作
}
}
});
}
});
//查找需要入队的订单
ThreadPoolUtils.execute(new Runnable(){
public void run() {
log.error("查找需要入队的订单");
Set<String> keys = redisService.sacn();
if(keys == null || keys.size() <= 0){
return;
}
log.error("需要入队的订单keys:"+keys);
log.error("写到DelayQueue");
for(String key : keys){
String orderKey = redisService.getOrder(key);
int surpsTime = redisService.getSurplusTime(key).intValue();
log.error("读redis,key:"+key);
log.error("redis键:" + key + ";剩余过期时间:"+surpsTime);
if(orderKey != null){
DshOrder dshOrder = new DshOrder(orderKey,surpsTime);
delayService.add(dshOrder);
log.error("订单自动入队:"+dshOrder);
}
}
}
});
}
}
(4) 接下来便是对订单的业务层进行最后的操作,也就是在你生成订单的时候给订单加入到队列和缓存中去并设置过期时间,这里我使用的线程池来进行操作。
public class ThreadPoolUtils {
private final ExecutorService executor;
private static ThreadPoolUtils instance = new ThreadPoolUtils();
private ThreadPoolUtils() {
this.executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2);
}
public static ThreadPoolUtils getInstance() {
return instance;
}
public static <T> Future<T> execute(final Callable<T> runnable) {
return getInstance().executor.submit(runnable);
}
public static Future<?> execute(final Runnable runnable) {
return getInstance().executor.submit(runnable);
}
}
最后便是将订单加入到缓存和队列。
//把订单插入到待取消的队列和redis
ThreadPoolUtils.execute(new Runnable() {
@Override
public void run() {
String itrOrderId = "order" + orderId;
//1 插入到待收货队列
DshOrder dshOrder = new DshOrder(itrOrderId, 600);
delayService.add(dshOrder);
log.error("订单order" + orderId + "入队列");
//2插入到redis
orderRedisService.saveOrder(itrOrderId, orderObject);
log.error("订单order" + orderId + "入redis缓存");
}
});
还有一点便是在你设置完后,但不需要自动去实现,要去清理缓存和队列中的数据
String delOrderId = "order" + orderId;
int surpsTime = orderRedisService.getSurplusTime(delOrderId).intValue();
log.error("redis键:" + delOrderId + ";剩余过期时间:"+surpsTime);
if (surpsTime <= 0) {
delayService.remove(delOrderId);
log.error("订单手动出队:" + delOrderId);
orderRedisService.deleteOrder(delOrderId);
log.error("订单手动出redis:" + delOrderId);
}
总结:其实在消息这方面有着很多中间件,例如rabbitMQ,activitiMQ,kafka
RabbitMQ,遵循AMQP协议,由内在高并发的erlanng语言开发,用在实时的对可靠性要求比较高的消息传递上。
kafka是Linkedin于2010年12月份开源的消息发布订阅系统,它主要用于处理活跃的流式数据,大数据量的数据处理上,但大多数用于的是日志。
这让我想到springcloud和dubbo,前者是spring的产物,后者是阿里巴巴的产物。都在微服务中起到决定性的作用。
所以在对实现功能时的选择很重要,如果你的系统所处理的数据量不是很大,我觉得队列和缓存很适合你,这样你可以对消息的传递更加了解,但你使用MQ,kafka的中间件时,你会发现使用起来更加轻松,但对于数据量大的系统来说,中间件是最好的选择,在这个大数据的时代,高并发,多线程,分布式会越来越重要,也是你技术上升的一个很大转折点。