宏光PLUS上市拉新活動-技術總結

簡介

彈彈車是爲五菱宏光PLUS上市而誕生的,其目的主要在於

  • 拉新
  • 提高用戶活躍度
  • 慶祝宏光plus上市,提高產品知名度

彈彈車遊戲方法

  • 彈車。按下力度條,車子彈射出去一段距離,然後停止。

彈車距離影響因素有:

  1. 力度大小。

    1.1 力度區間爲:(0,0.5] 和 (1,0.5],即中間(0.5)處,力度最大。

    1.2 同等條件下,力度越大,彈射距離越大。

  2. 是否具備助推劑。

    2.1 獲得方式:分享拉新成功則得一瓶氮氣

    2.2 同等條件下,有助推器可以彈車更遠的距離。

  3. 宏光plus座駕還是舊座駕。

    3.1 獲得方式:進行愛車評估

    3.2 宏光plus比舊座駕在同等條件下彈射更遠距離。

  • 爆旅途獎品。彈車到停止的過程會隨機獲取獎品。
  • 累計彈車裏程達到繞地球20圈,則贈與油卡券。

彈彈車涉及到的技術主要有

  • 彈射里程計算。
  • 路途如何爆出獎品。
  • 獎品庫存控制。

彈射里程計算

里程的計算會影響到油卡券的發放,故需要嚴格控制每次彈車的距離,做到

  1. 油卡券能發放完畢。即用戶在活動結束後有部分人達到目標里程(繞地球20圈);
  2. 拉新與否、愛車評估與否、彈射力度大小能通過彈車距離來明顯感知。例如,同等條件下,彈射力爲0.1的彈車距離理應小於彈射力爲0.5的彈車距離。
劃分區間

劃分區間可以規範好彈車距離,使之不重疊,即能讓用戶明顯感知條件的不同會影響彈車距離。

彈車距離影響因子:
1. 是否進行愛車評估,設代碼爲plus_car
2. 是否有助推器,設代碼爲props
3. 力度大小,設代碼爲pressure

eg.
plus_car  props pressure  distance
   1        1      1       (0.8,1]
   0        1      1       (0.8*0.9,1*0.9]即(0.72,0.9]
...

注:
力度轉成0-1的範圍:[2x , 2(1-x])
正態分佈

雖然劃分區間能夠明確知道每個用戶彈車的距離區間在哪裏,但是其具體值是隨機的。從自然界的一些現象,如

  • 某年某地區降雨量
  • 某年某高校大學生的身高
  • 測量誤差
  • 射擊目標的水平或者垂直偏差(跟彈車有點類似)

等等都近似或者服從正態分佈,雖然沒有進行實際進行彈車測試,我們可以做個取巧,即認爲彈車距離就是服從正態分佈(模擬也要近似於常識、自然現象)。

這裏,實現方式採用GB4086.1-83這個規範來實現正態分佈。其原理如下圖所示

正態分佈

Java實現正態分佈

其中k的值,我個人認爲,只要其滿足GB4086.1-83規範給出表格的得出即可。即將u=1.33帶入公式,能得出0.908241值即可。

 /**
     * 正態分佈,詳細見https://max.book118.com/html/2018/1213/6144221053001235.shtm
     * @param u 範圍是0-5
     * @return 範圍是[0.5-1]
     */
    public static double normalDistribution(double u) {
        double y = Math.abs(u);
        double y2 = y * y;
        double z = Math.exp(-0.5 * y2) * 0.398942280401432678;
        double p = 0;
        //循環次數(連分式的項數)控制精度
        int k = 28;
        double s = -1;
        double fj = k;

        if (y > 3) {
            //當y>3時
            for (int i = 1; i <= k; i++) {
                p = fj / (y + p);
                fj = fj - 1.0;
            }
            p = z / (y + p);
        } else {
            //當y<3時
            for (int i = 1; i <= k; i++) {
                p = fj * y2 / (2.0 * fj + 1.0 + s * p);
                s = -s;
                fj = fj - 1.0;
            }
            p = 0.5 - z * y / (1 - p);
        }
        if (u > 0) {
            p = 1.0 - p;
        }
        return p;
    }

爆出旅途獎品(開獎、抽獎)

抽獎算法有很多種,但是

  • 時間、空間複雜度低(性能好)
  • 數據結構恰當(對象建模)
  • 算法易於理解

的就比較少了,本活動抽獎算法使用《darts-dice-coins》文章裏介紹的Alias改進算法來實現的。下面介紹其算法的主要思想

拋均勻硬幣

假設硬幣是均勻的,則其結果和概率爲

  • 硬幣正面:概率爲1/2。
  • 硬幣反面:概率爲1/2。

算法(僞代碼)

Algorithm: Simulating a Fair Die

//可以用(new Random().nextDouble())來生成[0,1)的僞隨機數
1. Generate a uniformly-random value x in the range [0,1).

//n:產生多少種結果,丟硬幣只有正反面2種,故n=2
//⌊⌋是向下取證。
2. Return ⌊xn⌋.

舉個栗子

1. 隨機生成一個[0,1)的數爲0.82
2. 代入算法得⌊xn⌋=⌊0.82 * 2⌋=⌊1.64⌋=1,是正面

// another example
1. 隨機生成一個[0,1)的數爲0.1
2. 代入算法得⌊xn⌋=⌊0.1 * 2⌋=⌊0.2⌋=0,是反面
拋不均勻硬幣

假設硬幣是均勻的,則其結果和概率爲

  • 硬幣正面:概率爲1/4。
  • 硬幣反面:概率爲3/4。

算法(僞代碼)

Algorithm: Simulating a Biased Coin
1. Generate a uniformly-random value x in the range [0,1).
                                                     
//pheads就是正面的概率,即pheads=1/4                                                     
2. If x<pheads, return "heads."
3. If x≥pheads, return "tails."
Alias改進算法

其實它的算法來源思想就是由

  • 拋均勻硬幣
  • 拋不均勻硬幣

而來的。要詳細介紹,請移步darts-dice-coins文章。要注意的點是概率總和爲1,因爲這是Alias算法的前提。

Alias算法實現(Java版本)
/******************************************************************************
 * File: AliasMethod.java
 * Author: Keith Schwarz ([email protected])
 *
 * An implementation of the alias method implemented using Vose's algorithm.
 * The alias method allows for efficient sampling of random values from a
 * discrete probability distribution (i.e. rolling a loaded die) in O(1) time
 * each after O(n) preprocessing time.
 *
 * For a complete writeup on the alias method, including the intuition and
 * important proofs, please see the article "Darts, Dice, and Coins: Smpling
 * from a Discrete Distribution" at
 *
 *                 http://www.keithschwarz.com/darts-dice-coins/
 */
import java.util.*;

public final class AliasMethod {
    /* The random number generator used to sample from the distribution. */
    private final Random random;

    /* The probability and alias tables. */
    private final int[] alias;
    private final double[] probability;

    /**
     * Constructs a new AliasMethod to sample from a discrete distribution and
     * hand back outcomes based on the probability distribution.
     * <p>
     * Given as input a list of probabilities corresponding to outcomes 0, 1,
     * ..., n - 1, this constructor creates the probability and alias tables
     * needed to efficiently sample from this distribution.
     *
     * @param probabilities The list of probabilities.
     */
    public AliasMethod(List<Double> probabilities) {
        this(probabilities, new Random());
    }

    /**
     * Constructs a new AliasMethod to sample from a discrete distribution and
     * hand back outcomes based on the probability distribution.
     * <p>
     * Given as input a list of probabilities corresponding to outcomes 0, 1,
     * ..., n - 1, along with the random number generator that should be used
     * as the underlying generator, this constructor creates the probability 
     * and alias tables needed to efficiently sample from this distribution.
     *
     * @param probabilities The list of probabilities.
     * @param random The random number generator
     */
    public AliasMethod(List<Double> probabilities, Random random) {
        /* Begin by doing basic structural checks on the inputs. */
        if (probabilities == null || random == null)
            throw new NullPointerException();
        if (probabilities.size() == 0)
            throw new IllegalArgumentException("Probability vector must be nonempty.");

        /* Allocate space for the probability and alias tables. */
        probability = new double[probabilities.size()];
        alias = new int[probabilities.size()];

        /* Store the underlying generator. */
        this.random = random;

        /* Compute the average probability and cache it for later use. */
        final double average = 1.0 / probabilities.size();

        /* Make a copy of the probabilities list, since we will be making
         * changes to it.
         */
        probabilities = new ArrayList<Double>(probabilities);

        /* Create two stacks to act as worklists as we populate the tables. */
        Deque<Integer> small = new ArrayDeque<Integer>();
        Deque<Integer> large = new ArrayDeque<Integer>();

        /* Populate the stacks with the input probabilities. */
        for (int i = 0; i < probabilities.size(); ++i) {
            /* If the probability is below the average probability, then we add
             * it to the small list; otherwise we add it to the large list.
             */
            if (probabilities.get(i) >= average)
                large.add(i);
            else
                small.add(i);
        }

        /* As a note: in the mathematical specification of the algorithm, we
         * will always exhaust the small list before the big list.  However,
         * due to floating point inaccuracies, this is not necessarily true.
         * Consequently, this inner loop (which tries to pair small and large
         * elements) will have to check that both lists aren't empty.
         */
        while (!small.isEmpty() && !large.isEmpty()) {
            /* Get the index of the small and the large probabilities. */
            int less = small.removeLast();
            int more = large.removeLast();

            /* These probabilities have not yet been scaled up to be such that
             * 1/n is given weight 1.0.  We do this here instead.
             */
            probability[less] = probabilities.get(less) * probabilities.size();
            alias[less] = more;

            /* Decrease the probability of the larger one by the appropriate
             * amount.
             */
            probabilities.set(more, 
                              (probabilities.get(more) + probabilities.get(less)) - average);

            /* If the new probability is less than the average, add it into the
             * small list; otherwise add it to the large list.
             */
            if (probabilities.get(more) >= 1.0 / probabilities.size())
                large.add(more);
            else
                small.add(more);
        }

        /* At this point, everything is in one list, which means that the
         * remaining probabilities should all be 1/n.  Based on this, set them
         * appropriately.  Due to numerical issues, we can't be sure which
         * stack will hold the entries, so we empty both.
         */
        while (!small.isEmpty())
            probability[small.removeLast()] = 1.0;
        while (!large.isEmpty())
            probability[large.removeLast()] = 1.0;
    }

    /**
     * Samples a value from the underlying distribution.
     *
     * @return A random value sampled from the underlying distribution.
     */
    public int next() {
        /* Generate a fair die roll to determine which column to inspect. */
        int column = random.nextInt(probability.length);

        /* Generate a biased coin toss to determine which option to pick. */
        boolean coinToss = random.nextDouble() < probability[column];

        /* Based on the outcome, return either the column or its alias. */
        return coinToss? column : alias[column];
    }
}

獎品庫存控制

庫存是共享資源,多線程下不加以控制會造成多減庫存。控制庫存減去主要利用

  • 庫存數據來源唯一,即都由redis控制。
  • 利用redis自帶的原子性操作函數來減庫存。
//減庫存
redisTemplate.opsForValue().increment(key, -1)
// 加庫存
redisTemplate.opsForValue().increment(key, 1)
  • 庫存從db放入redis緩存中需要加以控制(用分佈式鎖),因爲項目是分佈式的。 參考一章中已經給出具體的實現方案了,這裏不贅餘。

分佈式鎖建議:單實例redis鎖 < redisson < zookepper

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章