自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Redis 浮點數(shù)累計實現(xiàn)

數(shù)據(jù)庫 Redis
按照官方文檔的說法 INCRBYFLOAT 可以表示小數(shù)位 17 位。比如按照 jedis 的 api 來說,我們能夠使用的就是在 double 的精度范圍內(nèi),也就是 15-16位。這里我也看了 redis 的源碼,他在底層實現(xiàn)是通過 c 語言的 long double 類型來進行計算的。

Redis 浮點數(shù)累計主要是有兩個命令

  • INCRBYFLOAT 是 SET 指令的浮點數(shù)累計
  • HINCRBYFLOAT 是 HASH 類型的浮點數(shù)累計

在內(nèi)部 HINCRBYFLOAT 和 INCRBYFLOAT 自增實現(xiàn)相同。所以我們分析 INCRBYFLOAT 即可。

基本使用

直接使用指令。

INCRBYFLOAT mykey 0.1
INCRBYFLOAT mykey 1.111
INCRBYFLOAT mykey 1.111111

使用 lua 腳本的方式,因為 redis 可以通過 lua 腳本來保證操作的原子性,所以當我們同時操作多個 key 的時候一般使用 lua 腳本的方式。

eval "return redis.call('INCRBYFLOAT', KEYS[1], ARGV[1])" 1 mykey1 "1.11" 
eval "return redis.call('INCRBYFLOAT', KEYS[1], ARGV[1])" 1 mykey1 "1.11111" 
eval "return redis.call('INCRBYFLOAT', KEYS[1], ARGV[1])" 1 mykey1 "1.11111"

INCRBYFLOAT 可表示范圍

按照官方文檔的說法 INCRBYFLOAT 可以表示小數(shù)位 17 位。比如按照 jedis 的 api 來說,我們能夠使用的就是在 double 的精度范圍內(nèi),也就是 15-16位。這里我也看了 redis 的源碼,他在底層實現(xiàn)是通過 c 語言的 long double 類型來進行計算的。

void incrbyfloatCommand(client *c) {
    long double incr, value;
    robj *o, *new;

    o = lookupKeyWrite(c->db,c->argv[1]);
    if (checkType(c,o,OBJ_STRING)) return;
    if (getLongDoubleFromObjectOrReply(c,o,&value,NULL) != C_OK ||
        getLongDoubleFromObjectOrReply(c,c->argv[2],&incr,NULL) != C_OK)
        return;

    value += incr;
    if (isnan(value) || isinf(value)) {
        addReplyError(c,"increment would produce NaN or Infinity");
        return;
    }
    new = createStringObjectFromLongDouble(value,1);
    if (o)
        dbReplaceValue(c->db,c->argv[1],new);
    else
        dbAdd(c->db,c->argv[1],new);
    signalModifiedKey(c,c->db,c->argv[1]);
    notifyKeyspaceEvent(NOTIFY_STRING,"incrbyfloat",c->argv[1],c->db->id);
    server.dirty++;
    addReplyBulk(c,new);

    /* Always replicate INCRBYFLOAT as a SET command with the final value
     * in order to make sure that differences in float precision or formatting
     * will not create differences in replicas or after an AOF restart. */
    rewriteClientCommandArgument(c,0,shared.set);
    rewriteClientCommandArgument(c,2,new);
    rewriteClientCommandArgument(c,3,shared.keepttl);
}

源碼地址:https://github.com/redis/redis/blob/unstable/src/t_string.c long double 是 c 語言的長雙精度浮點型,在 x86 的 64 位操作系統(tǒng)上占通常占用 16 字節(jié)(128 位),相較于 8 字節(jié)的 double 類型具有更大的范圍和更高的精度。(這部分來源于 chatgpt) 因為 redis 采用的 long double 類型來做浮點數(shù)計算, 所以 redis 就可以保證到小數(shù)點后 17 位的精度。 整數(shù)位也可以表示 17 位 redis 的浮點數(shù)計算通常情況下會丟失精度嗎? 通常情況下是不會的,但是不能保證一定不會。

浮點數(shù)范圍測試

測試代碼如下:

public class RedisIncrByFloatTest {

    public static void main(String[] args) {
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        BigDecimal decimalIncr = java.math.BigDecimal.ZERO;
        String key = "IncrFloat:Digit100";


        //測試精度
        test_accuracy(jedis, decimalIncr, key);

        //測試正浮點數(shù)最大值
        test_max_positive_float(jedis, decimalIncr, key);

        jedis.disconnect();
        jedis.close();
    }

    private static void test_max_positive_float(Jedis jedis, BigDecimal decimalIncr, String key) {
        jedis.del(key);
        String value = "99999999999999999.00000000000000003";
        List<String> evalKeys = Collections.singletonList(key);
        List<String> evalArgs = Collections.singletonList(value);
        String luaStr = "redis.call('INCRBYFLOAT', KEYS[1], ARGV[1]) return redis.call('GET', KEYS[1])";
        Object result = jedis.eval(luaStr, evalKeys, evalArgs);
        decimalIncr = decimalIncr.add(new BigDecimal(value));
        BigDecimal redisIncr = new BigDecimal(String.valueOf(result));

        value = "0.99999999999999996";
        evalKeys = Collections.singletonList(key);
        evalArgs = Collections.singletonList(value);
        luaStr = "redis.call('INCRBYFLOAT', KEYS[1], ARGV[1]) return redis.call('GET', KEYS[1])";
        result = jedis.eval(luaStr, evalKeys, evalArgs);
        decimalIncr = decimalIncr.add(new BigDecimal(value));
        redisIncr = new BigDecimal(String.valueOf(result));


        boolean eq = comparteNumber(redisIncr, decimalIncr);
        if (eq) {
            System.out.println("累計結果正確, 整數(shù)位: " + 17 + "位, 結果期望值: decimalIncr " + decimalIncr.toPlainString() + ", 目標值(redis):" + redisIncr.toPlainString());
        } else {
            System.out.println("累計結果不正確, 整數(shù)位: " + 17 + "位, 期望值: decimalIncr " + decimalIncr.toPlainString() + ", 目標值(redis):" + redisIncr.toPlainString());
        }
    }

    private static void test_accuracy(Jedis jedis, BigDecimal decimalIncr, String key) {
        jedis.del(key);
        for (int i = 16; i < 30; i++) {
            String value = createValue(i);
            final List<String> evalKeys = Collections.singletonList(key);
            final List<String> evalArgs = Collections.singletonList(value);
            String luaStr = "redis.call('INCRBYFLOAT', KEYS[1], ARGV[1]) return redis.call('GET', KEYS[1])";

            Object result = jedis.eval(luaStr, evalKeys, evalArgs);
            decimalIncr = decimalIncr.add(new BigDecimal(value));
            BigDecimal redisIncr = new BigDecimal(String.valueOf(result));
            boolean eq = comparteNumber(redisIncr, decimalIncr);
            if (eq) {
                System.out.println("累計結果正確, 整數(shù)位: " + i + "位, 結果期望值: decimalIncr " + decimalIncr.toPlainString() + ", 目標值(redis):" + redisIncr.toPlainString());
            } else {
                System.out.println("累計結果不正確, 整數(shù)位: " + i + "位, 期望值: decimalIncr " + decimalIncr.toPlainString() + ", 目標值(redis):" + redisIncr.toPlainString());
                break;
            }
        }
    }

    private static String createValue(int i) {
        String result = "9" + "0".repeat(Math.max(0, i - 1));
        return result + ".00000000000000003";
    }

    private static boolean comparteNumber(BigDecimal redisIncr, BigDecimal decimalIncr) {
        return decimalIncr.compareTo(redisIncr) == 0;
    }
}

輸出結果:

累計結果正確, 整數(shù)位: 16位, 結果期望值: decimalIncr 9000000000000000.00000000000000003, 目標值(redis):9000000000000000.00000000000000003
累計結果正確, 整數(shù)位: 17位, 結果期望值: decimalIncr 99000000000000000.00000000000000006, 目標值(redis):99000000000000000.00000000000000006
累計結果不正確, 整數(shù)位: 18位, 期望值: decimalIncr 999000000000000000.00000000000000009, 目標值(redis):999000000000000000
累計結果正確, 整數(shù)位: 17位, 結果期望值: decimalIncr 99999999999999999.99999999999999999, 目標值(redis):99999999999999999.99999999999999999

INCRBYFLOAT 導致精度丟失

INCRBYFLOAT 導致精度丟失有兩種情況:

  1. 累計的范圍值超過 INCRBYFLOAT 所能表示的最大精度范圍,在 double 范圍內(nèi)。

INCRBYFLOAT 底層計算是通過long double 來計算的在 C語言中 long double占用128 位,其范圍為: 最小值: ±5.4×10^-4951 最大值: ±1.1×10^4932 能表示的有效數(shù)字在34~35位之間。

  1. 我們使用類似 jedis 的 api 提供的是 double 類型的參數(shù),可能在調(diào)用之前,參數(shù)轉(zhuǎn)換的過程就發(fā)生了精度問題。比如
StringRedisTemplate template = new StringRedisTemplate();        
template.opsForValue().increment("v1", 1.3D);

在 RedisTemplate 的這個 increment 接受的參數(shù)類型就是一個 double 所以會發(fā)生精度問題

C 語言長雙精度類型

因為 redis 底層采用的是long double 計算,所以這個問題轉(zhuǎn)化為長雙精度(long double)為什么沒有精度問題? 這是因為 long double 具有更大的范圍和更高的精度。long double 的范圍和精度高于 double 類型:

  • 范圍更大:long double 可以表示更大和更小的數(shù)字
  • 精度更高:long double 可以表示的有效數(shù)字多于 double 類型這意味著,對于同樣的浮點計算,long double 具有更少的舍入誤差。

具體來說,幾點原因造成 long double 沒有精度問題:

  1. long double 使用更多的bit位來表示浮點數(shù)。
  2. long double 使用四舍五入(rounding to nearest)而不是銀行家舍入(bankers' rounding),導致更少的誤差累加。
  3. 許多編譯器及 CPU 針對 long double 具有優(yōu)化, 會生成精度更高的機器碼來執(zhí)行 long double 計算。
  4. long double 內(nèi)部采用更大的指數(shù)域, 能更準確地表示相同范圍內(nèi)的數(shù)字。

綜上,long double 的更廣范圍和更高精度,讓它在相同的浮點計算中具有更少的舍入誤差。這也就解釋了為什么 long double 沒有明顯的精度問題,因為它天生就是為了提供更高精度而設計的。相比之下,double 使用的位數(shù)相對有限,即使采用折中舍入法,在一些場景下它的誤差也可能累加顯著。所以總的來說,long double 之所以沒有精度問題,主要還是源于其更大的范圍和更高的內(nèi)在精度。

問題總結

  1. Redis 浮點數(shù)累計操作 INCRBYFLOAT 不適合精度要求比較高的金額計算。
  2. Redis 浮點數(shù)累計操作 INCRBYFLOAT 也不能平替 BigDecimal 計算,如果一定需要存儲可以考慮通過 lua 腳本實現(xiàn) CAS 進行修改,最終存儲為 String 類型的一個結果。
  3. Redis 的浮點數(shù)雖然做了比較好的優(yōu)化,但是沒有從根本解決計算精度問題。

參考文檔

  • https://redis.io/commands/incrbyfloat/。
  • https://wiki.c2.com/?BankersRounding。
  • https://www.wikihow.com/Round-to-the-Nearest-Tenth。
  • https://learn.microsoft.com/zh-cn/cpp/c-language/type-long-double?view=msvc-170。
  • https://learn.microsoft.com/zh-cn/cpp/c-runtime-library/reference/strtold-strtold-l-wcstold-wcstold-l?view=msvc-170。
責任編輯:姜華 來源: 運維開發(fā)故事
相關推薦

2020-09-15 12:57:46

C 語言浮點數(shù)內(nèi)存

2017-10-16 10:42:27

前端JavaScript浮點數(shù)

2024-05-31 08:38:35

Python浮點數(shù)屬性

2015-12-02 10:21:34

JavaScript浮點數(shù)精度調(diào)整

2018-08-24 10:16:23

內(nèi)存浮點數(shù)存儲

2011-05-25 14:10:38

浮點數(shù)

2020-10-12 06:38:08

存儲定點數(shù)

2021-10-19 14:04:28

C++類型數(shù)字

2009-05-19 10:10:01

Oracle時間加減時間操作

2010-07-22 17:39:44

2010-01-15 15:21:35

C++

2021-11-15 09:32:06

浮點面試Java

2022-06-15 15:44:21

無損數(shù)據(jù)壓縮鴻蒙

2025-04-01 07:50:00

Dinero.js前端開發(fā)

2024-07-11 15:50:36

2024-08-23 08:43:08

2025-01-17 09:20:00

2025-03-03 04:20:00

2023-11-08 13:32:00

JavaScript浮點數(shù)計算

2025-03-14 10:34:22

點贊
收藏

51CTO技術棧公眾號