Backend Development 22 min read

Implementing a High‑Concurrency Red Packet System with Java, MySQL, MyBatis, and Spring

This article demonstrates how to build a high‑concurrency red‑packet (抢红包) system using Java backend technologies—including MySQL table design, POJOs, MyBatis DAOs, Spring services with transaction management, and a web layer, and discusses performance testing, data consistency issues, and solutions such as pessimistic and optimistic locking.

Java Captain
Java Captain
Java Captain
Implementing a High‑Concurrency Red Packet System with Java, MySQL, MyBatis, and Spring

The article introduces a high‑concurrency scenario (e.g., e‑commerce flash sales, Spring Festival ticket grabbing, WeChat/QQ red‑packet snatching) and explains why system optimization and stability are critical.

Red‑packet case : Simulate a 200,000 CNY red packet split into 20,000 small packets, with 30,000 concurrent users attempting to grab them, exposing potential over‑issuance and consistency problems.

Database schema (MySQL 5.7) for the red‑packet and user‑red‑packet tables:

/*==============================================================*/
/* Table: 红包表 */
/*==============================================================*/
create table T_RED_PACKET (
    id int(12) not null auto_increment comment '红包编号',
    user_id int(12) not null comment '发红包的用户id',
    amount decimal(16,2) not null comment '红包金额',
    send_date timestamp not null default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP comment '发红包日期',
    total int(12) not null comment '红包总数',
    unit_amount decimal(12) not null comment '单个红包的金额',
    stock int(12) not null comment '红包剩余个数',
    version int(12) default 0 not null comment '版本(为后续扩展用)',
    note varchar(256) null comment '备注',
    primary key clustered (id)
);

/*==============================================================*/
/* Table: 用户抢红包表 */
/*==============================================================*/
create table T_USER_RED_PACKET (
    id int(12) not null auto_increment comment '用户抢到的红包id',
    red_packet_id int(12) not null comment '红包id',
    user_id int(12) not null comment '抢红包用户的id',
    amount decimal(16,2) not null comment '抢到的红包金额',
    grab_time timestamp not null default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP comment '抢红包时间',
    note varchar(256) null comment '备注',
    primary key clustered (id)
);

Sample data insertion creates one large red packet with 20,000 small packets of 10 CNY each:

insert into T_RED_PACKET(user_id, amount, send_date, total, unit_amount, stock, note)
values (1, 200000.00, now(), 20000, 10.00, 20000, '20万元金额,2万个小红包,每个10元');
commit;

Domain objects (POJOs) map the tables to Java classes:

package com.artisan.redpacket.pojo;

public class RedPacket implements Serializable {
    private Long id;
    private Long userId;
    private Double amount;
    private Timestamp sendDate;
    private Integer total;
    private Double unitAmount;
    private Integer stock;
    private Integer version;
    private String note;
    // getters/setters omitted
}

public class UserRedPacket implements Serializable {
    private Long id;
    private Long redPacketId;
    private Long userId;
    private Double amount;
    private Timestamp grabTime;
    private String note;
    // getters/setters omitted
}

DAO layer uses MyBatis interfaces and XML mappers. Example DAO interface:

package com.artisan.redpacket.dao;

import org.springframework.stereotype.Repository;
import com.artisan.redpacket.pojo.RedPacket;

@Repository
public interface RedPacketDao {
    RedPacket getRedPacket(Long id);
    int decreaseRedPacket(Long id);
}

Corresponding mapper XML for getRedPacket and decreaseRedPacket :

<mapper namespace="com.artisan.redpacket.dao.RedPacketDao">
    <!-- 查询红包具体信息 -->
    <select id="getRedPacket" parameterType="long" resultType="com.artisan.redpacket.pojo.RedPacket">
        select id, user_id as userId, amount, send_date as sendDate, total,
               unit_amount as unitAmount, stock, version, note
        from T_RED_PACKET where id = #{id}
    </select>
    <!-- 扣减抢红包库存 -->
    <update id="decreaseRedPacket">
        update T_RED_PACKET set stock = stock - 1 where id = #{id}
    </update>
</mapper>

Similar DAO and mapper are defined for UserRedPacketDao with an insert statement that uses useGeneratedKeys to obtain the generated primary key.

Service layer provides business logic. The RedPacketService simply forwards DAO calls, while UserRedPacketServiceImpl implements the core grabbing logic with transaction management:

@Service
public class UserRedPacketServiceImpl implements UserRedPacketService {
    @Autowired
    private UserRedPacketDao userRedPacketDao;
    @Autowired
    private RedPacketDao redPacketDao;
    private static final int FAILED = 0;

    @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
    public int grapRedPacket(Long redPacketId, Long userId) {
        RedPacket redPacket = redPacketDao.getRedPacket(redPacketId);
        int left = redPacket.getStock();
        if (left > 0) {
            redPacketDao.decreaseRedPacket(redPacketId);
            UserRedPacket urp = new UserRedPacket();
            urp.setRedPacketId(redPacketId);
            urp.setUserId(userId);
            urp.setAmount(redPacket.getUnitAmount());
            urp.setNote("redpacket- " + redPacketId);
            return userRedPacketDao.grapRedPacket(urp);
        }
        return FAILED;
    }
}

Spring configuration is split into two Java config classes. RootConfig sets up the data source, MyBatis SqlSessionFactoryBean , mapper scanning, and enables annotation‑driven transaction management:

@Configuration
@ComponentScan(value="com.*", [email protected](type=FilterType.ANNOTATION, value={Service.class}))
@EnableTransactionManagement
public class RootConfig implements TransactionManagementConfigurer {
    @Bean(name="dataSource")
    public DataSource initDataSource() { /* creates BasicDataSource from jdbc.properties */ }
    @Bean(name="sqlSessionFactory")
    public SqlSessionFactoryBean initSqlSessionFactory() { /* sets MyBatis config location */ }
    @Bean
    public MapperScannerConfigurer initMapperScannerConfigurer() { /* scans "com.*" for @Repository */ }
    @Bean(name="annotationDrivenTransactionManager")
    public PlatformTransactionManager annotationDrivenTransactionManager() {
        return new DataSourceTransactionManager(initDataSource());
    }
}

WebConfig configures Spring MVC, a view resolver, and a custom RequestMappingHandlerAdapter that registers a MappingJackson2HttpMessageConverter to support JSON responses:

@Configuration
@ComponentScan(value="com.*", [email protected](type=FilterType.ANNOTATION, value=Controller.class))
@EnableWebMvc
public class WebConfig {
    @Bean(name="internalResourceViewResolver")
    public ViewResolver initViewResolver() {
        InternalResourceViewResolver vr = new InternalResourceViewResolver();
        vr.setPrefix("/WEB-INF/jsp/");
        vr.setSuffix(".jsp");
        return vr;
    }
    @Bean(name="requestMappingHandlerAdapter")
    public HandlerAdapter initRequestMappingHandlerAdapter() {
        RequestMappingHandlerAdapter rmha = new RequestMappingHandlerAdapter();
        MappingJackson2HttpMessageConverter json = new MappingJackson2HttpMessageConverter();
        json.setSupportedMediaTypes(Collections.singletonList(MediaType.APPLICATION_JSON_UTF8));
        rmha.getMessageConverters().add(json);
        return rmha;
    }
}

The WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer to bootstrap the Spring context and configure multipart upload settings.

Controller exposes a JSON endpoint for grabbing a red packet:

@Controller
@RequestMapping("/userRedPacket")
public class UserRedPacketController {
    @Autowired
    private UserRedPacketService userRedPacketService;

    @RequestMapping(value="/grapRedPacket")
    @ResponseBody
    public Map
grapRedPacket(Long redPacketId, Long userId) {
        int result = userRedPacketService.grapRedPacket(redPacketId, userId);
        Map
ret = new HashMap<>();
        boolean success = result > 0;
        ret.put("success", success);
        ret.put("message", success ? "抢红包成功" : "抢红包失败");
        return ret;
    }
}

The JSP view grap.jsp uses jQuery to fire 30,000 asynchronous POST requests to simulate 30,000 users concurrently grabbing the same red packet (id = 1):

<script src="https://code.jquery.com/jquery-3.2.0.js"></script>
<script>
$(document).ready(function(){
    var max = 30000;
    for (var i = 1; i <= max; i++) {
        $.post({
            url: "./userRedPacket/grapRedPacket.do?redPacketId=1&userId=" + i,
            success: function(result) {}
        });
    }
});
</script>

Running the test on a modest MySQL instance (1 GB RAM) produced two key observations:

Data inconsistency: the total amount issued became 200,020 CNY with 20,002 packets, and the remaining stock turned negative (‑2), indicating over‑issuance.

Performance: the whole process took about 190 seconds to complete 20,002 successful grabs.

To resolve the over‑issuance problem, the article suggests three approaches: a pessimistic lock (SELECT … FOR UPDATE), an optimistic lock using a version field, and a Redis‑based atomic operation with Lua scripting.

All source code is available at https://github.com/yangshangwei/ssm_redpacket .

JavatransactionconcurrencySpringMySQLMyBatisRed Packet
Java Captain
Written by

Java Captain

Focused on Java technologies: SSM, the Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading; occasionally covers DevOps tools like Jenkins, Nexus, Docker, ELK; shares practical tech insights and is dedicated to full‑stack Java development.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.