配置Redis LUA脚本

2023-10-25 15:49:52 浏览数 (2)

前言

在工作中碰到统计相关的业务,原先是从DB里面读数据,还因为是几乎近乎实时统计,仔细思考发现公式还是有优化的空间,考虑放到内存里面来统计,之前的单体服务倒是很好解决,加锁就可以,但是碰到微服务就要考虑多端并发原子性问题,自然而然想到了Lua脚本。

1.配置Lua脚本

脚本文件

代码语言:javascript复制
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举例:

代码语言:javascript复制
>>> 0.1 0.2
0.30000000000000004

看到这个精度丢失问题头大,java还有个decimal可以操作,这种脚本语言就发现无解,想着精度丢失的原因是因为小数问题,还好没有除法,就把小数去掉再做运算,想着问题解决哈哈,没想到最后显示还是0.30000000000000004,有没有一种可能是因为redis存值的问题,就想着再优化一步直接转字符类型。终于问题解决,满满的辛酸泪。

代码语言:javascript复制
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

代码语言:javascript复制
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

java核心代码

代码语言:javascript复制
    /**
     * 统计脚本的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执行脚本是单线程的所以他保证原子性,这点要注意⚠️!

总结

在业务这块没有银弹,适合的才是最好的,总要取舍,要么空间换时间,要么就好好考虑算法优化吧。

引用

Lua脚本语法

0 人点赞