【精华】利用Redis lua脚本实现高并发抢红包

前言

如今各种直播、裂变盛行,抢红包不可以少。

本文讲解如何通过Redis lua实现抢红包逻辑,包含RedissionSpring-data-redis两种常见客户端Api实现。

红包需求:
1、支持拼手气、普通两种红包
2、同一个用户一个红包只能抢一次
3、统计每个红包的已抢金额、剩余金额、已抢个数,红包过期未抢自动失效
4、记录每个用户抢的红包明细:时间、金额


编写抢红包脚本

在工程resources/lua下面,编写脚本redis.lua
lua脚本需要传递三个参数,分别为:Key前缀、红包id、用户id。使用了cjson操作json

do
    -- Key前缀
    local keyPrefix = KEYS[1]
    local bagId = KEYS[2]
    local userId = KEYS[3]
    local bagInfoKey = keyPrefix..bagId..':info'
    local bagPoolKey = keyPrefix..bagId..':pool'
    local bagTakeKey = keyPrefix..bagId..':take'
    local nowTime = redis.call('time')

    -- 开启单命令复制模式
    redis.replicate_commands();

    -- 检查是否已抢购
    if redis.call('hexists', bagTakeKey, userId)~=0 then
        return nil
    end

    -- 检查是否还有剩余红包
    local rbTake = redis.call('rpop', bagPoolKey)
    if not rbTake then
        return nil
    end

    -- 记录用户抢购信息
    local item = cjson.decode(rbTake)
    item["userId"] = userId
    item["takeDate"] = nowTime[1]*1000
    local rb = cjson.encode(item)
    redis.call('hset', bagTakeKey, userId, rb)

    -- 更新红包信息
    local rbJson = redis.call('get', bagInfoKey)
    local rbObj = cjson.decode(rbJson)
    rbObj["takeCount"] = rbObj["takeCount"] + 1
    rbObj["takeAmount"] = rbObj["takeAmount"] + item["amount"]
    redis.call('set', bagInfoKey, cjson.encode(rbObj))

    return rb
end

编码代码

1、创建红包信息:RedEnvelope

@Data
public class RedEnvelope {
    /**
     * 红包id
     */
    private String id;
    /**
     * 红包总金额
     */
    private int amount;
    /**
     * 红包总个数
     */
    private int count;
    /**
     * 红包已抢个数
     */
    private int takeCount;
    /**
     * 红包已抢金额
     */
    private int takeAmount;
    /**
     * 相对当前到期时间,单位s
     */
    private long expireIn;

    /**
     * 红包类型
     */
    public enum Type {
        /**
         * 拼手气红包
         */
        LUCKY,
        /**
         * 普通红包
         */
        GENERAL
    }
}

2、红包抢购明细:RedEnvelopeDetail

@Data
public class RedEnvelopeDetail {
    /**
     * 用户id
     */
    private String userId;
    /**
     * 红包id
     */
    private String bagId;
    /**
     * 抢到金额
     */
    private int amount;
    /**
     * 抢到日期(毫秒)
     */
    private long takeDate;
}

3、红包拆分:RedEnvelopeSpliter

@Slf4j
public class RedEnvelopeSpliter {
    public static List<RedEnvelopeDetail> splitRedPacket(RedEnvelope sendRedBag, int total, int count, RedEnvelope.Type type) {
        return type == RedEnvelope.Type.LUCKY ?
                splitLuckyRedPacket(sendRedBag, total, count) :
                splitEquallyRedPacket(sendRedBag, total, count);
    }

    public static List<RedEnvelopeDetail> splitLuckyRedPacket(RedEnvelope sendRedBag, int total, int count) {
        int use = 0;
        int[] array = new int[count];
        // 最小红包
        int minValue = 1;
        // 最大红包
        int maxValue = Math.max(minValue, total - count * minValue);
        // 红包
        List<RedEnvelopeDetail> bags = new ArrayList<>(count);

        for (int i = 0; i < count && use < total; i++) {
            if (i == count - 1) {
                array[i] = total - use;
            } else {
                // 红包随机金额浮动系数
                int avg = (total - use) * 2 / (count - i);
                // 随机最大值限制
                avg = Math.min(maxValue, Math.max(minValue, avg));
                // 红包金额
                array[i] = RandomUtils.nextInt(minValue, avg);
            }

            use = use + array[i];
            RedEnvelopeDetail bagDetail = createDetail(sendRedBag, array[i]);
            bags.add(bagDetail);
        }

        log.info("拆解红包,总额:{}, 个数:{}, 红包金额:{}", total, count, Arrays.stream(array).boxed().collect(Collectors.toList()));

        return bags;
    }

    public static List<RedEnvelopeDetail> splitEquallyRedPacket(RedEnvelope sendRedBag, int total, int count) {
        int money = total / count;
        int used = 0;
        List<RedEnvelopeDetail> bags = new ArrayList<>(count);

        for (int i = 0; i < count; i++) {
            // 最后一个
            if (i == count - 1) {
                money = total - used;
            }
            RedEnvelopeDetail bagDetail = createDetail(sendRedBag, money);
            bags.add(bagDetail);
            used += money;
        }

        return bags;
    }

    private static RedEnvelopeDetail createDetail(RedEnvelope sendRedBag, int money) {
        RedEnvelopeDetail bagDetail = new RedEnvelopeDetail();
        bagDetail.setBagId(sendRedBag.getId());
        bagDetail.setAmount(money);
        return bagDetail;
    }
}

4、抢红包接口:RedEnvelopeService

public interface RedEnvelopeService {
    /**
     * 发送红包
     *
     * @param amount          红包总金额
     * @param count           红包个数
     * @param expireInSeconds 过期时间
     * @param type            红包类型,分拼手气、普通红包
     * @return 红包信息
     */
    RedEnvelope sendBag(int amount, int count, int expireInSeconds, RedEnvelope.Type type);

    /**
     * 领取红包
     *
     * @param bagId  红包id
     * @param userId 用户id
     * @return 领取红包结果
     */
    RedEnvelopeDetail takeBag(String bagId, String userId);
}

4.1 Redission实现

配置Redission

@Bean
public RedissonClient redissonClient(RedisProperties redis) {
    Config config = new Config();
    config.useSingleServer()
            .setDatabase(redis.getDatabase())
            .setAddress("redis://"+ redis.getHost() +":" + redis.getPort());
    return Redisson.create(config);
}

实现

@Slf4j
@Component
@ConditionalOnProperty(name = "red-envelop.type", havingValue = "redission")
public class RedissionRedEnvelopeService implements RedEnvelopeService, InitializingBean {
    private final Codec codec = new JsonJacksonCodec();
    private String scriptSha;
    @Value("${prefixCacheKey:_red_envelop:}")
    private String prefixCacheKey;
    @Autowired
    private RedissonClient redissonClient;

    @Override
    public void afterPropertiesSet() throws Exception {
        String luaScript =  ResourceUtil.readStr("lua/redis.lua", StandardCharsets.UTF_8);
        RScript rScript = redissonClient.getScript(new JsonJacksonCodec());
        scriptSha = rScript.scriptLoad(luaScript);
    }

    @Override
    public RedEnvelope sendBag(int totalMoney, int count, int expireSeconds, RedEnvelope.Type type) {
        RedEnvelope sendRedBag = new RedEnvelope();
        sendRedBag.setId(UUID.randomUUID().toString());
        sendRedBag.setAmount(totalMoney);
        sendRedBag.setCount(count);
        sendRedBag.setExpireIn(expireSeconds);

        // 记录发送的红包
        RBucket<RedEnvelope> bucket = redissonClient.getBucket(prefixCacheKey + sendRedBag.getId() + ":info", codec);
        bucket.set(sendRedBag);

        // 分割红包
        List<RedEnvelopeDetail> bagDetailList = RedEnvelopeSpliter.splitRedPacket(sendRedBag, totalMoney, count, type);

        // 发送红包池
        RDeque<RedEnvelopeDetail> deque = redissonClient.getDeque(prefixCacheKey + sendRedBag.getId() + ":pool", new JsonJacksonCodec());
        deque.addAll(bagDetailList);
        // 设置失效时间, 默认30s
        deque.expire(expireSeconds <= 0 ? 30 : expireSeconds, TimeUnit.SECONDS);

        return sendRedBag;
    }

    @Override
    public RedEnvelopeDetail takeBag(String bagId, String userId) {
        List<Object> keys = Arrays.asList(prefixCacheKey, bagId, userId);
        return redissonClient.getScript(new JsonJacksonCodec())
                .evalSha(RScript.Mode.READ_WRITE, scriptSha, RScript.ReturnType.VALUE, keys);
    }
}

4.2 Spring-data-redis实现

	@Component
	@ConditionalOnProperty(name = "red-envelop.type", havingValue = "redis", matchIfMissing = true)
public class RedisRedEnvelopeService implements RedEnvelopeService, InitializingBean {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    @Value("${prefixCacheKey:_red_envelop:}")
    private String prefixCacheKey;
    private RedisScript<RedEnvelopeDetail> rateLuaScript;

    @Override
    public void afterPropertiesSet() {
        String luaScript =  ResourceUtil.readStr("lua/redis.lua", StandardCharsets.UTF_8);
        rateLuaScript = new DefaultRedisScript<>(luaScript, RedEnvelopeDetail.class);
    }

    @Override
    public RedEnvelope sendBag(int amount, int count, int expireInSeconds, RedEnvelope.Type type) {
        RedEnvelope sendRedBag = new RedEnvelope();
        sendRedBag.setId(UUID.randomUUID().toString());
        sendRedBag.setAmount(amount);
        sendRedBag.setCount(count);
        sendRedBag.setExpireIn(expireInSeconds);

        // 保存红包信息
        String key = prefixCacheKey + sendRedBag.getId() + ":info";
        redisTemplate.opsForValue().set(key, sendRedBag);

        // 分割红包、入池
        List<RedEnvelopeDetail> bagDetailList = RedEnvelopeSpliter.splitRedPacket(sendRedBag, amount, count, type);
        key = prefixCacheKey + sendRedBag.getId() + ":pool";
        redisTemplate.opsForList().rightPushAll(key, bagDetailList.toArray(new RedEnvelopeDetail[]{}));
        redisTemplate.expire(key, expireInSeconds, TimeUnit.SECONDS);

        return sendRedBag;
    }

    @Override
    public RedEnvelopeDetail takeBag(String bagId, String userId) {
        List<String> keys = Arrays.asList(prefixCacheKey, bagId, userId);
        return redisTemplate.execute(rateLuaScript, keys);
    }
}

编写测试

RedisRedEnvelopeServiceTest

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(properties = {"spring.profiles.active=201"}, classes = TestApplication.class)
public class RedisRedEnvelopeServiceTest {
    @Autowired
    private RedEnvelopeService redEnvelopeService;

    @Test
    public void sendAndTake() {
        RedEnvelope redEnvelope = redEnvelopeService.sendBag(100, 3, 1200, RedEnvelope.Type.LUCKY);
        // 第一次
        RedEnvelopeDetail redEnvelopeResult = redEnvelopeService.takeBag(redEnvelope.getId(), "jxw");
        assert redEnvelopeResult != null;
        // 第二次
        redEnvelopeResult = redEnvelopeService.takeBag(redEnvelope.getId(), "jxw");
        assert redEnvelopeResult == null;
    }
}

RedissionRedEnvelopeServiceTest

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(properties = {"spring.profiles.active=201", "red-envelop.type=redission"}, classes = TestApplication.class)
public class RedissionRedEnvelopeServiceTest {
    @Autowired
    private RedEnvelopeService redEnvelopeService;

    @Test
    public void sendAndTake() {
        RedEnvelope redEnvelope = redEnvelopeService.sendBag(100, 3, 1200, RedEnvelope.Type.LUCKY);
        // 第一次
        RedEnvelopeDetail redEnvelopeResult = redEnvelopeService.takeBag(redEnvelope.getId(), "jxw");
        assert redEnvelopeResult != null;
        // 第二次
        redEnvelopeResult = redEnvelopeService.takeBag(redEnvelope.getId(), "jxw");
        assert redEnvelopeResult == null;
    }
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值