“This is the 7th day of my participation in the Gwen Challenge in November. Check out the details: The Last Gwen Challenge in 2021.”

An overview of the

How to realize the function of snatching red packets as a typical high concurrency scenario?

Source code address: gitee.com/tech-famer/…

Analysis of the

According to the function of snatching red envelopes on wechat, snatching red envelopes can be divided into the following steps:

  1. Send a red envelope; Mainly fill in the red envelope information, generate red envelope records
  2. Red envelope payment callback; After the user successfully sends the red envelope payment, the user will receive the callback of wechat Pay’s successful payment and generate the specified number of red envelopes.
  3. Grab a red envelope; Users grab red envelopes simultaneously.
  4. Tear open red envelope; Record user snatching red envelope record, transfer snatching red envelope amount.

Results show

The project uses sessionId to simulate the user. The example opens two browser Windows to simulate two users.

Design and development

Table structure design

The red packets are recorded in the Redpacket table, and the details of the user receiving the red packets are recorded in the Redpacket_detail table.

CREATE DATABASE `redpacket`; use `redpacket`; CREATE TABLE 'redpacket'. 'redpacket' (' id 'bigint(20) NOT NULL AUTO_INCREMENT COMMENT' id ', 'packet_no' varchar(32) NOT NULL COMMENT 'order number ',' amount 'decimal(5,2) NOT NULL COMMENT' Max. 10000.00 yuan ', 'num' int(11) NOT NULL COMMENT 'red packets ',' order_status' int(4) NOT NULL DEFAULT '0' COMMENT 'order status: ', 'pay_seq' varchar(32) DEFAULT NULL COMMENT 'iD ', 'create_time' datetime NOT NULL COMMENT 'create time ',' user_id 'varchar(32) NOT NULL COMMENT' create time ', 'update_time' datetime NOT NULL COMMENT 'update time ',' pay_time 'datetime DEFAULT NULL COMMENT' update time ', PRIMARY KEY (' id ')) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT=' CREATE TABLE 'redpacket'. 'redpacket_detail' (' id 'bigint(20) NOT NULL AUTO_INCREMENT COMMENT' id ', 'packet_id' bigint(20) NOT NULL COMMENT 'red decimal ',' amount 'decimal(5,2) NOT NULL COMMENT' red decimal ', 'received' int(1) NOT NULL DEFAULT '0' COMMENT ', 'create_time' datetime NOT NULL COMMENT 'create_time ', 'update_time' datetime NOT NULL COMMENT 'update_time ',' user_id 'varchar(32) DEFAULT NULL COMMENT' update_time ', 'packet_no' varchar(32) NOT NULL, PRIMARY KEY (' id ') ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT=' InnoDB ';Copy the code

Red Envelope Design

Users need to fill in the amount of red envelopes, the number of red envelopes, remarks information, etc., to generate red envelope records, wechat cashier order, return to the user to pay.

public RedPacket generateRedPacket(ReqSendRedPacketsVO data,String userId) { final BigDecimal amount = data.getAmount();  Final Integer num = data.getnum (); Final RedPacket RedPacket = new RedPacket(); redPacket.setPacketNo(UUID.randomUUID().toString().replace("-", "")); redPacket.setAmount(amount); redPacket.setNum(num); redPacket.setUserId(userId); Date now = new Date(); redPacket.setCreateTime(now); redPacket.setUpdateTime(now); int i = redPacketMapper.insertSelective(redPacket); if (i ! = 1) {throw new ServiceException(" error ", ExceptionType.SYS_ERR); String paySeq = uuid.randomuuid ().toString().replace("-", ""); Redpacket.setorderstatus (1); redpacket.setorderStatus (1); // redpacket.setPayseq (paySeq); i = redPacketMapper.updateByPrimaryKeySelective(redPacket); if (i ! = 1) {throw new ServiceException(" error ", ExceptionType.SYS_ERR); } return redPacket; }Copy the code

Red envelope payment successfully callback design

After the user pays successfully, the system receives the wechat callback interface.

  1. Update red envelope payment status
  2. Double means method to generate a specified number of red envelopes, and batch storage. Red envelope algorithm reference: Java to achieve 4 kinds of wechat red envelope algorithm, take away no thanks!
  3. Enter the total amount of red packets into Redis and set the expiration time of red packets to 24 hours
  4. Websocket notifies online users of the receipt of new red packets
@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(" order status update failed ", ExceptionType. } 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<BigDecimal> 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(" failed to generate red packet ", 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<BigDecimal> subList = detailList.subList(fromIndex, toIndex); i = detailMapper.batchInsert(subList, redPacket); if (subList.size() ! = I) {throw new ServiceException(" failed to generate red packet ", 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"; Final Long execute = redistemplate. execute(new DefaultRedisScript<>(lua, long.class), Arrays.asList(redisKey), size, 3600 * 24); if (execute ! = 1L) {throw new ServiceException(" failed to generate red packet ", ExceptionType.SYS_ERR); } / / websocket notice online users receive new red envelope websocket sendMessageToUser (userId, JSONObject toJSONString (redPacket)); }} /** * @param amount * @param num * @return Random number */ private List<BigDecimal> getRedPacketDetail(BigDecimal amount, Integer num) { List<BigDecimal> 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 {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; }Copy the code

After the page is loaded successfully, the websocket is initialized, the generation of new red packets at the back end is monitored successfully, and the red packets are dynamically added to the chat window.

$(function (){ var websocket; If ('WebSocket' in window) {console.log(" this browser supports WebSocket "); Websocket = new websocket (ws: / / "127.0.0.1:8082 / websocket / ${. The session id}"); } else if('MozWebSocket' in window) {alert(" this browser only supports MozWebSocket"); } else {alert(" this browser only supports SockJS"); } websocket.onopen = function(evnt) {console.log(" Link server successfully!" )}; 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(" Disconnected from server!" )}});Copy the code

Grab red envelope design

Red packet snatching design high concurrency, local single machine project, through atomic Integer control red packet snatching interface concurrency limit is 20,

private AtomicInteger receiveCount = new AtomicInteger(0); @PostMapping("/receive") public CommonJsonResponse receiveOne(@Validated @RequestBody CommonJsonRequest<ReqReceiveRedPacketVO> vo) { Integer num = null; If (receivecount.get () > 20) {return new CommonJsonResponse("9999", "too fast "); } 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(); }}}Copy the code

For the user who has not received the red envelope, if the red envelope has not expired and there is still some red envelope left, the red envelope is successfully grabbed, and the red envelope is successfully marked into Redis, and the expiration time of the red envelope is set to 5 seconds.

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 "Red envelope has expired "; } 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<RedPacketDetail> details = detailMapper.selectByExample(example); if (! IsEmpty ()) {return "The red envelope is already empty "; } final String receiveKey = REDPACKET_RECEIVE_PREFIX + redPacketNo + ":" + data.getUserId(); 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"; Final Long execute = redistemplate. execute(new DefaultRedisScript<>(lua, long.class), Arrays.asList(receiveKey), 1, 5); if (execute ! = 1L) {return "too fast "; } return ""; }Copy the code

Red Envelope Design

When the user successfully grabs the red envelope and the red envelope has not expired, the user will get a red envelope from the database and write the receiving record into Redis for checking the expiration time of 48 hours.

@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(" No, packet:{},user:{}", packetNo, userId); Throw new ServiceException(" The red packet flew away ", exceptionType.sys_err); } redisTemplate.delete(receiveKey); if (! Redistemplate.haskey (redisKey)) {log.info(" Red packet expired, packet:{}", packetNo); Throw new ServiceException(" The red packet flew away ", exceptionType.sys_err); } num = redisTemplate.opsForValue().increment(redisKey, -1); If (num < 0L) {log.info(" packet:{}", packetNo); Throw new ServiceException(" The red packet flew away ", exceptionType.sys_err); } final int i = detailMapper.receiveOne(packetId, packetNo, userId); if (i ! = 1) {log.info(" packet:{}", packetNo); Throw new ServiceException(" The red packet flew away ", exceptionType.sys_err); } RedPacketDetailExample example = new RedPacketDetailExample(); example.createCriteria().andPacketIdEqualTo(packetId) .andReceivedEqualTo(1) .andUserIdEqualTo(userId); final List<RedPacketDetail> details = detailMapper.selectByExample(example); if (details.size() ! = 1) {log.info(" packet:{}, user:{}", packetNo, userId); Throw new ServiceException(" The red packet flew away ", exceptionType.sys_err); Log.info (" grab the red packet amount {}, 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(" error opening red envelope ", e); Throw new ServiceException(" The red packet flew away ", exceptionType.sys_err); }}Copy the code

Detailmapper. receiveOne(packetId, packetNo, userId); SQL is as follows: the specified red envelope will be recorded as the red envelope has not been received by the current user, if the update is successful, it indicates that the red envelope has been received successfully, otherwise, it fails to receive.

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
Copy the code

Get red envelope collection record design

The user claim record can be obtained directly in REDis, and the database can be directly obtained and synchronized to Redis if there is no such record.

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); final List<RedPacketDetail> list = detailMapper.selectByExample(example); redisTemplate.opsForList().leftPushAll(redisKey, list); redisTemplate.expire(redisKey, 24, TimeUnit.HOURS); } List retList = redisTemplate.opsForList().range(redisKey, 0, -1); final Object collect = retList.stream().map(item -> { final JSONObject packetDetail = (JSONObject) item; return ReceiveRecordVO.builder() .amount(packetDetail.getBigDecimal("amount")) .receiveTime(packetDetail.getDate("updateTime")) .userId(packetDetail.getString("userId")) .packetId(packetDetail.getLong("redpacketId")) .packetNo(packetDetail.getString("redpacketNo")) .build(); }).collect(Collectors.toList()); return RespReceiveListVO.builder().list((List) collect).build(); }Copy the code

Jmeter concurrently tests the interface of grabbing red packets and checking red packets

Set the Jmeter parameter to make 50 concurrent requests and grab 11 red packets in 1 second. It can be seen that the previous requests are all successful, while some of the requests reached the maximum concurrency and were intercepted, and all the requests after grabbing red packets failed.