php下载15万条数据的csv文件的解决方案_PHP_编程开发_程序员俱乐部

中国优秀的程序员网站程序员频道CXYCLUB技术地图
热搜:
更多>>
 
您所在的位置: 程序员俱乐部 > 编程开发 > PHP > php下载15万条数据的csv文件的解决方案

php下载15万条数据的csv文件的解决方案

 2017/10/31 23:11:37  远去的渡口  程序员俱乐部  我要评论(0)
  • 摘要:今天来总结一下解决了下载16万条数据到csv文件的问题。主要是消费报表,用于财务对账,需要下载成excel文件,我们是下载成.csv文件,遇到数据量大的商家,一个月的消费数据超过15万条,经常就下载进度到99%卡住,其实后台已经报错500或者504。今天彻底解决了下载15万8千多条数据不成功的问题,当然之前还出现过内存溢出的问题,后来找到瓶颈,优化了代码,解决了内存溢出的问题。以前的代码是将数据库取的2万条数据没按分页取,直接一下子取出来,然后将数据拼装成csv文件所需要的整块的data
  • 标签:解决方案 解决 PHP 文件 下载 数据

今天来总结一下解决了下载16万条数据到csv文件的问题。主要是消费报表,用于财务对账,需要下载成excel文件,我们是下载成.csv文件,遇到数据量大的商家,一个月的消费数据超过15万条,经常就下载进度到99%卡住 ,其实后台已经报错500或者504。

?

今天彻底解决了下载15万8千多条数据不成功的问题,当然之前还出现过内存溢出的问题,后来找到瓶颈,优化了代码,解决了内存溢出的问题。

以前的代码是将数据库取的2万条数据没按分页取,直接一下子取出来,然后将数据拼装成csv文件所需要的整块的data,也就是不是逐行写入,第一次浪费内存是在将数据存放在array中,第二次浪费内存是将数据拼装成array再往csv文件中写,所以内存溢出。优化方案是扩大php内存,然后优化代码,php默认的内存128M太小,相对于下载20w条数据的业务来说,所以内存扩大到1G。代码优化方案:将取数据改成分页取,每10000条取一次,然后foreach这一万条数据,一边拼装一边往csv文件中写,fputcsv($fp, $row); 这样优化以后, 下载8万条数据不会报内存溢出。

?

将下载这个特殊的action的内存扩大,

class="php" name="code"> ini_set('memory_limit', '1G');//将内存最大限制改为1G
 

?

之后就不再出现内存溢出了,但是数据量在11万条以上时,仍然下载不成功,查日志发现是PHP Fatal error:? Maximum execution time of 30 seconds exceeded in /xx/xx 目录,所以解决办法是在这个下载的action里前面加上一句:

       set_time_limit(0);//不设置超时时间

?然后再执行下载,发现php里不再报错,但是页面端返回504超时。在页面的response里能看到提示的是nginx报错,去nginx日志里查,果然有Fatal的错误

2017/10/31 15:25:03 [error] 15639#0: *696311 upstream timed out (110: Connection timed out) while reading response header from upstream, client: 61.148.196.162, server: manage.dev.acewill.net, request: "POST /statisticTrade/exportTrade?start=2017-09-30&end=2017-10-31&cardno=&phoneNo=&sid=&type=0&tctype=&pay=&grid=all&source=&startTime=0&endTime=11&tcId= HTTP/1.1", upstream: "fastcgi://127.0.0.1:9001", host: "manage.dev.acewill.net", referrer: "http://manage.dev.acewill.net/statisticTrade/trade?start=2017-09-30&startTime=0&end=2017-10-31&endTime=11&serialnumber=&cardno=&type=&tctype=0&pay=&grid=all&shopselector_shops=&shopselector_isEntire=true&sid=all&search=&entire_chk=on&lv=on&lv=on&chk=on&chk=on&chk=on&chk=on&lv=on&lv=on&chk=on&chk=on&chk=on&chk=on&chk=on&chk=on&chk=on&chk=on&chk=on&lv=on&lv=on&chk=on&chk=on&chk=on&chk=on&lv=on&lv=on&chk=on&lv=on&lv=on&chk=on&lv=on&lv=on&chk=on&lv=on&lv=on&chk=on&lv=on&lv=on&chk=on&lv=on&lv=on&chk=on&chk=on&chk=on&lv=on&chk=on&chk=on&source=all"

?

然后找到运维同事,帮忙修改一下服务器nginx的配置,反向代理超时,下载15万8千多条数据成功。

修改配置就是:

NGINX反向代理的超时报错,解决方法:

        server {
    listen *:80;

    server_name manage.dev.acewill.net;
    index index.php;
    access_log                    logs/manage.dev.access.log     main;
    error_log                     logs/manage.dev.error.log      notice;
    root   /data/dev/www/WeLife/manage_www;
    charset utf-8;

    location / {
        if (!-e $request_filename) {
            rewrite ^/(.*)  /index.php last;
        }

        location ~ .*\.(php|php5)?$
        {
            fastcgi_pass   127.0.0.1:9001;
            fastcgi_index  index.php;
            fastcgi_read_timeout 200;
            fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
            include        fastcgi.conf;
        }

        location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$
        {
            expires      30d;
        }
    }

?

?生效的主要就是那个fastcgi_read_timeout时间。改成了200,测试下载158500条数据没问题。

?

这里贴一下关于写csv文件的重要代码,

  $header = array('流水号','卡号/手机号','交易类型','消费总金额(元)','实收金额(元)','使用储值(元)','使用储值实收金额(元)','使用储值奖励金额(元)','券抵扣(元)','使用代金券','使用礼品券','积分抵现(元)','奖励代金券','奖励礼品券','奖励积分','累计次数返券活动','支付方式','会员等级','门店','交易时间','来源','操作人','备注');
            if ($listType == self::LIST_TYPE_COUPON) {
                $header = array('流水号','卡号/手机号','消费总金额(元)','使用储值(元)','积分抵现(元)','实收金额(元)','支付方式','购买详情','交易类型','门店','交易时间','操作人');
            }
            $path = '/tmp/statistic/';
            $fileName = md5($this->_bid.$starttime.$endtime) . '.csv';
            $exportName = "消费流水".$starttime.'-'.$endtime.'.csv';
            if ($listType == self::LIST_TYPE_COUPON) {
                $exportName = "商品售卖流水".$starttime.'-'.$endtime.'.csv';
            }


            $fpath = '/tmp/statistic/'.$fileName;
            $appName = basename(Bootstrap_Env::get('app_path'));
            $dir = Config::get("$appName");
            $dir = rtrim($dir, '/') . '/' . ltrim($fpath, '/');

            // 自动创建不存在的目录
            $directory = dirname($dir);
            if (!is_dir($directory)) {
                mkdir($directory, 0755, true);
            }

            chmod($directory, 0755);

            if (file_exists($dir)) {
                chmod($dir, 0644);
            }

            $fp = fopen($dir, 'w');
            if (!$fp) {
                throw new Exception('文件无法打开');
            }
            // 输出Excel列名信息
            $head = $header;
            foreach ($head as $i => $v) {
                // CSV的Excel支持GBK编码,一定要转换,否则乱码
                $head[$i] =  iconv("utf-8", "gbk//TRANSLIT", $v);
            }
            // 将数据通过fputcsv写到文件句柄
            fputcsv($fp, $head);


            $staModel = new WeLife_models_Statistic();
            //线上当天数据不用分页处理

            $pageOptions = array();
            $pageOptions1 = array();
            $order = array('tcCreated' => 'DESC');
            if($starttime < date('Ymd',  time())){
                $sh = 0;
            }else{
                $sh = $startHour;
            }
            if($endtime >= date('Ymd',  time())) {

                while (true) {

                    $pageOptions = array('currentPage' => $pageOptions['currentPage'] + 1, 'perPage' => 10000);

                    $todayConsumers = $staModel->listOnLineConsumeDetail($this->_bid, $sid, $grid, $memberType, $resource, $paytype, $consumetype, $cardNo, $phoneNo, $storedtype, $order, Date('Y-m-d', time()), Date('Y-m-d', time()), $sh, $endHour, $pageOptions,array(),'',$tcid,$tableType);
                    if ($todayConsumers['data']) {
                        //活动名称的集合
                        $actResArr = array();
                        $aids = array();

                        foreach ($todayConsumers['data'] as $cons) {
                            if ($cons['aid']) {
                                $aids[] = $cons['aid'];
                            }
                        }


                        $aids = array_unique($aids);//aid去除重复
                        if ($aids) {
                            $activitys = WeLife_src_Rpc_YarClient_Activity::get($this->_bid, $aids);
                            if ($activitys) {
                                foreach ($activitys as $key => $act) {
                                    $actResArr[$key] = $act['aName'];
                                }
                            }
                        }

                        foreach ($todayConsumers['data'] as $key => $data) {

                            if ($listType == self::LIST_TYPE_COUPON) {
                                $v = $this->_coupondealExportData($data, $tcCouponType, $payTypes, $sources, $actResArr, $gradeRules);
                            } else {
                                $v = $this->_dealExportData($data, $tcType, $payTypes, $sources, $actResArr, $gradeRules);//这里单独的方法,处理数据,将数据按文件需要的列的顺序组成一个array
                            }
                            foreach ($v as $i => $va) {
                                $row[$i] = iconv("utf-8", "gbk//TRANSLIT", $va);
                            }
                            fputcsv($fp, $row);//注意这里就是将每一行数据逐行写入到文件中。
                        }

                    }

                    $pageOptions = $todayConsumers['pageOptions'];

                    // 没有更多页。
                    if ($pageOptions['currentPage'] * $pageOptions['perPage'] >= $pageOptions['totalItems']) {
                        break;
                    }
                }
            }

?

其中将数据库查的数据拼装成下载的文件所需要的列,且按列的顺序,代码如下:

    private function _dealExportData($data,$tcType,$payTypes,$sources,$actResArr,$gradeRules){
            $cancelType = Consts::get('Trade', 'Trade_src_Entity_TradeConsume', 'TYPE_CANCEL_CONSUME');
            if($data['tctype'] == $cancelType){
                $status = "-";
            }else{
                $status = "";
            }
            $tcIdstr = $data['tcid'] ."\t" ; //加上"\t"避免科学计数法
            if($tcType[$data['tctype']] == '撤销消费'){
                $tcIdstr = $tcIdstr . '(原流水号:'.$data['tcrelateid'].')';
            }
            if($data['tcThirdId']){
                $tcIdstr = $tcIdstr . '(微信商户单号:'.$data['tcThirdId'].')';
            }

            if(in_array($data['tcsourcetype'], array(Consts::get('Trade', 'Trade_src_Entity_TradeConsume', 'SOURCE_TYPE_YPOS'), Consts::get('Trade', 'Trade_src_Entity_TradeConsume', 'SOURCE_TYPE_WANGPOS'), Consts::get('Trade', 'Trade_src_Entity_TradeConsume', 'SOURCE_TYPE_SUNMIPOS')))){

                if($data['tcpaytype'] ==  Consts::get('Trade', 'Trade_src_Entity_TradeConsume', 'PAY_TYPE_WEIXIN') || $data['tcpaytype'] ==  Consts::get('Trade', 'Trade_src_Entity_TradeConsume', 'PAY_TYPE_WEIXIN_OFFLINE')) {
                    $tcIdstr = $tcIdstr . '(微信商户单号:'.$data['tctransnumber'].')';

                }else if($data['tcpaytype'] == Consts::get('Trade', 'Trade_src_Entity_TradeConsume', 'PAY_TYPE_ALIPAY')) {
                    $tcIdstr = $tcIdstr . '(支付宝交易号:'.$data['tctransnumber'].')';
                }

            }
            $trades[] = $tcIdstr;

            if($data['uphone'] != 0){
                $trades[] = $data['uphone'] .'(卡号:'.$data['uno'].')';
            }  else {
                $trades[] = $data['uno']."\t";
            }
            $trades[] = $tcType[$data['tctype']];
            $trades[] = $tcType[$data['tctype']] == '储值过期'?number_format($data['overdue']/100,2):$data['tctotalfee'] == 0?'-': number_format($data['tctotalfee']/100,2);
            $trades[] = $data['tcfee'] == 0?'-': number_format($data['tcfee']/100,2);
            $trades[] = $tcType[$data['tctype']] == '储值过期'?number_format($data['overdue']/100, 2):($data['tcstoredpay'] == 0?'-':number_format($data['tcstoredpay']/100,2));

            $useStoredPay = 0;
            $usePresentPay = 0;

            if($tcType[$data['tctype']] == '储值过期'){
                $useStoredPay =  $data['overduetclprinciple'] == null ?'-':number_format($data['overduetclprinciple']/100,2) ;
            }else{
                $useStoredPay = $data['tclprinciple'] == null?'-':number_format($data['tclprinciple']/100,2) ;
            }

            if($tcType[$data['tctype']] == '储值过期'){
                $usePresentPay = $data['overduetclprofit'] == null?'-':number_format($data['overduetclprofit']/100,2);
            }else{
                $usePresentPay = $data['tclprofit'] == null?'-':number_format($data['tclprofit']/100,2) ;
            }

            $trades[] = $useStoredPay; //使用储值实收金额(元)
            $trades[] = $usePresentPay; //使用储值奖励金额(元)
            $couPayMoney = $data['tccouponpay'] + $data['tcgiftcouponpay'];
            $trades[] = $couPayMoney == 0 ? '-':number_format($couPayMoney/100, 2);


            //使用代金券coupon
            if($tcType[$data['tctype']] == '撤销消费'){
                $coup = $data['cancelcoupon'] ;
            }else {
                $coup = $data['usecoupon'];
            }

            $trades[] = $this->_dealCoupon($status,$coup);

            //礼品券
            if($tcType[$data['tctype']] == '撤销消费'){
                $giftcoup = $data['cancelgiftcoupon'] ;
            }else{
                $giftcoup = $data['usegiftcoupon'];
            }
            if($giftcoup){
                $giftStr = $this->_dealCoupon($status,$giftcoup);
                if($giftStr != '-'){
                    $giftStr = $giftStr  .'抵扣'.number_format($data['tcgiftcouponpay']/100,2) .'元';
                }
            }else{
                $giftStr = '-';
            }

            $trades[] = $giftStr;
            $trades[] = $data['pointpay'] == 0?'-':number_format($data['pointpay']/100,2);//积分抵现金额
            $trades[] = $this->_dealCoupon($status,$data['sendcoupon']);  //奖励代金券
            $trades[] = $this->_dealCoupon($status,$data['sendgiftcoupon']);  //奖励礼品券
            $trades[] = $data['sendpoint'] == 0?'-':$data['sendpoint'];//奖励的积分
            $trades[] = $tcType[$data['tctype']] == '储值过期'?'-':$data['tclactivityaddup'] == '' || $data['tclactivityaddup'] == 0 ? '-':$actResArr[$data['aid']] == '' ? '-' : $actResArr[$data['aid']].' ' . $status . $data['tclactivityaddup'] .'次';
            $trades[] = ($tcType[$data['tctype']] == '储值过期' || $data['tctotalfee'] == 0)?'-':($payTypes[$data['tcpaytype']]);
            $trades[] = $data['grid'] == 0 ?'普通会员': $gradeRules[$data['grid']];
            $trades[] = $this->_shopNamesArray[$data['sid']] == ''?'-':$this->_shopNamesArray[$data['sid']];
            $trades[] = date('Y-m-d H:i:s',  strtotime($data['tclcreated']));
            $tcResourceType = $sources[$data['tcsourcetype']];
            if($data['tcsourcetype'] == Consts::get('Trade', 'Trade_src_Entity_TradeConsume', 'SOURCE_TYPE_OPENAPI') && $data['tcbizid']){
                $tcResourceType .= '-' .$data['tcbizid'];
            }
            $trades[] = $tcResourceType ;

            $trades[] = $tcType[$data['tctype']] == '储值过期'?'-':$data['mname'];
            $trades[] = $tcType[$data['tctype']] == '储值过期'?'-':$data['remark'];


            return $trades;

    }

?

?

发表评论
用户名: 匿名