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

微信截图_20211124140547.png

红包支付成功回调设计

用户支付成功后,系统接收到微信回调接口。

  1. 更新红包支付状态

  2. 二倍均值法生成指定数量红包,并批量入库。 红包算法参考:Java实现4种微信抢红包算法,拿走不谢!

  3. 红包总数入redis,设置红包过期时间24小时

  4. 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

复制代码

获取红包领取记录设计

直接充redis中获取用户领取记录,没有则直接获取数据库并同步至redis。

public RespReceiveListVO receiveList(ReqReceiveListVO data) {

//红包记录redisKey

final String packetNo = data.getPacketNo();

final String redisKey = REDPACKET_LIST_PREFIX + packetNo;

if (!redisTemplate.hasKey(redisKey)) {

RedPacketDetailExample example = new RedPacketDetailExample();

example.createCriteria().andPacketNoEqualTo(packetNo)

.andReceivedEqualTo(1);

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

深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

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

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

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

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024c (备注前端)
img

前端资料汇总

我一直觉得技术面试不是考试,考前背背题,发给你一张考卷,答完交卷等通知。

首先,技术面试是一个 认识自己 的过程,知道自己和外面世界的差距。

更重要的是,技术面试是一个双向了解的过程,要让对方发现你的闪光点,同时也要 试图去找到对方的闪光点,因为他以后可能就是你的同事或者领导,所以,面试官问你有什么问题的时候,不要说没有了,要去试图了解他的工作内容、了解这个团队的氛围。
找工作无非就是看三点:和什么人、做什么事、给多少钱,要给这三者在自己的心里划分一个比例。
最后,祝愿大家在这并不友好的环境下都能找到自己心仪的归宿。

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
img

包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新**

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024c (备注前端)
[外链图片转存中…(img-4xftnl8s-1712863889069)]

前端资料汇总

我一直觉得技术面试不是考试,考前背背题,发给你一张考卷,答完交卷等通知。

首先,技术面试是一个 认识自己 的过程,知道自己和外面世界的差距。

更重要的是,技术面试是一个双向了解的过程,要让对方发现你的闪光点,同时也要 试图去找到对方的闪光点,因为他以后可能就是你的同事或者领导,所以,面试官问你有什么问题的时候,不要说没有了,要去试图了解他的工作内容、了解这个团队的氛围。
找工作无非就是看三点:和什么人、做什么事、给多少钱,要给这三者在自己的心里划分一个比例。
最后,祝愿大家在这并不友好的环境下都能找到自己心仪的归宿。

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
[外链图片转存中…(img-kBn8EnUD-1712863889070)]

为了实现Spring Boot应用程序中的自动刷新,可以使用WebSocket协议。WebSocket协议是一种在Web浏览器和服务器之间进行双向通信的协议。在Spring Boot中,可以使用Spring WebSocket模块来实现WebSocket功能。下面是一个简单的示例,演示如何使用Spring BootWebSocket实现自动刷新: 1.添加依赖 在pom.xml文件中添加以下依赖: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> ``` 2.创建WebSocket配置类 创建一个WebSocket配置类,用于配置WebSocket相关的参数。在这个类中,我们需要实现WebSocketMessageBrokerConfigurer接口,并重写configureMessageBroker()和registerStompEndpoints()方法。 ```java @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry config) { config.enableSimpleBroker("/topic"); config.setApplicationDestinationPrefixes("/app"); } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/ws").withSockJS(); } } ``` 3.创建WebSocket控制器 创建一个WebSocket控制器,用于处理WebSocket请求。在这个控制器中,我们需要使用@MessageMapping注解来处理客户端发送的消息,并使用@SendTo注解将处理结果发送给所有订阅了“/topic/greetings”主题的客户端。 ```java @Controller public class GreetingController { @MessageMapping("/hello") @SendTo("/topic/greetings") public Greeting greeting(HelloMessage message) throws Exception { Thread.sleep(1000); // simulated delay return new Greeting("Hello, " + message.getName() + "!"); } } ``` 4.创建HTML页面 创建一个HTML页面,用于测试WebSocket功能。在这个页面中,我们需要使用JavaScript代码来创建WebSocket连接,并使用STOMP协议来发送和接收消息。 ```html <!DOCTYPE html> <html> <head> <title>Hello WebSocket</title> <script src="https://cdn.jsdelivr.net/sockjs/1.1.4/sockjs.min.js"></script> <script src="https://cdn.jsdelivr.net/stomp.js/2.3.3/stomp.min.js"></script> <script> var stompClient = null; function connect() { var socket = new SockJS('/ws'); stompClient = Stomp.over(socket); stompClient.connect({}, function(frame) { console.log('Connected: ' + frame); stompClient.subscribe('/topic/greetings', function(greeting) { showGreeting(JSON.parse(greeting.body).content); }); }); } function disconnect() { if (stompClient !== null) { stompClient.disconnect(); } console.log("Disconnected"); } function sendName() { var name = document.getElementById('name').value; stompClient.send("/app/hello", {}, JSON.stringify({ 'name': name })); } function showGreeting(message) { document.getElementById('greetings').innerHTML += "<tr><td>" + message + "</td></tr>"; } </script> </head> <body> <div> <label for="name">Name:</label> <input type="text" id="name" /> <button onclick="sendName()">Send</button> <button onclick="disconnect()">Disconnect</button> </div> <table> <thead> <tr> <th>Greetings</th> </tr> </thead> <tbody id="greetings"> </tbody> </table> <script> connect(); </script> </body> </html> ``` 5.运行应用程序 运行Spring Boot应用程序,并在浏览器中打开HTML页面。在页面中输入名称并单击“Send”按钮,应该会看到一个新的问候语出现在页面上。如果您更改了控制器中的问候语消息,页面上的问候语也会相应更改。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值