想到之前面试遇到的一个关于Redis解决方案的问题,趁今天有时间来进行一番实践,问题是这样的:
Redis的五种数据结构中,使用什么样的数据结构来实现发红包、抢红包以及红包记录?
首先回顾一下redis的五种数据结构以及底层实现原理:

  1. String:最基本的数据类型,底层主要是int和SDS实现
  2. Hash:String元素组成的字典,底层使用ziplist和hashtable,适合用于存储对象
  3. List:列表,底层使用链表(大数据量)/压缩列表(小数据量)实现,按照String元素插入顺序排序
  4. Set:String元素组成的无序集合,底层使用intset和hashtable实现,不允许重复,支持 多个集合取交集/并集/差集
  5. ZSet:通过分数值(score)来为集合中的成员进行从小到大的排序,底层使用ziplist或skiplist实现

       面试时回答的是使用Hash结构来实现红包功能,既可以存储每个红包的值也可以存储每个用户抢到红包的多少。
       可能也是由于面试时时间紧张,思考问题没有那么全面,下来一细想这样也会存在一些弊端,比如红包数值明显会多存储一份在Redis,增加Redis内存负担。现在我的思路是使用List结构存储红包金额,然后每个用户抢红包的时候从List中取一个红包,并把红包记录存入Hash结构中,红包抢完后再次从List中获取数据时会返回null表示红包已经抢完,List过期时也方便退回余额。

代码如下:
1、根据红包大小和个数定义红包金额

public static double[] defineRedPacket(int total, double amount) {
   Random random = new Random();
    DecimalFormat df = new DecimalFormat(".##");
    double middle = Double.parseDouble(df.format(amount / total));
    double[] result = new double[total];
    double redMoney;
    double nextMoney = amount;
    int index = 0;
    for (int i = total; i > 0; i--) {
        if (i == 1) {
            result[index] = nextMoney;
        } else {
            while (true) {
                String str = df.format(random.nextDouble() * nextMoney);
                redMoney = Double.parseDouble(str);
                if (redMoney > 0 && redMoney < middle) {
                    break;
                }
            }
            nextMoney = Double.parseDouble(df.format(nextMoney - redMoney));
            result[index] = redMoney;
            middle = Double.parseDouble(df.format(nextMoney / (i - 1)));
            index++;
        }
    }
    return result;
}

2、保存红包 使用redis的list数据类型

public static void saveRedPacket(String packetId, double[] redPacketArr) {
    Jedis jedis = new Jedis("127.0.0.1", 6379);
    for (double amount : redPacketArr) {
        jedis.lpush(packetId, String.valueOf(amount));
    }
    jedis.close();
}

3、用户抢红包 使用redis的hash保存红包记录(可持久化到数据库)

public static String getRedPacket(String packetId, String userId) {
    Jedis jedis = new Jedis("127.0.0.1", 6379);
    String amount = jedis.lpop(packetId);
    //保存红包记录
    jedis.hset("user_" + packetId, userId, amount);
    jedis.close();
    return amount;
}

4、测试类

public static void main(String[] args) {
   String redPacketId = UUID.randomUUID().toString();
    double[] packets = RedPacketRedis.defineRedPacket(100, 10000d);
    RedPacketRedis.saveRedPacket(redPacketId, packets);
    CountDownLatch latch = new CountDownLatch(100);
    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            try {
                latch.countDown();
                latch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            String userId = Thread.currentThread().getName();
            String amount = RedPacketRedis.getRedPacket(redPacketId, userId);
            System.out.println("userId:" + userId + ",金额:" + amount);
        }
    };
    for (int i = 0; i < 100; i++) {
        Thread thread = new Thread(runnable);
        thread.start();
    }
}