springboot+websocket实现并发抢红包功能

  1. websocket通知在线用户收到新的红包

@Transactional(rollbackFor = Exception.class)

public void dealAfterOrderPayCallback(String userId,ReqOrderPayCallbackVO data) {

RedPacketExample example = new RedPacketExample();

final String packetNo = data.getPacketNo();

final String paySeq = data.getPaySeq();

final Integer payStatus = data.getPayStatus();

example.createCriteria().andPacketNoEqualTo(packetNo)

.andPaySeqEqualTo(paySeq)

.andOrderStatusEqualTo(1);//待支付状态

//更新订单支付状态

Date now = new Date();

RedPacket updateRedPacket = new RedPacket();

updateRedPacket.setOrderStatus(payStatus);

updateRedPacket.setUpdateTime(now);

updateRedPacket.setPayTime(now);

int i = redPacketMapper.updateByExampleSelective(updateRedPacket, example);

if (i != 1) {

throw new ServiceException(“订单状态更新失败”, ExceptionType.SYS_ERR);

}

if (payStatus == 2) {

RedPacketExample query = new RedPacketExample();

query.createCriteria().andPacketNoEqualTo(packetNo)

.andPaySeqEqualTo(paySeq)

.andOrderStatusEqualTo(2);

final RedPacket redPacket = redPacketMapper.selectByExample(query).get(0);

final List detailList = getRedPacketDetail(redPacket.getAmount(), redPacket.getNum());

final int size = detailList.size();

if (size <= 100) {

i = detailMapper.batchInsert(detailList, redPacket);

if (size != i) {

throw new ServiceException(“生成红包失败”, ExceptionType.SYS_ERR);

}

} else {

int times = size % 100 == 0 ? size / 100 : (size / 100 + 1);

for (int j = 0; j < times; j++) {

int fromIndex = 100 * j;

int toIndex = 100 * (j + 1) - 1;

if (toIndex > size - 1) {

toIndex = size - 1;

}

final List subList = detailList.subList(fromIndex, toIndex);

i = detailMapper.batchInsert(subList, redPacket);

if (subList.size() != i) {

throw new ServiceException(“生成红包失败”, ExceptionType.SYS_ERR);

}

}

}

final String redisKey = REDPACKET_NUM_PREFIX + redPacket.getPacketNo();

String lua = “local i = redis.call(‘setnx’,KEYS[1],ARGV[1])\r\n” +

“if i == 1 then \r\n” +

" local j = redis.call(‘expire’,KEYS[1],ARGV[2])\r\n" +

“end \r\n” +

“return i”;

//优化成lua脚本

final Long execute = redisTemplate.execute(new DefaultRedisScript<>(lua, Long.class), Arrays.asList(redisKey), size, 3600 * 24);

if (execute != 1L) {

throw new ServiceException(“生成红包失败”, ExceptionType.SYS_ERR);

}

//websocket通知在线用户收到新的红包

Websocket.sendMessageToUser(userId, JSONObject.toJSONString(redPacket));

}

}

/**

  • 红包随机算法

  • @param amount 红包金额

  • @param num 红包数量

  • @return 随机红包集合

*/

private List getRedPacketDetail(BigDecimal amount, Integer num) {

List redPacketsList = new ArrayList<>(num);

//最小红包金额

final BigDecimal min = new BigDecimal(“0.01”);

//最少需要红包金额

final BigDecimal bigNum = new BigDecimal(num);

final BigDecimal atLastAmount = min.multiply(bigNum);

//出去最少红包金额后剩余金额

BigDecimal remain = amount.subtract(atLastAmount);

if (remain.compareTo(BigDecimal.ZERO) == 0) {

for (int i = 0; i < num; i++) {

redPacketsList.add(min);

}

return redPacketsList;

}

final Random random = new Random();

final BigDecimal hundred = new BigDecimal(“100”);

final BigDecimal two = new BigDecimal(“2”);

BigDecimal redPacket;

for (int i = 0; i < num; i++) {

if (i == num - 1) {

redPacket = remain;

} else {

//100内随机获得的整数

final int rand = random.nextInt(100);

redPacket = new BigDecimal(rand).multiply(remain.multiply(two).divide(bigNum.subtract(new BigDecimal(i)), 2, RoundingMode.CEILING)).divide(hundred, 2, RoundingMode.FLOOR);

}

if (remain.compareTo(redPacket) > 0) {

remain = remain.subtract(redPacket);

} else {

remain = BigDecimal.ZERO;

}

redPacketsList.add(min.add(redPacket));

}

return redPacketsList;

}

复制代码

页面加载成功后初始化websocket,监听后端新红包生成成功,动态添加红包到聊天窗口。

$(function (){

var websocket;

if(‘WebSocket’ in window) {

console.log(“此浏览器支持websocket”);

websocket = new WebSocket(“ws://127.0.0.1:8082/websocket/${session.id}”);

} else if(‘MozWebSocket’ in window) {

alert(“此浏览器只支持MozWebSocket”);

} else {

alert(“此浏览器只支持SockJS”);

}

websocket.onopen = function(evnt) {

console.log(“链接服务器成功!”)

};

websocket.onmessage = function(evnt) {

console.log(evnt.data);

var json = eval(‘(’+evnt.data+ ‘)’);

obj.addPacket(json.id,json.packetNo,json.userId)

};

websocket.onerror = function(evnt) {};

websocket.onclose = function(evnt) {

console.log(“与服务器断开了链接!”)

}

});

复制代码

抢红包设计

抢红包设计高并发,本地单机项目,通过原子Integer控制抢红包接口并发限制为20,

private AtomicInteger receiveCount = new AtomicInteger(0);

@PostMapping(“/receive”)

public CommonJsonResponse receiveOne(@Validated @RequestBody CommonJsonRequest vo) {

Integer num = null;

try {

//控制并发不要超过20

if (receiveCount.get() > 20) {

return new CommonJsonResponse(“9999”, “太快了”);

}

num = receiveCount.incrementAndGet();

final String s = orderService.receiveOne(vo.getData());

return StringUtils.isEmpty(s) ? CommonJsonResponse.ok() : new CommonJsonResponse(“9999”, s);

} finally {

if (num != null) {

receiveCount.decrementAndGet();

}

}

}

复制代码

对于没有领取过该红包的用户,在红包没有过期且红包还有剩余的情况下,抢红包成功,记录成功标识入redis,设置标识过期时间为5秒。

public String receiveOne(ReqReceiveRedPacketVO data) {

final Long redPacketId = data.getPacketId();

final String redPacketNo = data.getPacketNo();

final String redisKey = REDPACKET_NUM_PREFIX + redPacketNo;

if (!redisTemplate.hasKey(redisKey)) {

return “红包已经过期”;

}

final Integer num = (Integer) redisTemplate.opsForValue().get(redisKey);

if (num <= 0) {

return “红包已抢完”;

}

RedPacketDetailExample example = new RedPacketDetailExample();

example.createCriteria().andPacketIdEqualTo(redPacketId)

.andReceivedEqualTo(1)

.andUserIdEqualTo(data.getUserId());

final List details = detailMapper.selectByExample(example);

if (!details.isEmpty()) {

return “该红包已经领取过了”;

}

final String receiveKey = REDPACKET_RECEIVE_PREFIX + redPacketNo + “:” + data.getUserId();

//优化成lua脚本

String lua = “local i = redis.call(‘setnx’,KEYS[1],ARGV[1])\r\n” +

“if i == 1 then \r\n” +

" local j = redis.call(‘expire’,KEYS[1],ARGV[2])\r\n" +

“end \r\n” +

“return i”;

//优化成lua脚本

final Long execute = redisTemplate.execute(new DefaultRedisScript<>(lua, Long.class), Arrays.asList(receiveKey), 1, 5);

if (execute != 1L) {

return “太快了”;

}

return “”;

}

复制代码

拆红包设计

在用户抢红包成功标识未过期的状态下,且红包未过期红包未领完时,从数据库中领取一个红包,领取成功将领取记录写入redis以供查询过期时间为48小时。

@Transactional(rollbackFor = Exception.class)

public String openRedPacket(ReqReceiveRedPacketVO data) {

final Long packetId = data.getPacketId();

final String packetNo = data.getPacketNo();

final String userId = data.getUserId();

final String redisKey = REDPACKET_NUM_PREFIX + packetNo;

Long num = null;

try {

final String receiveKey = REDPACKET_RECEIVE_PREFIX + packetNo + “:” + userId;

if (!redisTemplate.hasKey(receiveKey)) {

log.info(“未获取到红包资格,packet:{},user:{}”, packetNo, userId);

throw new ServiceException(“红包飞走了”, ExceptionType.SYS_ERR);

}

redisTemplate.delete(receiveKey);

if (!redisTemplate.hasKey(redisKey)) {

log.info(“红包过期了,packet:{}”, packetNo);

throw new ServiceException(“红包飞走了”, ExceptionType.SYS_ERR);

}

num = redisTemplate.opsForValue().increment(redisKey, -1);

if (num < 0L) {

log.info(“红包领完了,packet:{}”, packetNo);

throw new ServiceException(“红包飞走了”, ExceptionType.SYS_ERR);

}

final int i = detailMapper.receiveOne(packetId, packetNo, userId);

if (i != 1) {

log.info(“红包真的领完了,packet:{}”, packetNo);

throw new ServiceException(“红包飞走了”, ExceptionType.SYS_ERR);

}

RedPacketDetailExample example = new RedPacketDetailExample();

example.createCriteria().andPacketIdEqualTo(packetId)

.andReceivedEqualTo(1)

.andUserIdEqualTo(userId);

final List details = detailMapper.selectByExample(example);

if (details.size() != 1) {

log.info(“已经领取过了,packet:{},user:{}”, packetNo, userId);

throw new ServiceException(“红包飞走了”, ExceptionType.SYS_ERR);

}

//处理加款

log.info(“抢到红包金额{},packet:{},user:{}”, details.get(0).getAmount(), packetNo, userId);

final String listKey = REDPACKET_LIST_PREFIX + packetNo;

redisTemplate.opsForList().leftPush(listKey,details.get(0));

redisTemplate.expire(redisKey, 48, TimeUnit.HOURS);

return “” + details.get(0).getAmount();

} catch (Exception e) {

if (num != null) {

redisTemplate.opsForValue().increment(redisKey, 1L);

}

log.warn(“打开红包异常”, e);

throw new ServiceException(“红包飞走了”, ExceptionType.SYS_ERR);

}

}

复制代码

其中 detailMapper.receiveOne(packetId, packetNo, userId); sql如下,将指定红包记录下未领取的红包更新一条未当前用户已经领取,若成功更新一条则表示领取成功,否则领取失败。

update redpacket_detail d

set received = 1,update_time = now(),user_id = #{userId,jdbcType=VARCHAR}

where received = 0

and packet_id = #{packetId,jdbcType=BIGINT}

and packet_no = #{packetNo,jdbcType=VARCHAR}

and user_id is null

limit 1

复制代码

获取红包领取记录设计

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。**

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-xcrhKBfx-1715600291924)]

[外链图片转存中…(img-Xri3LdTq-1715600291924)]

[外链图片转存中…(img-GpHkrHhS-1715600291925)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

  • 26
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值