Parte de Limitador de Tasa Distribuido
Contabilidad de ventana deslizante con sorted sets de Redis
Reemplacé contadores por cubeta con una ventana en sorted sets de Redis para que cada decisión recorte hits expirados, cuente el rango vivo y registre la solicitud actual en un pipeline transaccional.
La primera versión usaba un contador fijo por identidad y cubeta de tiempo. Era barata, pero el borde de la ventana se comportaba mal: un cliente podía gastar una cubeta completa cerca del final de un minuto y otra cubeta completa al inicio del siguiente. El límite configurado seguía viéndose correcto en el almacén, pero la tasa efectiva en el borde era casi el doble. Eso no servía como control de admisión delante de workers con cola finita.
La reescritura guarda cada solicitud admitida como un miembro en un sorted set de Redis. El score es UnixMilli, el miembro es UnixNano más un sufijo de proceso para evitar colisiones entre solicitudes que caen en el mismo milisegundo, y el TTL de la clave es la duración de la ventana. Cada decisión elimina scores expirados, lee la cardinalidad restante, escribe el hit candidato y fija el TTL mediante un pipeline transaccional. Si el conteo ya está en el límite, el miembro candidato se elimina después del pipeline y la solicitud se rechaza.
func allowRedis(ctx context.Context, rdb *redis.Client, key string, limit int, window time.Duration) (bool, error) {
now := time.Now()
cutoff := now.Add(-window).UnixMilli()
member := fmt.Sprintf("%d:%s", now.UnixNano(), processID)
pipe := rdb.TxPipeline()
pipe.ZRemRangeByScore(ctx, key, "0", strconv.FormatInt(cutoff, 10))
count := pipe.ZCard(ctx, key)
pipe.ZAdd(ctx, key, redis.Z{Score: float64(now.UnixMilli()), Member: member})
pipe.Expire(ctx, key, window)
if _, err := pipe.Exec(ctx); err != nil {
return false, err
}
if count.Val() >= int64(limit) {
_ = rdb.ZRem(ctx, key, member).Err()
return false, nil
}
return true, nil
}
Esto cambió el costo de almacenamiento de un entero por cubeta a un miembro por solicitud aceptada dentro de la ventana activa. El costo queda acotado por limit * keys más candidatos rechazados de vida corta, y el TTL elimina identidades inactivas sin un worker de limpieza. La ganancia es que cada instancia evalúa la misma ventana móvil en vez de estimarla desde cubetas locales.