CSV导出长数值展示科学计数问题

项目中使用 CSV 导出数据时,如果数值过长则使用科学计数法展示,这往往跟实际的需求不符。究其原因不是 CSV 造成的,CSV 就是简单的文本格式,使用文本编辑器打开导出的 CSV 文件,可以发现数值就是原本的数值。是 Excel 以科学计数法存储长数值。

看网上的解决方案大都是在长数值的前/后加上一个字符,比如”\t”,让 Excel 以字符串的形式展示数值。

如果后续还需要对 CSV 文件做其它处理,加字符的方式可以会带来额外的工作,所以不如直接改为 Excel 导出。

Excel 本身也可以直接导入 CSV 文件,设置对应字段的格式。在 “数据”->“从文本/CSV”->选择对应的 csv 文件->点击转换数据->将对应的数值字段设置为文本 type text -> 点击关闭并上载

image-20221104143914628

记一次问题排查经历

先简单描述下背景。我们的项目是商家发布订单,可以对订单进行加减量,用户以任务为单位做商家发布的订单,每一个任务有各种状态,不同状态会有对订单加减量的操作,比如用户超时未提交会有脚本自动返还扣的量。

我们有一个脚本每天都会对一下订单的消耗各数据是否一致,就是订单支付的总额与订单的消耗、余量的一个对比。有一天报警了一个订单数据不一致的情况,我排查了一下商家的行为,存在大量的加一个量的情况,我以为是这里出了问题,便以此为目标去寻找证据,最终没找到,商家加减量的代码逻辑并没有看到漏洞。不过排查的过程中得到了一个结论——商家的余量比实际支付的多了一个。于是我们手动执行 sql 给这个订单减了一个量,这个问题就暂时搁置了。

前两天又有订单出现了这个问题,我这次换了个思路排查了一下,确定了出现这个问题的原因。由于上次已经确定了不是商家加减量那里的问题,这次就先看了下订单操作余量的代码逻辑(就一处入口,各处调用),以及调用这块逻辑的地方。手动操作的地方是用户开始、放弃任务及商家审核任务,自动操作的地方是两个定时任务——超时未提交及超时未重提。据此提出了一个可能:手动操作与定时脚本同时操作了同一个任务,导致量差了。最终也确实定位了是这个原因,由于任务超时我们并没有限制不能提交或放弃(定时任务将状态更改后才会限制),导致定时任务执行放弃的时候有可能会多退一个量。

在第一次出现这个问题未定位到原因时,再一次出现时,心里是有点抵触去解决的,可能是因为前一次的失败阴影。不过这都是正常的心理,调整好心态就好了。以我的经验来看,遇到的问题基本上都是可以解决的,就是要反复去面对嘛,失败几次是很正常的。


后来是新写了一个更新订单状态的方法,当状态更新失败后,抛出异常来使事务回滚的方式解决的这个问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$this->default_model->pdo->beginTransaction();
try {
// 修改任务状态
$res = $task->updateTaskStatusUseOldStatus((int)$item['task_id'], $new_status, (int)$item['status']);
if (!$res) {
throw new PDOException('修改任务状态失败');
}
// ... 其它业务
} catch (Exception $e) {
$this->default_model->pdo->rollBack();
}

public function updateTaskStatusUseOldStatus(int $task_id, int $status, int $old_status): int
{
$sql = "UPDATE task SET status=:status WHERE task_id=:task_id AND status=:old_status";
$this->default_model->query($sql, [$status, $task_id, $old_status]);

return (int)$this->default_model->rowCount();
}

分块处理数据

项目中经常碰到一些慢 sql,sql 本身没有继续优化的空间,该使用的索引也都用上了。想要继续优化,就要考虑每次分块处理,也就是把原先大的结果集分割成一个个的小块,类似于 Laravelchunk 方法。

这里基于我司的 PHP 框架写了个简单的使用方式,通过传入 sql 的方式把 sql 中的占位符 ? 号替换为 where in 条件中的子集。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
trait getDataInChunkTrait
{
/**
* 分块获取数据:目前只支持对 sql 中单个 IN 条件的分块获取
*
* @param array $list 要放入 where 条件 IN 的集合
* @param string $sql 执行的sql,其中的 ? 会被 list 子集替换
* @param int $size
*
* @return array
*/
public function getDataInChunk(array $list, string $sql, int $size = 500): array
{
$result = [];
/** @var Model $model */
$model = M('');
foreach (array_chunk($list, $size) as $sub_list) {
$sub_str = implode(',', $sub_list);
$run_sql = str_replace('?', $sub_str, $sql);
$result = array_merge($result, $model->select($run_sql) ?: []);
}
return $result;
}
}

可以这么使用

1
2
3
4
5
6
7
$sql    = "SELECT order_id, max(create_time) as running_time
FROM order_operate
WHERE
order_id IN (?)
AND after_status = 4
GROUP BY order_id";
$orders = $this->getDataInChunk($orders, $sql, 200);

Beanstalk使用

Beanstalk 介绍

beanstalk 的官网介绍,beanstalk 是一个简单、快速的工作队列,其设计之初是异步执行耗时的操作,来减少大型 web 应用的延迟。它是一个内存队列,也支持把任务写入 binlog,完成持久化。

核心概念

  • job : 一个需要异步处理的任务,是 Beanstalkd 中的基本单元,需要放在一个 tube 中。

  • tube :一个有名的任务队列,用来存储统一类型的 job,是 producer 和 consumer 操作的对象。

  • producer :Job 的生产者,通过 put 命令来将一个 job 放到一个 tube 中。

  • consumer :Job 的消费者,通过 reserve/release/bury/delete 命令来获取 job 或改变 job 的状态

  • 任务优先级:任务(job)可以有 0~2^32 个优先级, 0 代表最高优先级。beanstalkd 采用最大最小堆(Min-max heap)处理任务优先级排序, 任何时刻调用 reserve 命令的消费者总是能拿到当前优先级最高的任务,时间复杂度为 O(logn).

  • 延时任务(delay):有两种方式可以延时执行任务(job): 生产者发布任务时指定延时;或者当任务处理完毕后,消费者再次将任务放入队列延时执行(RELEASE with )。

  • 任务超时重发(time-to-run): Beanstalkd 把任务返回给消费者以后:消费者必须在预设的 TTR (time-to-run) 时间内发送 delete /release/ bury 改变任务状态;否则 Beanstalkd 会认为消息处理失败,然后把任务交给另外的消费者节点执行。如果消费者预计在 TTR (time-to-run) 时间内无法完成任务, 也可以发送 touch 命令,它的作用是让 Beanstalkd 从系统时间重新计算 TTR (time-to-run).

  • 任务预留(buried):如果任务因为某些原因无法执行, 消费者可以把任务置为 buried 状态让 Beanstalkd 保留这些任务。管理员可以通过 peek buried 命令查询被保留的任务,并且进行人工干预。简单的, kick 能够一次性把 n 条被保留的任务放回准备队列。

实际使用

pda/pheanstalk 为例

生产者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
require __DIR__ . '/vendor/autoload.php';

use Pheanstalk\Pheanstalk;
# 创建客户端
$pheanstalk = Pheanstalk::create('127.0.0.1', 11300, 10);

# 往指定管道生产一个字符串 job
$pheanstalk
->useTube('testtube')
->put("job payload goes here\n");

# 往指定管道生产一个 encode 后的数组
$pheanstalk
->useTube('testtube')
->put(
json_encode(['test' => 'data']),
Pheanstalk::DEFAULT_PRIORITY, // job 的优先级
30, // 延迟多长时间处理
60 // beanstalk 重试时间
);

消费者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
require __DIR__ . '/vendor/autoload.php';
use Pheanstalk\Pheanstalk;

$pheanstalk = Pheanstalk::create('127.0.0.1', 11300, 10);

// 从 testtube 管道获取任务
$pheanstalk->watch('testtube');

// 挂起直到有一个任务返回
$job = $pheanstalk->reserve();
// 等待 3 秒直到有一个任务返回,超过 3 秒则退出
$job = $pheanstalk->reserveWithTimeout(3);

try {
// 获取 job 的数据
$jobPayload = $job->getData();
// 处理业务
// 如果业务的处理时间超过 beanstalk 对该任务的重试时间,可以通过 touch 方法,重置重试时间
$pheanstalk->touch($job);

// 如果当前不处理此任务,将任务加入 bury 队列,等待调用 kick 方法再次推入 ready 队列
// $pheanstalk->bury($job);

// 处理完任务之后进行删除,不删除 beanstalk 会一直重试
$pheanstalk->delete($job);
}
catch(\Exception $e) {
// 如果有异常可以重新将 job 推入任务,等待重试
$pheanstalk->release($job);
// 延迟 5 秒处理任务
$pheanstalk->release($job, Pheanstalk::DEFAULT_PRIORITY, 5);
}

参考链接:https://www.fzb.me/beanstalkd/