版权所有,禁止匿名转载;禁止商业使用。
经过一段时间项目的沉淀之后,对实际应用中的多线程开发及队列使用产生了深厚的兴趣,也将<<java并发编程实战>>仔细的阅读了两三遍,也看了很多并发编程的实践项目,也有了深刻的理解与在实践中合理应用队列、多线程开发的应用场景
1、真实应用场景描述:
由于一段时间以来要针对公司整个电商平台包括官网、移动端所有的交易数据进行统计,统计指标包括:pv、uv、实付金额、转化率、毛利率等等,按照各种不同的维度来统计计算出当前交易系统的各个指标的数据,但要求该项目是独立的,没有任务其它资源的协助及接品提供。经过一番xxxx思考讨论之后。业务上决定用以下解决方案:
A: 用一个定时服务每隔10秒去别的系统数据库抓取上一次查询时间以来新确认的订单(这种订单表示已经支付完在或者客户已经审核确认了),然后将这些订单的唯一编号放入redis队列。
B: 由于用到了队列,根据经验自然而然的想到了 启动单独的线程去redis队列中不断获取要统计处理的订单编号,然后将获取到的订单编号放入线程池中进行订单的统计任务处理。
开发实现:
FetchConfirmOrdersFromErpJob.java /** * 1、从redis中获取上次查询的时间戳 * 2、将当前时间戳放入到redis中,以便 下次按这个时间查询 * 3、去erp订单表查询confirm_time>=上次查询的时间的订单,放入队列中 */ @Scheduled(cron = "0/30 * * * * ?") public void start(){ logger.info("FetchConfirmOrdersFromErpJob start................."+ new Date()); StopWatch watch=new StopWatch(); watch.start(); //上次查询的时间 String preQueryTimeStr=this.readRedisService.get(Constans.CACHE_PREQUERYORDERTIME); Date now=new Date(); if(StringUtils.isBlank(preQueryTimeStr)){ preQueryTimeStr=DateFormatUtils.format(DateUtils.addHours(now, -1), Constans.DATEFORMAT_PATTERN_YYYYMMDDHHMMSS);//第一次查询之前一个小时的订单 // preQueryTimeStr="2015-05-07 10:00:00";//本地测试的时候使用 } //设置当前时间为上次查询的时间 this.writeRedisService.set(Constans.CACHE_PREQUERYORDERTIME, DateFormatUtils.format(now, Constans.DATEFORMAT_PATTERN_YYYYMMDDHHMMSS)); List<Map<String, Object>> confirmOrderIds = this.erpOrderService.selectOrderIdbyConfirmtime(preQueryTimeStr); if(confirmOrderIds==null){ logger.info("query confirmOrderIds is null,without order data need dealth.........."); return; } for (Map<String, Object> map : confirmOrderIds) { //将订单编号放入队列中 this.writeRedisService.lpush(Constans.CACHE_ORDERIDS, map.get("channel_orderid").toString()); logger.info("=======lpush orderid:"+map.get("channel_orderid").toString()); } watch.stop(); logger.info("FetchConfirmOrdersFromErpJob end................."+ new Date()+" total cost time:"+watch.getTime()+" dealth data count:"+confirmOrderIds.size()); } derCalculate.java 队列获取订单线程 public class OrderCalculate { private static final Log logger = LogFactory.getLog(OrderCalculate.class); @Autowired private static WriteRedisService writeRedisService; private static ExecutorService threadPool=Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()*4 ,new TjThreadFactory("CalculateAmount")); static{ Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { @Override public void run() { QueuePop.stop(); threadPool.shutdown(); } })); } public void init(){ if(writeRedisService==null){ writeRedisService=SpringContext.getBean(WriteRedisService.class); } new Thread(new QueuePop(),"OrderIdQueuePop").start();//由于是用redis做的队列,所以只要使用一个线程从队列里拿就ok } static class QueuePop implements Runnable{ volatile static boolean stop=false; @Override public void run() { while(!stop){ //不断循环从队列里取出订单id String orderId=null; try { orderId = writeRedisService.rpop(Constans.CACHE_ORDERIDS); if(orderId!=null){ logger.info("pop orderId:"+orderId); //将获取的订单编号交给订单统计任务处理线程处理 threadPool.submit(new CalculateAmount(Integer.parseInt(orderId),new Date())); } } catch (Exception e1) { logger.error("",e1); } //根据上线后的业务反馈来确定是否改成wait/notify策略来及时处理确认的订单 try { Thread.sleep(10); } catch (InterruptedException e) { logger.error("",e); // Thread.currentThread().interrupt(); //stop=true;//线程被打算继续执行,不应该被关闭,保证该线程永远不会死掉 } } } public static void stop(){ stop=true; } } }
lculateAmoiunt.java 订单任务处理
public class CalculateAmount implements Runnable { private static final Log logger = LogFactory.getLog(CalculateAmount.class); private int orderId; private Date now;//确认时间 这个时间有一定的延迟,基本可以忽略,如果没什么用 private OrderService orderServices; private OrdHaveProductService ordHaveProductService; private OrdPayByCashbackService ordPayByCashbackService; private OrdPayByCouponService ordPayByCouponService; private OrdPayByGiftCardService ordPayByGiftCardService; private StatisticsService statisticsService; private WriteRedisService writeRedisService; private ReadRedisService readRedisService; private ErpOrderGoodsService erpOrderGoodsService; private ErpOrderService erpOrderService; public CalculateAmount(int orderId,Date now) { super(); this.orderId = orderId; this.now=now; orderServices=SpringContext.getBean(OrderService.class); ordHaveProductService=SpringContext.getBean(OrdHaveProductService.class); ordPayByCashbackService=SpringContext.getBean(OrdPayByCashbackService.class); ordPayByCouponService=SpringContext.getBean(OrdPayByCouponService.class); ordPayByGiftCardService=SpringContext.getBean(OrdPayByGiftCardService.class); statisticsService=SpringContext.getBean(StatisticsService.class); writeRedisService=SpringContext.getBean(WriteRedisService.class); readRedisService=SpringContext.getBean(ReadRedisService.class); erpOrderGoodsService=SpringContext.getBean(ErpOrderGoodsService.class); erpOrderService=SpringContext.getBean(ErpOrderService.class); } @Override public void run() { logger.info("CalculateAmount task run start........orderId:"+orderId); StopWatch watch=new StopWatch(); watch.start(); /** * 取出订单相关的所有数据同步到统计的库中 */ //TODO 考虑要不要将下面所有操作放到一个事务里面 List<Map<String, Object>> orders = this.orderServices.selectOrderById(orderId); if(orders!=null&&orders.size()>0){ Map<String, Object> order = orders.get(0); String orderSN=U.nvl(order.get("OrderSN"));//订单编号 Integer userId=U.nvlInt(order.get("usr_UserID"),null);//用户d Integer status=U.nvlInt(order.get("Status"),null);//状态 Date createTime=now;//(Date)order.get("CreateTime");//创建时间 Date modifyTime=now;//(Date)order.get("ModifyTime");// 更新时间 BigDecimal discountPrice=U.nvlDecimal(order.get("DiscountPrice"),null);//优惠总额 满减金额 BigDecimal payPrice=U.nvlDecimal(order.get("PayPrice"), null);//实付金额 BigDecimal totalPrice=U.nvlDecimal(order.get("TotalPrice"), null);//总金额 //从erp里查询出订单的确认时间 int dbConfirmTime=0; try { dbConfirmTime = this.erpOrderService.selectConfirmTimeByOrderId(orderId); } catch (Exception e2) { logger.error("",e2); } Date ct=new Date(dbConfirmTime*1000L); int[] dates=U.getYearMonthDayHour(ct);// if(modifyTime!=null){ dates=U.getYearMonthDayHour(modifyTime);// } int year=dates[0];//年 int month=dates[1];//月 int day=dates[2];//日 int hour=dates[3];//小时 String ordersId=orderId+"";//生成订单id //查询订单的来源和搜索引擎关键字 String source=""; String seKeyWords=""; List<OrdersData> orderDataList=this.statisticsService.selectOrdersDataByOrdersId(orderSN); if(orderDataList!=null&&!orderDataList.isEmpty()){ OrdersData ordersData = orderDataList.get(0); source=ordersData.getSource(); seKeyWords=ordersData.getSeKeyWords(); } //TODO 将订单入库 ArrayList<RelOrders> relOrdersList = Lists.newArrayList(); RelOrders relOrders=new RelOrders(orderSN,userId+"",Byte.valueOf(status+""),source,seKeyWords,IsCal.未计算.getFlag(),(byte)U.getSimpleYearByYear(year),(byte)month,(byte)day,(byte)hour,ct,createTime,modifyTime); relOrdersList.add(relOrders); try { relOrders.setConfirmTime(ct); //查询RelOrders是否存在 RelOrders dbOrders=this.statisticsService.selectByPrimaryKey(orderSN); if(dbOrders!=null){ //更新 dbOrders.setStatus(Byte.valueOf(status+"")); dbOrders.setConfirmTime(ct); dbOrders.setModifyTime(modifyTime); this.statisticsService.updateByPrimaryKeySelective(dbOrders); return; }else{ Integer relResult=this.statisticsService.insertRelOrdersBatch(relOrdersList); } } catch (Exception e) { logger.error("insertRelOrdersBatch error",e); } /** * 查这个订单的返现、优惠券、礼品卡 的金额 */ List<Map<String, Object>> cashs = this.ordPayByCashbackService.selectDecutionPriceByOrderId(orderId); List<Map<String, Object>> coupons = this.ordPayByCouponService.selectDecutionPriceByOrderId(orderId); BigDecimal cashAmount=U.getValueByKey(cashs, "DeductionPrice", BigDecimal.class, BigDecimal.ZERO);//返现金额 BigDecimal couponAmont=U.getValueByKey(coupons, "DeductionPrice", BigDecimal.class, BigDecimal.ZERO);//红包金额 /** * 查询出这个订单的所有商品 */ List<Map<String, Object>> products=null; Map<String,Object> productToKeyWordMap=Maps.newHashMap(); try { products = this.ordHaveProductService.selectByOrderId(orderId); List<OrdersItemData> ordersItemDataList=this.statisticsService.selectOrdersItemDataByOrdersId(orderSN); if(ordersItemDataList!=null){ for (OrdersItemData ordersItemData : ordersItemDataList) { productToKeyWordMap.put(ordersItemData.getItemId(), ordersItemData.getKeyWords()); } } } catch (Exception e1) { logger.error("",e1); } if(products!=null){ ArrayList<RelOrdersItem> relOrdersItemList = Lists.newArrayList(); for (Map<String, Object> product : products) { Integer productId=U.nvlInt(product.get("pro_ProductID"), null);//商品Id Integer buyNo=U.nvlInt(product.get("BuyNo"), 0);//购买数量 String SN=U.nvl(product.get("SN"),""); BigDecimal buyPrice=U.nvlDecimal(product.get("BuyPrice"), BigDecimal.ZERO);//购买价格 BigDecimal buyTotalPrice=U.nvlDecimal(product.get("BuyTotalPrice"), null);//购买总价格 BigDecimal productPayPrice=U.nvlDecimal(product.get("PayPrice"), null);//单品实付金额 BigDecimal cost=null;//商品成本 TODO 调别人的接口 BigDecimal realtimeAmount=null;//实付金额 BigDecimal pdCashAmount=BigDecimal.ZERO;//每个商品的返现 BigDecimal pdcouponAmont=BigDecimal.ZERO;//每个商品的优惠券 //商品价格所占订单比例 if(buyTotalPrice!=null&&totalPrice!=null&&totalPrice.doubleValue()!=0){ pdCashAmount=buyTotalPrice.divide(totalPrice,8,BigDecimal.ROUND_HALF_UP).multiply(cashAmount).setScale(2,BigDecimal.ROUND_HALF_UP); pdcouponAmont=buyTotalPrice.divide(totalPrice,8,BigDecimal.ROUND_HALF_UP).multiply(couponAmont).setScale(2,BigDecimal.ROUND_HALF_UP); discountPrice=buyTotalPrice.divide(totalPrice,8,BigDecimal.ROUND_HALF_UP).multiply(discountPrice).setScale(2,BigDecimal.ROUND_HALF_UP); } realtimeAmount=buyTotalPrice.subtract((pdCashAmount.add(pdcouponAmont).add(discountPrice))).setScale(2,BigDecimal.ROUND_HALF_UP); RelOrdersItem item=new RelOrdersItem(U.randomUUID(),orderSN,productId,SN,buyNo,realtimeAmount,U.nvl(productToKeyWordMap.get(productId))); relOrdersItemList.add(item); //如果确认时间属于同一天的话,将商品实付金额放入到redis排行榜中 if((status==1||status==5||status==6||status==7||status==11)&&DateUtils.isSameDay(new Date(), ct)){ //如果订单的状态是这几种,刚将该商品加入到实付金额的排行 榜中 dates=U.getYearMonthDayHour(ct);// int days=dates[2]; //某一个商品某一天的实付金额 BigDecimal itemRelAmount=BigDecimal.ZERO; //从redis里取出这个商品的实付金额,然后累加 String itemRelAmountStr=readRedisService.get(Constans.CACHE_PERITEMRELAMOUNTSS_KEY_PREFIX+productId+Constans.CACHE_KEY_SEPARATOR+days); if(StringUtils.isNotBlank(itemRelAmountStr)){ itemRelAmount=new BigDecimal(itemRelAmountStr); } realtimeAmount=itemRelAmount.add(realtimeAmount); writeRedisService.set(Constans.CACHE_PERITEMRELAMOUNTSS_KEY_PREFIX+productId+Constans.CACHE_KEY_SEPARATOR+days, realtimeAmount.toPlainString()); writeRedisService.lpush(Constans.CACHE_DELKEYS_KEY_PRDFIX+days, Constans.CACHE_PERITEMRELAMOUNTSS_KEY_PREFIX+productId+Constans.CACHE_KEY_SEPARATOR+days); writeRedisService.zadd(Constans.CACHE_ITEMREALAMOUNTSS_KEY+days, realtimeAmount.doubleValue(), productId+""); //确认的销量 Long itemCount= writeRedisService.incrBy(Constans.CACHE_ITEMSALES_KEY_PRDFIX+productId+Constans.CACHE_KEY_SEPARATOR+days,buyNo); writeRedisService.zadd(Constans.CACHE_ITEMSALES_SS_KEY_PRDFIX+days, itemCount, productId+""); String itemType=""; Map<String, String> pMap = this.readRedisService.hmget(Constans.CACHE_PRODUCT_KEY+productId); itemType=pMap.get("categoryId"); if(StringUtils.isNotBlank(itemType)){ if(ProductCategory.isGuanBai(itemType)){ //如果是白酒 官白的访客数排行 this.writeRedisService.zadd(Constans.CACHE_ITEMREALAMOUNTWHITESS_KEY+days, realtimeAmount.doubleValue(), productId+"");// //确认的销量排行 this.writeRedisService.zadd(Constans.CACHE_ITEMSALESWHITE_SS_KEY_PRDFIX+days, itemCount, productId+"");// }else if(ProductCategory.isGuanHong(itemType)){ //官红的访客数排行 this.writeRedisService.zadd(Constans.CACHE_ITEMREALAMOUNTREDSS_KEY+days, realtimeAmount.doubleValue(), productId+"");// //确认的销量排行 this.writeRedisService.zadd(Constans.CACHE_ITEMSALESRED_SS_KEY_PRDFIX+days, itemCount, productId+"");// } } //某一个商品的销量加入删除列表 writeRedisService.lpush(Constans.CACHE_DELKEYS_KEY_PRDFIX+days, Constans.CACHE_ITEMSALES_KEY_PRDFIX+productId+Constans.CACHE_KEY_SEPARATOR+days); } } try { //TODO 将订单商品明细入库 this.statisticsService.insertRelOrdersItemBatch(relOrdersItemList); //再将订单的状态改为已计算 this.statisticsService.updateIsCal(orderSN,IsCal.已计算.getFlag());//将是否计算改成已计算 //该订单的所有商品的成本同步到现在的库中。 this.calOrderProductCostSync(orderId,orderSN,products); } catch (Exception e) { logger.error("insertRelOrdersItemBatch or updateIsCal error",e); } } } watch.stop(); logger.info("CalculateAmount task run end........total cost time:"+watch.getTime()+" orderId:"+orderId); } private void calOrderProductCostSync(int orderId,String orderSN,List<Map<String, Object>> products){ List<Map<String, Object>> ordersList = this.erpOrderGoodsService.selectProductCostByOrderSN(orderSN); if(ordersList==null||ordersList.isEmpty()){ logger.error("according orderId to query some data from erp return is null........."); return; } Map<String, String> itemIdToItemSnMap = U.convertToMapByList(products, "pro_ProductID", "SN"); List<RelItemCosts> list=Lists.newArrayList(); for (Map<String, Object> map : ordersList) { RelItemCosts itemCost=new RelItemCosts(); if(map==null){ continue; } Integer itemId=U.nvlInt(map.get("goods_id"),-99); BigDecimal costs=U.nvlDecimal(map.get("Dynamic_price"), BigDecimal.ZERO); itemCost.setId(U.randomUUID()); itemCost.setOrdersId(orderId+""); itemCost.setOrdersNo(orderSN); itemCost.setItemId(itemId); itemCost.setItemNo(itemIdToItemSnMap.get(itemId+"")); itemCost.setCosts(costs); itemCost.setCreateTime(new Date()); itemCost.setModifyTime(new Date()); list.add(itemCost); } this.statisticsService.insertRelItemCostsBatch(list); } }
注意:
1、redis2.6版本使用lpush、rpop出列的时候会丢失数据。换成2.8及以上的版本运行正常。
2、由于应用会部署到多个结点,所以无法直接采用java的BlockingQueue阻塞队列,帮采用redis提供的队列支持。
3、如果要做到统计的绝对实时,最好采用大数据的实时计算的解决方案:kafka+storm 来实现
原文 http://www.cnblogs.com/jhoney/p/4492324.html