首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >在web应用程序中进行A/B测试的库

在web应用程序中进行A/B测试的库
EN

Code Review用户
提问于 2012-10-04 21:08:31
回答 1查看 651关注 0票数 5

我制作了一个简单的库来帮助我在web应用程序中进行A/B测试。这个想法很简单:对于给定的页面,我有两个或更多的页面选项( URL ),每次对库方法的调用都应该给我一个URL,所以在最后,所有选项都被赋予相同的流量。

我的问题是:

  • 这100%线程安全吗?
  • 这是否具有表现性(我担心多线程(web)环境中的锁)?
  • 我用了最好的数据结构吗?
  • 它能更易读吗?

下面是测试和实现:

代码语言:javascript
复制
[TestFixture]
public class ABTestTest    
{
    [SetUp]
    public void Setup()
    {
        ABTest.ResetAll();
    }

    [Test]
    public void GetUrlWithTwoCandidates()
    {
        ABTest.RegisterUrl("CarrinhoPagamento", "Url1.aspx");
        ABTest.RegisterUrl("CarrinhoPagamento", "Url2.aspx");

        ABTest.GetUrl("CarrinhoPagamento").Should().Be.EqualTo("Url1.aspx");
        ABTest.GetUrl("CarrinhoPagamento").Should().Be.EqualTo("Url2.aspx");
        ABTest.GetUrl("CarrinhoPagamento").Should().Be.EqualTo("Url1.aspx");
        ABTest.GetUrl("CarrinhoPagamento").Should().Be.EqualTo("Url2.aspx");
        ABTest.GetUrl("CarrinhoPagamento").Should().Be.EqualTo("Url1.aspx");
        ABTest.GetUrl("CarrinhoPagamento").Should().Be.EqualTo("Url2.aspx");
        ABTest.GetUrl("CarrinhoPagamento").Should().Be.EqualTo("Url1.aspx");
    }

    [Test]
    public void GetUrlWithThreeCandidates()
    {
        var OptionsSelected = new Dictionary<string, int>();
        OptionsSelected.Add("Url1.aspx", 0);
        OptionsSelected.Add("Url2.aspx", 0);
        OptionsSelected.Add("Url3.aspx", 0);

        ABTest.RegisterUrl("CarrinhoPagamento", "Url1.aspx");
        ABTest.RegisterUrl("CarrinhoPagamento", "Url2.aspx");
        ABTest.RegisterUrl("CarrinhoPagamento", "Url3.aspx");

        var nextUrl = "";
        for (int i = 1; i < 10; i++)
        {
            nextUrl = ABTest.GetUrl("CarrinhoPagamento");

            OptionsSelected[nextUrl]++;
        }

        OptionsSelected["Url1.aspx"].Should().Be.EqualTo(3);
        OptionsSelected["Url2.aspx"].Should().Be.EqualTo(3);
        OptionsSelected["Url3.aspx"].Should().Be.EqualTo(3);
    }

    [Test]
    public void GetUrlWithThreeCandidatesThreaded()
    {
        var OptionsSelected = new Dictionary<string, int>();
        OptionsSelected.Add("Url1.aspx", 0);
        OptionsSelected.Add("Url2.aspx", 0);
        OptionsSelected.Add("Url3.aspx", 0);

        ABTest.RegisterUrl("CarrinhoPagamento", "Url1.aspx");
        ABTest.RegisterUrl("CarrinhoPagamento", "Url2.aspx");
        ABTest.RegisterUrl("CarrinhoPagamento", "Url3.aspx");

        ThreadPool.SetMaxThreads(3, 3);

        for (int i = 1; i < 10; i++)
        {
            ThreadPool.QueueUserWorkItem((object state) =>
            {
                var nextUrl = ABTest.GetUrl("CarrinhoPagamento");

                OptionsSelected[nextUrl]++;
            });                
        }

        while (OptionsSelected.Select(x => x.Value).Sum() != 9)
        {
            Thread.Sleep(100);
        }

        OptionsSelected["Url1.aspx"].Should().Be.EqualTo(3);
        OptionsSelected["Url2.aspx"].Should().Be.EqualTo(3);
        OptionsSelected["Url3.aspx"].Should().Be.EqualTo(3);
    }
}

public class ABTest
{
    private volatile static Hashtable NextOption = new Hashtable();
    private static Hashtable Options = new Hashtable();

    public static void ResetAll()
    {
        lock (NextOption)
        {
            NextOption = new Hashtable();
        }
        lock (Options)
        {
            Options = new Hashtable();
        }
    }

    public static void RegisterUrl(string key, string url)
    {
        if (Options.ContainsKey(key.GetHashCode()))
        {
            lock (Options)
            {
                ((List<ABTestOption>)Options[key.GetHashCode()]).Add(new ABTestOption()
                {
                    Url = url,
                    Count = 0
                });
            }
        }
        else
        {

            if (!Options.ContainsKey(key.GetHashCode()))
            {
                lock (Options)
                {
                    if (!Options.ContainsKey(key.GetHashCode()))
                    {
                        Options.Add(
                            key.GetHashCode(),
                            new List<ABTestOption>() { 
                            new ABTestOption() { 
                                Url = url, 
                                Count = 0 
                            } 
                        });
                    }
                }
            }


            if (!NextOption.ContainsKey(key.GetHashCode()))
            {
                lock (NextOption)
                {
                    if (!NextOption.ContainsKey(key.GetHashCode()))
                    {
                        NextOption.Add(key.GetHashCode(), url);
                    }
                }
            }
        }

    }

    public static string GetUrl(string key)
    {
        lock (NextOption)
        {
            var nextUrl = (string)NextOption[key.GetHashCode()];

            var keyOptions = (List<ABTestOption>)Options[key.GetHashCode()];
            var selectedOption = keyOptions.Where(x => x.Url == nextUrl).First();
            selectedOption.Count++;

            NextOption.Remove(key.GetHashCode());
            NextOption.Add(key.GetHashCode(), keyOptions.OrderBy(x => x.Count).First().Url);

            return nextUrl;
        }
    }

    private class ABTestOption
    {
        public string Url { get; set; }
        public int Count { get; set; }
    }
}
EN

回答 1

Code Review用户

发布于 2012-10-05 18:54:56

这里有一种可能的方法,希望它易于阅读。

我相信一个比我聪明的人(也会)指出这个建议可能存在的任何问题。

options字段从不为null,而且该字段的set操作是原子的,因此我认为您不需要对其进行锁定。

代码语言:javascript
复制
public static class ABTest
{
    private static IDictionary<string, ABTestOption> options = new Dictionary<string, ABTestOption>();
    private readonly static object locker = new object();

    public static string GetUrl(string key)
    {
        ABTestOption option;

        lock (locker)
        {
            if (!options.TryGetValue(key, out option))
            {
                return null;
            }
        }

        return option.GetNextUrl();
    }

    public static void RegisterUrl(string key, string url)
    {
        ABTestOption option;

        lock (locker)
        {
            if (!options.TryGetValue(key, out option))
            {
                option = new ABTestOption();
                options[key] = option;
            }
        }

        option.AddUrl(url);
    }

    public static void ResetAll()
    {
        options = new Dictionary<string, ABTestOption>();
    }

    private class ABTestOption
    {
        private readonly object locker = new object();
        private readonly List<string> urls = new List<string>();
        private int totalCallCount = -1;

        public void AddUrl(string url)
        {
            lock (this.locker)
            {
                if (!this.urls.Contains(url))
                {
                    this.urls.Add(url);
                }
            }
        }

        internal string GetNextUrl()
        {
            lock (this.locker)
            {
                int urlCount = this.urls.Count;
                int position = Interlocked.Increment(ref this.totalCallCount) % urlCount;

                return this.urls[position];
            }
        }
    }
}

我无法构建您的线程池测试,因为您的for语句是不完整的,并且我不确定应该如何完成它们,所以我使用了这两个测试:

代码语言:javascript
复制
[TestFixture]
public class ABTestTest
{
    [Test]
    public void GetUrlWithTwoCandidates()
    {
        ABTest.RegisterUrl("CarrinhoPagamento", "Url1.aspx");
        ABTest.RegisterUrl("CarrinhoPagamento", "Url2.aspx");

        Assert.AreEqual("Url1.aspx", ABTest.GetUrl("CarrinhoPagamento"));
        Assert.AreEqual("Url2.aspx", ABTest.GetUrl("CarrinhoPagamento"));
        Assert.AreEqual("Url1.aspx", ABTest.GetUrl("CarrinhoPagamento"));
        Assert.AreEqual("Url2.aspx", ABTest.GetUrl("CarrinhoPagamento"));
    }

    [Test]
    public void GetUrlWithThreeCandidates()
    {
        ABTest.RegisterUrl("CarrinhoPagamento", "Url1.aspx");
        ABTest.RegisterUrl("CarrinhoPagamento", "Url2.aspx");
        ABTest.RegisterUrl("CarrinhoPagamento", "Url3.aspx");

        Assert.AreEqual("Url1.aspx", ABTest.GetUrl("CarrinhoPagamento"));
        Assert.AreEqual("Url2.aspx", ABTest.GetUrl("CarrinhoPagamento"));
        Assert.AreEqual("Url3.aspx", ABTest.GetUrl("CarrinhoPagamento"));
        Assert.AreEqual("Url1.aspx", ABTest.GetUrl("CarrinhoPagamento"));
        Assert.AreEqual("Url2.aspx", ABTest.GetUrl("CarrinhoPagamento"));
        Assert.AreEqual("Url3.aspx", ABTest.GetUrl("CarrinhoPagamento"));
    }

    [SetUp]
    public void Setup()
    {
        ABTest.ResetAll();
    }
}

编辑:更新后的反馈。

锁很简单,而且速度很快,您可以通过使用ReaderWriterLockSlim获得更大的吞吐量,但这要复杂得多,除非您能够证明锁导致了很大的阻塞,否则我一开始会保留它。

票数 0
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/16211

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档