Java 对于同时进行的事务,是否有每个请求的限制?

Java 对于同时进行的事务,是否有每个请求的限制?,java,google-app-engine,transactions,google-cloud-datastore,Java,Google App Engine,Transactions,Google Cloud Datastore,我在应用程序中使用了很多切分计数器。根据我目前的设计,一个请求可以导致100-200个不同的计数器增加 因此,对于每个计数器,我将拾取一个值递增的碎片。我在一个事务中增加每个分片,这意味着作为处理单个请求的一部分,我将完成100-200个事务。当然,我打算异步完成这项工作,这样我就可以并行运行所有100-200个事务 由于这个数字感觉相当高,我想知道对于同时事务或数据存储请求的数量,是否有一些每个请求或每个实例的限制。我无法从文档中找到有关此的信息 顺便说一句,由于某种原因,谷歌的文档指出,如果

我在应用程序中使用了很多切分计数器。根据我目前的设计,一个请求可以导致100-200个不同的计数器增加

因此,对于每个计数器,我将拾取一个值递增的碎片。我在一个事务中增加每个分片,这意味着作为处理单个请求的一部分,我将完成100-200个事务。当然,我打算异步完成这项工作,这样我就可以并行运行所有100-200个事务

由于这个数字感觉相当高,我想知道对于同时事务或数据存储请求的数量,是否有一些每个请求或每个实例的限制。我无法从文档中找到有关此的信息

顺便说一句,由于某种原因,谷歌的文档指出,如果你的应用程序有频繁更新的计数器,你不应该以事务方式增加它们[1],但另一方面,他们关于切分计数器的代码示例使用事务来增加切分[2]。我认为只要使用足够的碎片,我就可以使用事务。我更喜欢交易,因为我希望我的计数器不会错过增量


我发现您正在使用切分计数器方法来避免争用,如中所述:

您能否将所有计数器收集到单个实体中,以便每个碎片都是一组计数器?那么你就不需要那么多单独的交易了。根据,一个实体最大可以是1MB,当然200个整数可以很好地适应这个大小限制

可能是因为您事先不知道酒店的名称。下面是一种在Go中表示的方法,使用其PropertyLoadSaver接口可以处理动态计数器名称

const (
    counterPrefix = "COUNTER:"
)

type shard struct {
    // We manage the saving and loading of counters explicitly.
    counters map[string]int64 `datastore:"-"`
}

// NewShard construct a new shard.
func NewShard() *shard {
    return &shard{make(map[string]int64)}
}

// Save implements PropertyLoadSaver.
func (s *shard) Save(c chan<- datastore.Property) error {
    defer close(c)
    for key, value := range s.counters {
        c <- datastore.Property{
            Name:  counterPrefix + key,
            Value: value,
            NoIndex: true,
        }
    }
    return nil
}

// Load implements PropertyLoadSaver.
func (s *shard) Load(c <-chan datastore.Property) error {
    s.counters = make(map[string]int64)
    for prop := range c {
        if strings.HasPrefix(prop.Name, counterPrefix) {
            s.counters[prop.Name[len(counterPrefix):]] = prop.Value.(int64)
        }
    }
    return nil
}
我们将其签名更改为面向批量的操作:

// // Increment increments the named counters.
func Increment(c appengine.Context, names []string) error {
    ...
}
实现将找到一个碎片,为我们想要增加的每个计数器调用增量,并将单个实体保存到数据存储中,所有这些都在一个事务中。查询还包括查询所有碎片。。。但是阅读速度很快。我们仍然维护切分体系结构以避免写争用

Go的完整示例代码如下:

package sharded_counter

import (
    "fmt"
    "math/rand"
    "strings"

    "appengine"
    "appengine/datastore"
)

const (
    numShards     = 20
    shardKind     = "CounterShard"
    counterPrefix = "counter:"
)

type shard struct {
    // We manage the saving and loading of counters explicitly.
    counters map[string]int64 `datastore:"-"`
}

// NewShard constructs a new shard.
func NewShard() *shard {
    return &shard{make(map[string]int64)}
}

// Returns a list of the names stored in the shard.
func (s *shard) Names() []string {
    names := make([]string, 0, len(s.counters))
    for name, _ := range s.counters {
        names = append(names, name)
    }
    return names
}

// Lookup finds the counter's value.
func (s *shard) Lookup(name string) int64 {
    return s.counters[name]
}

// Increment adds to the counter's value.
func (s *shard) Increment(name string) {
    s.counters[name]++
}

// Save implements PropertyLoadSaver.
func (s *shard) Save(c chan<- datastore.Property) error {
    for key, value := range s.counters {
        c <- datastore.Property{
            Name:    counterPrefix + key,
            Value:   value,
            NoIndex: true,
        }
    }
    close(c)
    return nil
}

// Load implements PropertyLoadSaver.
func (s *shard) Load(c <-chan datastore.Property) error {
    s.counters = make(map[string]int64)
    for prop := range c {
        if strings.HasPrefix(prop.Name, counterPrefix) {
            s.counters[prop.Name[len(counterPrefix):]] = prop.Value.(int64)
        }
    }
    return nil
}

// AllCounters returns all counters.
func AllCounters(c appengine.Context) (map[string]int64, error) {
    var results map[string]int64
    results = make(map[string]int64)
    q := datastore.NewQuery(shardKind)
    q = q.Ancestor(ancestorKey(c))
    for t := q.Run(c); ; {
        var s shard
        _, err := t.Next(&s)
        if err == datastore.Done {
            break
        }
        if err != nil {
            return results, err
        }
        for _, name := range s.Names() {
            results[name] += s.Lookup(name)
        }
    }
    return results, nil
}

// ancestorKey returns an key that all counter shards inherit.
func ancestorKey(c appengine.Context) *datastore.Key {
    return datastore.NewKey(c, "CountersAncestor", "CountersAncestor", 0, nil)
}

// Increment increments the named counters.
func Increment(c appengine.Context, names []string) error {
    shardName := fmt.Sprintf("shard%d", rand.Intn(numShards))
    err := datastore.RunInTransaction(c, func(c appengine.Context) error {
        key := datastore.NewKey(c, shardKind, shardName, 0, ancestorKey(c))
        s := NewShard()
        err := datastore.Get(c, key, s)
        // A missing entity and a present entity will both work.
        if err != nil && err != datastore.ErrNoSuchEntity {
            return err
        }
        for _, name := range names {
            s.Increment(name)
        }
        _, err = datastore.Put(c, key, s)
        return err
    }, nil)
    return err
}

如果仔细观察,它几乎就是一个未命名计数器的示例,但扩展为处理多个计数器名称。我在查询端做了一些更改,以便读取使用相同的祖先密钥,因此我们位于同一个实体组中。

这里有三个限制可能会导致您出现问题:

每个实体组的1/s写入限制 每个XG 5个实体组 每个实例10个并发“线程” 最后一个是针对您的用例的棘手问题

它有点难找到信息,事实上可能是过时的信息-所以它值得测试,但每个实例只允许10个并发核心线程,而不管大小-F1/F2/F

也就是说,忽略后台线程的创建,如果假设每个请求都有一个线程,就像每个RPC数据存储、memcache、文本搜索等一样,那么一次只能使用10个线程。如果调度程序认为传入请求将超过10,它将把请求路由到新实例

在一个您希望并行写入100个实体的场景中,我希望它只允许大约10次并发写入其余的阻塞,但是您的实例一次只能为一个请求提供服务

您的备选方案:

使用专用memcache—您需要将计数器备份到持久存储上,但您可以在后端分批执行此操作。这可能会导致由于刷新而丢失一些数据,不管这是否正确,您都必须做出决定 使用CloudSQL序列或表-如果不需要很大的规模, 但是确实需要很多计数器,这可能是一个很好的方法-你 可以将计数存储为原始计数,或存储为timeseries数据和 精确计数的后处理 使用拉队列在后端批量更新计数器。您可以在大量计数器表中处理许多“事件”。缺点是,在任何给定的时间点,计数都不会是最新的 最好的方法可能是混合

例如,接受计数的某些最终一致性:

当请求传入时-memcache中计数器的原子增量 当请求进入队列时,将执行“事件”任务 从memcache提供所需计数-如果不存在,则从数据存储加载 在memcache上使用TTL,以便最终将数据存储视为“真相的来源” 运行cron,每隔5分钟或视情况从队列中提取100个“事件”任务,并更新数据存储中事务中所有事件的计数器 更新:我在文档中找到了这一部分,它对

如果此设置太高,则可能会增加API延迟


我认为它值得一玩。

谢谢您的回复!我想我现在有了answ 我需要一个急救箱

关于每个请求或每个实例的限制 并发线程有每个实例的限制,这有效地限制了并发事务的数量。默认限制为10。它可以增加,但不清楚会有什么副作用

关于根本问题 我选择将计数器分组,这样通常一起递增的计数器就在同一组中。碎片携带与单个碎片关联的组内所有计数器的部分计数

计数在事务中仍会增加,但由于分组,每个请求最多只需要五个事务。每个事务都会增加存储在单个分片(表示为单个数据存储实体)中的大量部分计数

即使事务是串联运行的,处理请求的时间仍然可以接受。每个计数器组有几百个计数器。我确保有足够的碎片来避免争用


需要注意的是,此解决方案之所以可行,是因为计数器可以分成相当大的计数器组,这些计数器组通常一起递增。

感谢您的回复。这种方法也出现在我的脑海中,我还在考虑。然而,有一个更大的计数器池,从中拾取要递增的100-200个计数器。我需要将计数器分组到通常同时递增的类别中。虽然这会使代码复杂化,但现在开始觉得这是正确的方法;我认为你可能需要重新考虑最初的方法。我不认为我们引用的切分文章中的设计是为了批量更新大量计数器。即使使用并行性,每个请求100-200次单独写入听起来也很昂贵,而且充满了写入争用。当然,我的直觉可能让我误入歧途。分析可能会有所帮助。重申一下:我认为这里的关键是批量处理与数据存储的交互。我的答案中的设计尝试将计数器批处理为单个实体。但是,通过批量检索所涉及的所有碎片,对其进行变异,然后在一个事务中批量保存所有碎片,您可能会得到类似的结果。使用Go-on应用程序引擎对话查看高性能应用程序:;该演讲中的批处理概念应该适用于所有语言。为了正确地分析RPC,您可以查看Appstats:dyoo,在事务中执行批处理获取和保存需要我将不同计数器的碎片放在同一个实体组中,确切地说是其中的一些。从这个意义上讲,它与前面讨论的解决方案非常相似。无论如何,我现在已经实现了前面讨论的解决方案。不过,我不认为写争用会是单独写的问题。谢谢。我没有意识到对每个实例并发请求的限制实际上也是对每个实例并发线程的限制。这似乎在任何地方都没有明确的记录,但情况似乎仍然如此。
package sharded_counter

import (
    "fmt"
    "math/rand"
    "strings"

    "appengine"
    "appengine/datastore"
)

const (
    numShards     = 20
    shardKind     = "CounterShard"
    counterPrefix = "counter:"
)

type shard struct {
    // We manage the saving and loading of counters explicitly.
    counters map[string]int64 `datastore:"-"`
}

// NewShard constructs a new shard.
func NewShard() *shard {
    return &shard{make(map[string]int64)}
}

// Returns a list of the names stored in the shard.
func (s *shard) Names() []string {
    names := make([]string, 0, len(s.counters))
    for name, _ := range s.counters {
        names = append(names, name)
    }
    return names
}

// Lookup finds the counter's value.
func (s *shard) Lookup(name string) int64 {
    return s.counters[name]
}

// Increment adds to the counter's value.
func (s *shard) Increment(name string) {
    s.counters[name]++
}

// Save implements PropertyLoadSaver.
func (s *shard) Save(c chan<- datastore.Property) error {
    for key, value := range s.counters {
        c <- datastore.Property{
            Name:    counterPrefix + key,
            Value:   value,
            NoIndex: true,
        }
    }
    close(c)
    return nil
}

// Load implements PropertyLoadSaver.
func (s *shard) Load(c <-chan datastore.Property) error {
    s.counters = make(map[string]int64)
    for prop := range c {
        if strings.HasPrefix(prop.Name, counterPrefix) {
            s.counters[prop.Name[len(counterPrefix):]] = prop.Value.(int64)
        }
    }
    return nil
}

// AllCounters returns all counters.
func AllCounters(c appengine.Context) (map[string]int64, error) {
    var results map[string]int64
    results = make(map[string]int64)
    q := datastore.NewQuery(shardKind)
    q = q.Ancestor(ancestorKey(c))
    for t := q.Run(c); ; {
        var s shard
        _, err := t.Next(&s)
        if err == datastore.Done {
            break
        }
        if err != nil {
            return results, err
        }
        for _, name := range s.Names() {
            results[name] += s.Lookup(name)
        }
    }
    return results, nil
}

// ancestorKey returns an key that all counter shards inherit.
func ancestorKey(c appengine.Context) *datastore.Key {
    return datastore.NewKey(c, "CountersAncestor", "CountersAncestor", 0, nil)
}

// Increment increments the named counters.
func Increment(c appengine.Context, names []string) error {
    shardName := fmt.Sprintf("shard%d", rand.Intn(numShards))
    err := datastore.RunInTransaction(c, func(c appengine.Context) error {
        key := datastore.NewKey(c, shardKind, shardName, 0, ancestorKey(c))
        s := NewShard()
        err := datastore.Get(c, key, s)
        // A missing entity and a present entity will both work.
        if err != nil && err != datastore.ErrNoSuchEntity {
            return err
        }
        for _, name := range names {
            s.Increment(name)
        }
        _, err = datastore.Put(c, key, s)
        return err
    }, nil)
    return err
}