生成字母 + 数字的六位数唯一码

有个优惠码的需求, 由于历史原因, 优惠码需要是由小写字母跟阿拉伯数字组成的六位数的码, 并且必须是唯一的.

找不到什么高效的办法, 也看不懂高大上的算法.. 那就土办法吧.

优惠码的形式是六位数的 36 进制数, 最大值为 zzzzzz 也就是 十进制的 2176782335, 也就是最多会有 2176782336 个码. 随机生成肯定是不靠谱的, 从 0 开始自增也不靠谱, 那样容易被猜到其他的优惠码.

那么换成, 把 2176782336 个码按照递增顺序均匀切割成若干块, 每次随机取一块, 然后从块里面按自增获取一个码就好了.

假设每块里面有 50000 个码, 切割后会有 43536 块, 把每块里面开始的码当作种子 (a) 存入种子池, 记录每块里面已获取的码的数量 (b), 每次获取后, 把 b 自增.

那么当获取到种子后, 计算码的公式为:

a * 50000 + b

然后再把获取到的码转换成 36 进制.

实现上, 可以把使用 redis 存放种子池, 使用一个有序集合, member 为种子值, score 为已获取的码数量.

一个简单的实现:

<?php
/**
 * @author: RunnerLee
 * @email: runnerleer@gmail.com
 * @time: 2017-11
 */

namespace Runner\IdiotUuid;

use Exception;
use Predis\Client;

class Idiot
{
    const REDIS_AVAILABLE_SEEDS = 'coupon:available:seeds';

    const REDIS_SEEDS = 'coupon:seeds';

    /**
     * @var Client
     */
    protected $redis;

    /**
     * Coupon constructor.
     *
     * @param Client $client
     */
    public function __construct(Client $client)
    {
        $this->redis = $client;
    }

    /**
     * 初始化种子池, 提出头尾两个种子, 剩下可用码数 2176650000.
     *
     * @return void
     */
    public function initSeeds()
    {
        if (0 === $this->redis->exists(static::REDIS_SEEDS)) {
            $this->redis->zadd(static::REDIS_SEEDS, array_fill(1, 43534, 0));
            $this->redis->sadd(static::REDIS_AVAILABLE_SEEDS, range(1, 43534));
        }
    }

    /**
     * @throws Exception
     *
     * @return string
     */
    public function apply()
    {
        /*
         * 从有效种子池中获取一个有效的种子
         */
        if (is_null($index = $this->redis->srandmember(static::REDIS_AVAILABLE_SEEDS))) {
            throw new Exception('no available seeds');
        }

        /**
         * 获取种子的使用次数.
         */
        $score = (int) $this->redis->zscore(static::REDIS_SEEDS, $index);

        /**
         * 计算 code 值
         */
        $number = (int) $index * 50000 + $score;

        /*
         * 自增种子使用次数, 供下次直接使用
         */
        $this->redis->zincrby(static::REDIS_SEEDS, 1, $index);

        /*
         * 如果种子使用次数达到 50000 次, 从有效池中移除
         */
        if (49999 === $score) {
            $this->redis->srem(static::REDIS_AVAILABLE_SEEDS, $index);
        }

        /*
         * 返回三十六进制的 code
         */
        return str_pad(base_convert($number, 10, 36), 6, '0', STR_PAD_LEFT);
    }
}

写了一个小脚本, 死循环生成 code, 把 code 放到一个无序集合里面, 利用 sadd 的返回值判断是否有重复, 生成了七千多万个码都不会没有重复. 棒棒哒

GitHub: https://github.com/RunnerLee/idiot-uuid

本帖已被设为精华帖!
本帖由系统于 5年前 自动加精
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 6

另一个解决方案:自增 id 然后 hashid 。 自增 id 是可以保证唯一,然后用 Hashids 加密,Hashids 设置字母表为 0-9a-z

ID 混淆扩展包 Laravel Hashid:分享:ID 混淆扩展包 Laravel Hashid 新鲜出炉

示例:

$ composer require elfsundae/laravel-hashid

Laravel 5.5 的话会自动注册 ServiceProvider,直接用 tinker 测试:

$ php artisan tinker
Psy Shell v0.8.15 (PHP 7.1.11 — cli) by Justin Hileman
>>> config([
... 'hashid.connections.hashids_integer.alphabet' => 'abcdefghijklmnopqrstuvwxyz1234567890',
... 'hashid.connections.hashids_integer.min_length' => 6,
... ])
=> null
>>> hashid_encode(1)
=> "jdkoe5"
>>> hashid_encode(100000)
=> "ejpzky"
>>> hashid_encode(999888) 
=> "3rp2jp"
>>> 
6年前 评论

@ElfSundae 赞. 原来有看到这个 hashids, 没注意到是支持加 salt 和自定义字典的.

但是也有不足的地方, 我尝试了一下, 用 uniqid() 生成一个随机的 salt, 然后 alphabet 设置为 abcdefghijklmnopqrstuvwxyz1234567890, minHashLength 设置为 6.

将自增 ID 进行 encode, 简单试了一下, 在 7962624 的时候, 返回的码开始增加到了 7 位. 也就是如果要求优惠码只能 6 位的时候, 用上面的配置, 只能生成接近 800w 个码.

不过 800w 也够我们这破公司用很多了 hhh.

6年前 评论

@RunnerLee 嗯,限定优惠码长度就限定了优惠码数量... 如果在这个糟糕的、致命的、无意义的前提下还要能生成最大数量的唯一码,那只能自己实现发码了... 现成的、成熟的、高性能的算法可能不合适

6年前 评论

@ElfSundae 是的. 不过也不是在说一定要有这么苛刻的条件, 提前预知可能的问题而已. 毕竟.. 万一这套代码长命到已经把码全部发完了呢 :smile:

6年前 评论

感谢大大 :kissing_heart:提供思路

4年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!