面試題目:一個抽獎池設計(含程式碼)

逆光影者發表於2020-12-20

題目

如何設計一個抽獎池,總抽獎金額5w元,裡面有1元的,2元的,5元的面額等等。每個面額的有指定的次數限制,比如1元的10000次,2元的20000次,5元的2000次。
要求:

  1. 每個面額的次數不能超
  2. 總抽獎金額不能超
  3. 效能不能太差,比如不能設定一個全域性鎖,至少是使用者級別的鎖

設計思想

  1. 該場景主要需要解決高併發問題,
  2. 1元, 2元, 3元獎券, 以及金額池採用抽獎之前先凍結必要的獎券數和金額的方式, 之後抽獎後用不到的獎券和金額再釋放掉.
  3. 最初考慮bitmap來著, 但是bitmap的佔用和釋放以及遍歷每每都是麻煩, 最後採用AtomicInteger的CAS的方式解決併發量和多執行緒問題.

程式碼

先編寫抽獎相關的函式, 至於抽獎池以及獎券的佔用和釋放先寫為抽象函式, 之後慢慢實現

package cn.cpf.test;

import lombok.extern.slf4j.Slf4j;

import java.util.Random;

/**
 * <b>Description : </b>
 *
 * @author CPF
 * @date 2020/12/19 20:27
 **/
@Slf4j
public abstract class AbstractLotteryMachine {

    private final Random random = new Random();

    /**
     * 抽獎方法
     *
     * @return 抽獎金額(1: 1元, 2: 2元, 5: 5元, 0: 沒獎了)
     */
    public int lottery() {
        boolean et1 = false;
        boolean et2 = false;
        boolean et5 = false;
        int occupyMoney = 0;
        int money = 0;
        try {
            occupyMoney = occupyMoneyTotal();
            if (occupyMoney == 0) {
                return money;
            }
            assert occupyMoney > 0;
            et1 = occupyMoney1();
            et2 = occupyMoney2();
            et5 = occupyMoney5();
            money = randomMoney(et1, et2, et5, occupyMoney);
            assert money <= occupyMoney;
            return money;
        } catch (RuntimeException e){
            log.error("et1: {}, et2: {}, et3:{}, occ:{}, money: {}", et1, et2, et5, occupyMoney, money);
            throw e;
        } finally {
            if (et1 && money != 1) {
                releaseMoney1();
            }
            if (et2 && money != 2) {
                releaseMoney2();
            }
            if (et5 && money != 5) {
                releaseMoney5();
            }
            assert occupyMoney >= money;
            if (occupyMoney > 0) {
                final int releaseMoney = occupyMoney - money;
                releaseMoneyTotal(releaseMoney);
            }
        }
    }

    /**
     * 10000張1元 + 20000張2元 剛好等於 50000, 也就是說最優的情況是剛好抽獎抽了10000張1元 和 20000張2元,
     * 5元的獎券抽中的次數越少越節約成本, 1元, 2元的券之間概率調整沒什麼意義
     *
     * @param et1         1元獎券是否存在
     * @param et2         2元獎券是否存在
     * @param et5         5元獎券是否存在
     * @param occupyMoney 佔據金額
     * @return 抽取的獎券 0:沒有合適的將全力, 1: 1元, 2: 2元, 5: 5元
     */
    protected int randomMoney(boolean et1, boolean et2, boolean et5, int occupyMoney) {
        final int[] probability = new int[]{22, 44, 4};
        int number = 0;
        if (et1 && occupyMoney >= 1) {
            number += probability[0];
        }
        if (et2 && occupyMoney >= 2) {
            number += probability[1];
        }
        if (et5 && occupyMoney >= 5) {
            number += probability[2];
        }
        if (number == 0) {
            return 0;
        }
        int rdm = random.nextInt(number);
        if (et1 && occupyMoney >= 1) {
            if (rdm < probability[0]) {
                return 1;
            }
            rdm -= probability[0];
        }
        if (et2 && occupyMoney >= 2) {
            if (rdm < probability[1]) {
                return 2;
            }
            rdm -= probability[1];
        }
        if (et5 && occupyMoney >= 5 && rdm < probability[2]) {
            return 5;
        }
        throw new RuntimeException("計算異常");
    }

    /**
     * 佔據一張獎票(1元)
     *
     * @return 是否佔用成功
     */
    protected abstract boolean occupyMoney1();

    /**
     * 佔據一張獎票(2元)
     *
     * @return 是否佔用成功
     */
    protected abstract boolean occupyMoney2();

    /**
     * 佔據一張獎票(5元)
     *
     * @return 是否佔用成功
     */
    protected abstract boolean occupyMoney5();

    /**
     * 佔據金額(若餘額高於5元, 優先則佔據5元, 否則若高於2元, 優先佔據2元, 否則若高於1元, 則佔據1元, 否則不佔據任何金額, 返回0)
     *
     * @return 佔據金額
     */
    protected abstract int occupyMoneyTotal();

    /**
     * 釋放一張獎票(1元)
     */
    protected abstract void releaseMoney1();

    /**
     * 釋放一張獎票(2元)
     */
    protected abstract void releaseMoney2();

    /**
     * 釋放一張獎票(5元)
     */
    protected abstract void releaseMoney5();

    /**
     * 釋放金額
     * @param money 釋放金額
     */
    protected abstract void releaseMoneyTotal(int money);


}

獎券的佔用和釋放的實現類

package cn.cpf.test;

import lombok.ToString;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * <b>Description : </b>
 *
 * @author CPF
 * @date 2020/12/19 21:07
 **/
@ToString
public class AtomicLotteryMachine extends AbstractLotteryMachine {

    /**
     * 1元獎票數量
     */
    private final AtomicInteger ticketNumber1 = new AtomicInteger(10000);
    private final AtomicInteger ticketNumber2 = new AtomicInteger(20000);
    private final AtomicInteger ticketNumber5 = new AtomicInteger(2000);

    /**
     * 獎金池
     */
    private final AtomicInteger ticketMoneyTotal = new AtomicInteger(50000);

    /**
     * 佔據一張獎票(1元)
     *
     * @return 是否佔用成功
     */
    @Override
    protected boolean occupyMoney1() {
        final int andDecrement = ticketNumber1.getAndDecrement();
        if (andDecrement > 0) {
            return true;
        }
        releaseMoney1();
        return false;
    }

    /**
     * 佔據一張獎票(2元)
     *
     * @return 是否佔用成功
     */
    @Override
    protected boolean occupyMoney2() {
        final int andDecrement = ticketNumber2.getAndDecrement();
        if (andDecrement > 0) {
            return true;
        }
        releaseMoney2();
        return false;
    }

    /**
     * 佔據一張獎票(5元)
     *
     * @return 是否佔用成功
     */
    @Override
    protected boolean occupyMoney5() {
        final int andDecrement = ticketNumber5.getAndDecrement();
        if (andDecrement > 0) {
            return true;
        }
        releaseMoney5();
        return false;
    }


    /**
     * 佔據金額(若餘額高於5元, 優先則佔據5元, 否則若高於2元, 優先佔據2元, 否則若高於1元, 則佔據1元, 否則不佔據任何金額, 返回0)
     *
     * @return 佔據金額
     */
    @Override
    protected int occupyMoneyTotal() {
        final int i = ticketMoneyTotal.get();
        if (i <= 0) {
            return 0;
        }
        final int andAdd = ticketMoneyTotal.addAndGet(-5);
        if (andAdd >= 0) {
            return 5;
        }
        if (andAdd >= -3) {
            releaseMoneyTotal(3);
            return 2;
        }
        if (andAdd >= -4) {
            releaseMoneyTotal(4);
            return 1;
        }
        releaseMoneyTotal(5);
        return 0;
    }

    /**
     * 釋放一張獎票(1元)
     */
    @Override
    protected void releaseMoney1() {
        ticketNumber1.getAndIncrement();
    }

    /**
     * 釋放一張獎票(2元)
     */
    @Override
    protected void releaseMoney2() {
        ticketNumber2.getAndIncrement();
    }

    /**
     * 釋放一張獎票(5元)
     */
    @Override
    protected void releaseMoney5() {
        ticketNumber5.getAndIncrement();
    }

    /**
     * 釋放金額
     *
     * @param money 釋放金額
     */
    @Override
    protected void releaseMoneyTotal(int money) {
        ticketMoneyTotal.getAndAdd(money);
    }

}

測試類

package cn.cpf.test.d;

import cn.cpf.test.AbstractLotteryMachine;
import cn.cpf.test.AtomicLotteryMachine;
import com.github.cosycode.common.util.otr.TestUtils;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.IntStream;

/**
 * <b>Description : </b>
 *
 * @author CPF
 * @date 2020/12/19 22:39
 **/
public class Test {

    public static void main(String[] args) {
        System.out.println(Charset.defaultCharset().name());
        AbstractLotteryMachine lotteryMachine = new AtomicLotteryMachine();
        System.out.println("未抽獎之前獎池情況" + lotteryMachine.toString());

        AtomicInteger t1 = new AtomicInteger();
        AtomicInteger t2 = new AtomicInteger();
        AtomicInteger t5 = new AtomicInteger();
        AtomicInteger tM = new AtomicInteger();

        TestUtils.callTime("抽獎測試", () -> {
            // 30000 次併發可以抽空獎池
            IntStream.range(1, 30000).parallel().forEach(it -> {
                final int lottery = lotteryMachine.lottery();
                switch (lottery) {
                    case 1:
                        t1.addAndGet(1);
                        tM.addAndGet(1);
                        break;
                    case 2:
                        t2.addAndGet(1);
                        tM.addAndGet(2);
                        break;
                    case 5:
                        t5.addAndGet(1);
                        tM.addAndGet(5);
                        break;
                    case 0:
                        break;
                    default:
                        throw new RuntimeException("default");
                }
            });
        });

        System.out.println("抽獎之後獎池情況" + lotteryMachine.toString());
        System.out.println("1元獎券出現次數 " + t1);
        System.out.println("2元獎券出現次數 " + t2);
        System.out.println("5元獎券出現次數 " + t5);
        System.out.println("抽獎金額 " + tM);
    }

}

輸出

使用並行流30000次單機模擬抽獎測試 27毫秒, 資料正常.

未抽獎之前獎池情況AtomicLotteryMachine(ticketNumber1=10000, ticketNumber2=20000, ticketNumber5=2000, ticketMoneyTotal=50000)
[INFO] 02:44:40 (TestUtils.java:42)=> [624315079342100 : 抽獎測試] ==> start
[INFO] 02:44:40 (TestUtils.java:45)=> [624315079342100 : 抽獎測試] ==> end, consume time: 27538200 
抽獎之後獎池情況AtomicLotteryMachine(ticketNumber1=1549, ticketNumber2=2983, ticketNumber5=497, ticketMoneyTotal=0)
1元獎券出現次數 8451
2元獎券出現次數 17017
5元獎券出現次數 1503
抽獎金額 50000

相關文章