这里写目录标题
前言
如今各种直播、裂变盛行,抢红包不可以少。
本文讲解如何通过Redis lua实现抢红包逻辑,包含Redission
、Spring-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;
}
}