Scan 时进行 Rename 导致结果未定义

2018-01-19 10:35:55来源:http://blog.kankanan.com/article/scan-65f68fdb884c-rename-5b作者:看看俺人点击

分享

我们有一个服务有这种场景:


Web 服务提供查询接口返回一个很大的数据集给客户端


这个数据集使用Sets
结构进行保存,读取这个数据集使用sscan
命令每次返回100
条记录,直到读完为止。


后台计算脚本计算并更新数据集


计算结果先放到一个临时key
中,等到计算完再通过rename
覆盖正式的key


最近,客户方反映获取的数据集偏小,正常应该是 20 多万条记录,结果只取到几万条记录。



我们怀疑是 Scan 过程中如果数据集被rename
覆盖,结果可能是未定义的,见以下测试程序:



redis_rename_when_scan.js


"use strict";
const Redis = require('ioredis');
const async = require('async');
const _ = require('lodash');
const crypto = require('crypto');
const key_ = "test:set_";
const key = "test:set";
const min = 0;
const max = 100000;
const prefix_length = 20;
let scan_round = 0;
let write_round = 0;
function add(client, value, callback) {
if (value > max) {
return callback(null);
}
crypto.randomBytes(parseInt(prefix_length / 2, 10), function(err, buffer) {
if (err) {
return callback(err);
}
// Test full random dataset
//let prefix = buffer.toString('hex');
// Test fixed dataset
//let prefix = _.pad('', prefix_length, '0');
// Test partial random dataset
let prefix = _.pad('', prefix_length - 1, '0') + buffer.toString('hex')[0];
return client.sadd(key_, prefix + value, function(err) {
if (err) {
return callback(err);
}
value += 1;
return add(client, value, callback);
});
});
}
function write(client) {
if (!client) {
client = new Redis();
}
client.del(key_, function(err) {
if (err) {
throw err;
}
add(client, min, function(err) {
if (err) {
throw err;
}
client.rename(key_, key, function(err) {
if (err) {
throw err;
}
console.log("write#" + write_round + " DONE");
++write_round;
write(client);
});
});
});
}
function ready(client, callback) {
if (!client) {
client = new Redis();
}
client.exists(key, function(err, exists) {
if (err) {
callback(err);
} else if (exists) {
callback(null);
} else {
setTimeout(function() {
ready(client, callback);
}, 100);
}
});
}
function init(callback) {
let client = new Redis();
client.del(key_, function(err) {
if (err) {
throw err;
}
client.del(key, function(err) {
if (err) {
throw err;
}
callback();
});
});
}
function scan(client) {
if (!client) {
client = new Redis();
}
ready(null, function() {
let results = [];
var cursor = 0;
async.doWhilst(function(done) {
client.sscan(key, cursor, 'COUNT', 100, function(err, values) {
if (err) {
throw err;
}
cursor = +values[0];
values[1].forEach(function(value) {
results.push(value);
});
// console.log("scan#" + scan_round + " scan " + results.length + " values");
setTimeout(done, 5);
});
}, function() {
return cursor > 0;
}, function(err) {
if (err) {
throw err;
}
let count_error = false;
if (results.length != (max - min + 1)) {
if (results.length < (max - min + 1)) {
console.error("scan#" + scan_round + " Expected " + (max - min + 1) + " values, Got " + results.length);
count_error = true;
} else {
console.warn("scan#" + scan_round + " Expected " + (max - min + 1) + " values, Got " + results.length);
}
}
if (!count_error) {
results.sort((a, b) => parseInt(a.substring(prefix_length), 10) - parseInt(b.substring(prefix_length), 10));
let uniq_results = _.sortedUniq(results);
if (uniq_results.length != results.length) {
console.warn("scan#" + scan_round + " Expected 0 duplicated values, Got " + (results.length - uniq_results.length));
}
let result_error = false;
for (let i = 0; i <= (max - min); ++i) {
if (parseInt(results[i].substring(prefix_length), 10) != (min + i)) {
//console.log("results: " + results.toString());
console.error("scan#" + scan_round + " value#" + i + " Expected " + results[i].substring(0, prefix_length) + (min + i) + ", Got " + results[i]);
result_error = true;
break;
}
}
if (!result_error) {
console.log("scan#" + scan_round + " OK");
}
}
++scan_round;
scan(client);
});
});
}
init(function() {
write(null);
scan(null);
});


测试表明,取决于数据集变化的幅度,变化越大的数据集,获取到的结果偏差越大。 但是获取到的记录数终始相差不大,10
万条记录偏差+-200
左右,前面遇到的数据 记录数急剧减少可能是其它问题引起,如:计算出的记录数确实是有剧减。



记录数偏差不大可能是因为Redis
在存储Sets
数据时是有排序的,scan
使用的cursor
类似offset
,所以上一个数据集的cursor
用于查询下一个数据集时偏差也 不会太大。



但是这始终是一个问题,一个稳健的系统不应该有未定义行为。一个可行的解决方案是不要 使用rename
,而是在正式的key
中保存临时key
,查询时先从正式的key
取 到真正保存数据的临时key
,再从临时key
中取数据,只要确保每次计算都使用不同 的临时key
就可以避免数据访问冲突导致的未定义行为。


最新文章

123

最新摄影

闪念基因

微信扫一扫

第七城市微信公众平台