前言
在工作中碰到统计相关的业务,原先是从DB里面读数据,还因为是几乎近乎实时统计,仔细思考发现公式还是有优化的空间,考虑放到内存里面来统计,之前的单体服务倒是很好解决,加锁就可以,但是碰到微服务就要考虑多端并发原子性问题,自然而然想到了Lua脚本。
1.配置Lua脚本
脚本文件
local keyAccountSymbol = KEYS[1]
local keyPnlPrefix = KEYS[2]
local keyPosition = KEYS[3]
local keyPnl = KEYS[4]
local keyBuyPrice = KEYS[5]
local keySellPrice = KEYS[6]
local keyBuyOrderList = KEYS[7]
local keySellOrderList = KEYS[8]
local keyOrderList = KEYS[9]
local side = ARGV[1]
local quantity = ARGV[2]
local price = ARGV[3]
local commission = ARGV[4]
local orderItem = ARGV[5]
local buy = ARGV[6]
local sell = ARGV[7]
local position = tonumber(redis.call('HGET', keyPnlPrefix, keyPosition))
if position == nil then
position = 0
end
local pnl = tonumber(redis.call('HGET', keyPnlPrefix, keyPnl))
if pnl == nil then
pnl = 0
end
-- 一般价格会在2位小数防止精度丢失,所以先去掉小数再计算,手续费也一样
local multiple = 10000
pnl = pnl * multiple
price = price * multiple
commission = commission * multiple
if side == buy then
position = position + quantity
pnl = pnl - (quantity * price)
-- tostring 会防止精度丢失
redis.call('HSET', keyPnlPrefix, keyBuyPrice, tostring(price/multiple))
elseif side == sell then
position = position - quantity
pnl = pnl + (quantity * price)
redis.call('HSET', keyPnlPrefix, keySellPrice, tostring(price/multiple))
end
-- 直接扣减手续费,于已实现收益对应
pnl = pnl - commission
redis.call('HSET', keyPnlPrefix , keyPosition, position)
redis.call('HSET', keyPnlPrefix , keyPnl, tostring(pnl/multiple))
-- 删除队列,保持一边就好
if position > 0 then
redis.call('DEL', keySellOrderList)
elseif position < 0 then
redis.call('DEL', keyBuyOrderList)
else
redis.call('DEL', keySellOrderList)
redis.call('DEL', keyBuyOrderList)
end
-- 写入计算均价队列
redis.call('LPUSH', keyOrderList , orderItem)
return position;
2. Lua脚本语法
Lua脚本跟js语言感觉差不多,计算也会有精度丢失问题,后面会提到,这里主要说的点是KEYS和ARGV的区别还是挺大的,起初觉得都是参数随便传呗,直到碰到 string.format("the value is:%d",4)
格式化参数,才发现区别。用ARGV传过来的参数结果会多两个引号statistic:orders:000001:"0"
,起初还觉得是我双引号填多了,仔细跑跑单元测试发现奥妙所在,用KEYS传过来的参数,就没有出现这种情况。
3. Lua脚本精度丢失问题
涉及到统计业务发现计算还是会有精度丢失问题,拿python举例:
>>> 0.1+0.2
0.30000000000000004
看到这个精度丢失问题头大,java还有个decimal可以操作,这种脚本语言就发现无解,想着精度丢失的原因是因为小数问题,还好没有除法,就把小数去掉再做运算,想着问题解决哈哈,没想到最后显示还是0.30000000000000004
,有没有一种可能是因为redis存值的问题,就想着再优化一步直接转字符类型。终于问题解决,满满的辛酸泪。
local s = 0.1*10+0.2*10
redis.call('SET', 'test1' , s/10) -- 0.29999999999999999
redis.call('SET', 'test2' , tostring(s/10)) -- 0.3
3.java代码调用
java调用方面因为lua脚本和java的类型会有差,主要考虑的是,lua脚本只有number类型,而java有 long, int , double,这里需要注意。
pom
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
java核心代码
/**
* 统计脚本的bean
* @return
*/
@Bean
public DefaultRedisScript<Long> statisticRedisScript() {
DefaultRedisScript<Long> defaultRedisScript = new DefaultRedisScript<>();
defaultRedisScript.setResultType(Long.class);
defaultRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("statistic_pnl.lua")));
return defaultRedisScript;
}
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
@Qualifier("statisticRedisScript")
DefaultRedisScript<Long> statisticRedisScript;
public void consumerStatisticOrder(FilledOrder filledOrder) throws Exception {
redisUtil.sAdd(T0ExtendStatisticKeyConstants.STATISTIC_SYMBOL_KEY + filledOrder.getBrokerAccount(),
filledOrder.getSymbol());
String key = filledOrder.getBrokerAccount() + filledOrder.getSymbol();
List<String> parameters = new ArrayList<>();
parameters.add(key);
parameters.add(T0ExtendStatisticKeyConstants.STATISTIC_PNL_KEY + key);
parameters.add(T0ExtendStatisticKeyConstants.STATISTIC_PNL_PROPERTIES_POSITION_KEY);
parameters.add(T0ExtendStatisticKeyConstants.STATISTIC_PNL_PROPERTIES_PNL_KEY);
parameters.add(T0ExtendStatisticKeyConstants.STATISTIC_PNL_PROPERTIES_BUY_PRICE_KEY);
parameters.add(T0ExtendStatisticKeyConstants.STATISTIC_PNL_PROPERTIES_SELL_PRICE_KEY);
parameters.add(String.format(T0ExtendStatisticKeyConstants.STATISTIC_ORDER_KEY, key, Side.BUY.toChar()));
parameters.add(String.format(T0ExtendStatisticKeyConstants.STATISTIC_ORDER_KEY, key, Side.SELL.toChar()));
parameters.add(String.format(T0ExtendStatisticKeyConstants.STATISTIC_ORDER_KEY, key, filledOrder.getSide()));
Object[] objects = new Object[]{filledOrder.getSide(), filledOrder.getQuantity(),
filledOrder.getPrice(), filledOrder.getCommission(), FilledOrderStatsBo.transform(filledOrder).toJson(),
String.valueOf(Side.BUY.toChar()), String.valueOf(Side.SELL.toChar())
};
Long position = 0L;
try {
position = redisTemplate.execute(statisticRedisScript, parameters, objects);
} catch (Exception e) {
log.error("redis lua script execute exception:", e);
throw e;
}
}
4.Lua脚本特点
在执行脚本的时候发现,虽然lua脚本保证了原子性,但是无法回滚,你可以理解他就是一段脚本,但是因为redis执行脚本是单线程的所以他保证原子性,这点要注意⚠️!
总结
在业务这块没有银弹,适合的才是最好的,总要取舍,要么空间换时间,要么就好好考虑算法优化吧。