成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

Redis 浮點數累計實現

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

Redis 浮點數累計主要是有兩個命令

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

在內部 HINCRBYFLOAT 和 INCRBYFLOAT 自增實現相同。所以我們分析 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 可以表示小數位 17 位。比如按照 jedis 的 api 來說,我們能夠使用的就是在 double 的精度范圍內,也就是 15-16位。這里我也看了 redis 的源碼,他在底層實現是通過 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 位操作系統上占通常占用 16 字節(128 位),相較于 8 字節的 double 類型具有更大的范圍和更高的精度。(這部分來源于 chatgpt) 因為 redis 采用的 long double 類型來做浮點數計算, 所以 redis 就可以保證到小數點后 17 位的精度。 整數位也可以表示 17 位 redis 的浮點數計算通常情況下會丟失精度嗎? 通常情況下是不會的,但是不能保證一定不會。

浮點數范圍測試

測試代碼如下:

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);

        //測試正浮點數最大值
        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("累計結果正確, 整數位: " + 17 + "位, 結果期望值: decimalIncr " + decimalIncr.toPlainString() + ", 目標值(redis):" + redisIncr.toPlainString());
        } else {
            System.out.println("累計結果不正確, 整數位: " + 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("累計結果正確, 整數位: " + i + "位, 結果期望值: decimalIncr " + decimalIncr.toPlainString() + ", 目標值(redis):" + redisIncr.toPlainString());
            } else {
                System.out.println("累計結果不正確, 整數位: " + 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;
    }
}

輸出結果:

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

INCRBYFLOAT 導致精度丟失

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

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

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

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

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

C 語言長雙精度類型

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

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

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

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

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

問題總結

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

參考文檔

  • 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。
責任編輯:姜華 來源: 運維開發故事
相關推薦

2020-09-15 12:57:46

C 語言浮點數內存

2017-10-16 10:42:27

前端JavaScript浮點數

2024-05-31 08:38:35

Python浮點數屬性

2015-12-02 10:21:34

JavaScript浮點數精度調整

2018-08-24 10:16:23

內存浮點數存儲

2020-10-12 06:38:08

存儲定點數

2011-05-25 14:10:38

浮點數

2021-10-19 14:04:28

C++類型數字

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

無損數據壓縮鴻蒙

2025-04-01 07:50:00

Dinero.js前端開發

2024-07-11 15:50:36

2024-08-23 08:43:08

2025-03-03 04:20:00

2023-11-08 13:32:00

JavaScript浮點數計算

2025-01-17 09:20:00

2025-03-14 10:34:22

點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 亚洲风情在线观看 | 亚洲一视频 | 亚洲天堂中文字幕 | 亚洲精品白浆高清久久久久久 | 精国产品一区二区三区四季综 | 国产精品国产三级国产aⅴ中文 | 超碰人人爱 | 午夜精品久久久久久久久久久久 | 亚洲精品一区二区三区中文字幕 | 精品一区电影 | 日韩亚洲一区二区 | 日本精品在线观看 | 日本亚洲精品成人欧美一区 | 99久久影院 | 性高湖久久久久久久久3小时 | 欧一区二区 | 国产精品自产av一区二区三区 | 一区二区三区不卡视频 | 九九热免费看 | 日韩毛片在线观看 | 国产一区二区精品 | 一区二区在线 | 日韩欧美不卡 | 日韩精品一 | 在线视频一区二区 | 免费三级网站 | 中文字幕在线视频一区二区三区 | 亚洲精品国产区 | 九九热这里只有精品6 | 欧美精品在线免费观看 | 日韩精品中文字幕一区二区三区 | 日本精品裸体写真集在线观看 | 99精品欧美一区二区蜜桃免费 | 一区二区三区四区视频 | 精品视频一区二区 | 国产农村一级国产农村 | 91精品国产一区二区三区 | 91av在线免费播放 | 国产精品欧美一区二区三区不卡 | 国产精品一区二区欧美 | 国产精品久久久久久久7777 |