ssm环境搭建和红包超发问题

逆向工程生成mapper.java、mapper.xml、po

使用从逆向工程说明处的模板项目

GeneratorSqlmap.java



import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import org.mybatis.generator.api.MyBatisGenerator;
import org.mybatis.generator.config.Configuration;
import org.mybatis.generator.config.xml.ConfigurationParser;
import org.mybatis.generator.exception.XMLParserException;
import org.mybatis.generator.internal.DefaultShellCallback;

public class GeneratorSqlmap {

    public void generator() throws Exception{

        List warnings = new ArrayList();
        boolean overwrite = true;
        //指定 逆向工程配置文件
        File configFile = new File("generatorConfig.xml"); 
        ConfigurationParser cp = new ConfigurationParser(warnings);
        Configuration config = cp.parseConfiguration(configFile);
        DefaultShellCallback callback = new DefaultShellCallback(overwrite);
        MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config,
                callback, warnings);
        myBatisGenerator.generate(null);

    } 
    public static void main(String[] args) throws Exception {
        try {
            GeneratorSqlmap generatorSqlmap = new GeneratorSqlmap();
            generatorSqlmap.generator();
        } catch (Exception e) {
            e.printStackTrace();
        }
        
    }

}

generatorConfig.xml





    
        
            
            
        
        
        
        
        

        
        
            
        

        
        
            
            
            
            
        
        
        
        
            
            
        
        
        
        
            
            
        
        
        
        

运行java代码,将生成的po、mapper拷贝到主项目中,不建议在生成的代码上做操作,需要修改需求可以对生成的po、mapper进行包装。

关于po是否要实现序列化接口

序列化po主要为了实现二级缓存、分布式缓存,为了将缓存数据取出执行反序列化操作,因为二级缓存数据存储介质多种多样,不一定在内存。

参考:

https://www.jianshu.com/writer#/notebooks/39849728/notes/53800305/preview

在我们的抢红包项目中对实时性要求极高,所以不采用二级缓存。所以就不考虑序列化pojo类了。

搭建ssm框架(Dao层):

新建ssm_redpacket项目,导入jar包

使用的jdk为1.8
经过自己测试导入spring4.2.4的jar包正好好使,低版本运行时会有很多找不到对应包的错误。

spring整合mybatis sqlMapConfig

新建源文件夹config-包mybatis

sqlMapConfig.xml





        
         

        
        
            
            
        
        

        
        


(没剩多少内容了,主要部分sqlSessionFactory、mapper扫描都由spring接管)

applicationContext-dao.xml





        
         

        
        
            
            
        
        

        
        


db.properties

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/red_packet
jdbc.username=root
jdbc.password=suntong



log4j.properties

# Global logging configuration
log4j.rootLogger=DEBUG, stdout
# Console output...
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n

在TRedPacketMapper.java中添加两个方法 , 一个是查询红包,另一个是扣减红包库存。
抢红包的逻辑是,先查询红包的信息,看其是否拥有存量可以扣减。如果有存量,那么可以扣减它,否则就不扣减。

package com.redpacket.ssm.mapper;

import com.redpacket.ssm.po.TRedPacket;
import com.redpacket.ssm.po.TRedPacketExample;
import java.util.List;
import org.apache.ibatis.annotations.Param;

public interface TRedPacketMapper {
    int countByExample(TRedPacketExample example);

    int deleteByExample(TRedPacketExample example);

    int deleteByPrimaryKey(Integer id);

    int insert(TRedPacket record);

    int insertSelective(TRedPacket record);

    List selectByExample(TRedPacketExample example);

    //查询红包具体信息已包含
    TRedPacket selectByPrimaryKey(Integer id);

    int updateByExampleSelective(@Param("record") TRedPacket record, @Param("example") TRedPacketExample example);

    int updateByExample(@Param("record") TRedPacket record, @Param("example") TRedPacketExample example);

    int updateByPrimaryKeySelective(TRedPacket record);

    int updateByPrimaryKey(TRedPacket record);
    
    //新增按照id扣减红包库存
    int decreaseRedPacket(Integer id);
}

对应Mapper映射文件


    
        update t_red_packet set stock = stock - 1 where id =
        #{id,jdbcType=INTEGER}
    

(映射太多了,只摘咱们添加的)

TUserRedPacketMapper.java

package com.redpacket.ssm.mapper;

import com.redpacket.ssm.po.TUserRedPacket;
import com.redpacket.ssm.po.TUserRedPacketExample;
import java.util.List;
import org.apache.ibatis.annotations.Param;

public interface TUserRedPacketMapper {

    
    /**
     * 插入抢红包信息.
     * @param userRedPacket ——抢红包信息
     * @return 影响记录数.
     */
    public int grapRedPacket(TUserRedPacket  tUserRedPacket);
}

对应mapper映射文件


    
        insert into T_USER_RED_PACKET( red_packet_id, user_id, amount, grab_time, note)
        values (#{redPacketId}, #{userId}, #{amount}, now(), #{note}) 
    

这里使用了 useGeneratedKeys 和 keyProperty,这就意味着会Mybatis执行完插入语句后,自动将自增长值赋值给对象的属性id。这样就可以拿到插入记录的主键了 , 关于 DAO 层就基本完成了。

web.xml


    
        contextConfigLocation
        /WEB-INF/classes/spring/applicationContext-*.xml
    
    
        org.springframework.web.context.ContextLoaderListener
    

新建两个mapper的测试类测试对应的方法

TRedPacketMapperTest.java

package com.redpacket.ssm.test;

import static org.junit.Assert.*;

import org.junit.Before;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.redpacket.ssm.mapper.TRedPacketMapper;
import com.redpacket.ssm.po.TRedPacket;

public class TRedPacketMapperTest {
    
    private ApplicationContext applicationContext;

    //再setUp中构造spring容器
    @Before
    public void setUp() throws Exception{
        applicationContext = new ClassPathXmlApplicationContext("classpath:spring/applicationContext-dao.xml");
    }

    @Test
    public void testSelectByPrimaryKey() {
        
        TRedPacketMapper tRedPacketMapper = (TRedPacketMapper)applicationContext.getBean("TRedPacketMapper");
        
        TRedPacket tRedPacket = tRedPacketMapper.selectByPrimaryKey(1);
        
        System.out.println(tRedPacket);
    }

    @Test
    public void testDecreaseRedPacket() {
        TRedPacketMapper tRedPacketMapper = (TRedPacketMapper)applicationContext.getBean("TRedPacketMapper");
        
        tRedPacketMapper.decreaseRedPacket(1);
        
    }

}

TUserRedPacketMapperTest.java

package com.redpacket.ssm.test;

import static org.junit.Assert.*;

import java.math.BigDecimal;
import java.util.Date;

import org.junit.Before;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.redpacket.ssm.mapper.TRedPacketMapper;
import com.redpacket.ssm.mapper.TUserRedPacketMapper;
import com.redpacket.ssm.po.TRedPacket;
import com.redpacket.ssm.po.TUserRedPacket;

public class TUserRedPacketMapperTest {
    
    private ApplicationContext applicationContext;

    //再setUp中构造spring容器
    @Before
    public void setUp() throws Exception{
        applicationContext = new ClassPathXmlApplicationContext("classpath:spring/applicationContext-dao.xml");
    }

    @Test
    public void testGrapRedPacket() {
        

        TUserRedPacketMapper tUserRedPacketMapper = (TUserRedPacketMapper)applicationContext.getBean("TUserRedPacketMapper");
        
        TUserRedPacket tUserRedPacket = new TUserRedPacket();
        
        tUserRedPacket.setAmount(new BigDecimal(10.0));
        tUserRedPacket.setGrabTime(new Date());
        tUserRedPacket.setRedPacketId(1);
        tUserRedPacket.setUserId(1);
        
        tUserRedPacketMapper.grapRedPacket(tUserRedPacket);
        
        
    }

}

查询后输出的结果是一个对象地址,如果想增加toString方法最好新建一个包装类,不要在原pojo上做改动,增强以后的拓展性。

增添和修改到数据库总看方便(此处忘记截图)

工程结构

搭建ssm框架(service层)

考虑到项目主要功能是测试高并发情况下如何保证数据安全,所以不用注解了,xml配置更加稳定容易,不易出现意料之外的问题(半注解半xml配置的话要在xml中开启注解配置,全注解的话要继承配置类,全注解并不爽,


    

半xml加注解时在xml中配置这句话,spring就能开始扫描包下所有的注解)

RedPacketService.java

package com.redpacket.ssm.service;

import com.redpacket.ssm.po.TRedPacket;

public interface RedPacketService {

    /**
     * 获取红包
     * @param id——编号
     * @return 红包信息
     */
    public TRedPacket getRedPacket(Integer id);

    /**
     * 扣减红包
     * @param id——编号
     * @return 影响条数.
     */
    public int decreaseRedPacket(Integer id);
}

UserRedPacketService.java

package com.redpacket.ssm.service;

public interface UserRedPacketService {

    /**
     * 保存抢红包信息.
     * @param redPacketId 红包编号
     * @param userId 抢红包用户编号
     * @return 影响记录数.
     */
    public int grapRedPacket(Integer redPacketId, Integer userId);

}

RedPacketServiceImpl.java

package com.redpacket.ssm.service.impl;

import org.springframework.beans.factory.annotation.Autowired;

import com.redpacket.ssm.mapper.TRedPacketMapper;
import com.redpacket.ssm.po.TRedPacket;
import com.redpacket.ssm.service.RedPacketService;

public class RedPacketServiceImpl implements RedPacketService {

    @Autowired
    private TRedPacketMapper tRedPacketMapper;

    @Override
    public TRedPacket getRedPacket(Integer id) {
        return tRedPacketMapper.selectByPrimaryKey(id);
    }

    @Override
    public int decreaseRedPacket(Integer id) {
        return tRedPacketMapper.decreaseRedPacket(id);
    }


}

UserRedPacketServiceImpl.java

package com.redpacket.ssm.service.impl;

import java.math.BigDecimal;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;

import com.redpacket.ssm.mapper.TRedPacketMapper;
import com.redpacket.ssm.mapper.TUserRedPacketMapper;
import com.redpacket.ssm.po.TRedPacket;
import com.redpacket.ssm.po.TUserRedPacket;
import com.redpacket.ssm.service.UserRedPacketService;

public class UserRedPacketServiceImpl implements UserRedPacketService {
        @Autowired
        private TRedPacketMapper tRedPacketMapper;

        @Autowired
        private TUserRedPacketMapper tUserRedPacketMapper;

        // 失败
        final int FAILED = 0;

        @Override
        public int grapRedPacket(Integer redPacketId, Integer userId) {
            // 获取红包信息
            TRedPacket tRedPacket = tRedPacketMapper.selectByPrimaryKey(redPacketId);
            int leftRedPacket = tRedPacket.getStock();
            // 当前小红包库存大于0
            if (leftRedPacket > 0) {
                tRedPacketMapper.decreaseRedPacket(redPacketId);
                // logger.info("剩余Stock数量:{}", leftRedPacket);
                // 生成抢红包信息
                TUserRedPacket tUserRedPacket = new TUserRedPacket();
                tUserRedPacket.setRedPacketId(redPacketId);
                tUserRedPacket.setUserId(userId);
                tUserRedPacket.setAmount(new BigDecimal(tRedPacket.getUnitAmount()));
                tUserRedPacket.setNote("redpacket- " + redPacketId);
                // 插入抢红包信息
                int result = tUserRedPacketMapper.grapRedPacket(tUserRedPacket);
                return result;
            }
            // logger.info("没有红包啦.....剩余Stock数量:{}", leftRedPacket);
            // 失败返回
            return FAILED;
        }

}

applicationContext-service.xml






applicationContext-transaction.xml





    
    




    
        
        
        
        
        
        
        
        
        
        
    




    



事务配置参考

https://www.jianshu.com/writer#/notebooks/39651087/notes/53434246/preview

单元测试

RedPacketServiceTest.java

package com.redpacket.ssm.test;


import org.junit.Before;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.redpacket.ssm.po.TRedPacket;
import com.redpacket.ssm.service.impl.RedPacketServiceImpl;

public class RedPacketServiceTest {
    
    private ApplicationContext applicationContext;

    @Before
    public void setUp() throws Exception{
        applicationContext = new ClassPathXmlApplicationContext("classpath:spring/applicationContext-service.xml","classpath:spring/applicationContext-dao.xml");
    }

    
    @Test
    public void testGetRedPacket() {
        
        RedPacketServiceImpl redPacketServiceImpl = (RedPacketServiceImpl)applicationContext.getBean("redPacketService");
        
        TRedPacket tRedPacket = redPacketServiceImpl.getRedPacket(1);
        
        System.out.println(tRedPacket);
    }

    @Test
    public void testDecreaseRedPacket() {
        RedPacketServiceImpl redPacketServiceImpl = (RedPacketServiceImpl)applicationContext.getBean("redPacketService");
        
        int a = redPacketServiceImpl.decreaseRedPacket(1);
        
        System.out.println(a);
    }

}

测试方法testDecreaseRedPacket()

测试方法testGetRedPacket()

UserRedPacketServiceImplTest.java

package com.redpacket.ssm.test;

import static org.junit.Assert.*;

import java.math.BigDecimal;
import java.util.Date;

import org.junit.Before;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.redpacket.ssm.service.impl.UserRedPacketServiceImpl;

public class UserRedPacketServiceImplTest {

    private ApplicationContext applicationContext;

    @Before
    public void setUp() throws Exception{
        applicationContext = new ClassPathXmlApplicationContext("classpath:spring/applicationContext-service.xml","classpath:spring/applicationContext-dao.xml");
    }

    
    
    @Test
    public void testGrapRedPacket() {
        UserRedPacketServiceImpl userRedPacketServiceImpl = (UserRedPacketServiceImpl)applicationContext.getBean("userRedPacketService");
        
        int a = userRedPacketServiceImpl.grapRedPacket(1, 2);
        System.out.println(a);
    }

}

绿了绿了!!!绿了就好使了

搭建ssm框架(Controller层):

UserRedPacketController.java

package com.redpacket.ssm.controller;

import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import com.redpacket.ssm.service.UserRedPacketService;

@Controller
@RequestMapping("/userRedPacket")
public class UserRedPacketController {

    @Autowired
    private UserRedPacketService userRedPacketService;
    
    @RequestMapping("/grapRedPacket")
    public @ResponseBody Map grapRedPacket(Integer redPacketId, Integer userId) {
        // 抢红包
        int result = userRedPacketService.grapRedPacket(redPacketId, userId);
        Map retMap = new HashMap();
        boolean flag = result > 0;
        retMap.put("success", flag);
        retMap.put("message", flag ? "抢红包成功" : "抢红包失败");
        return retMap;
    }   

}

DateConverter.java

package com.redpacket.ssm.controller.converter;

import java.text.SimpleDateFormat;
import java.util.Date;

import org.springframework.core.convert.converter.Converter;

public class DateConverter implements Converter {

    @Override
    public Date convert(String source) {
        
        //实现日期串转成日期类型(格式"yyyy-MM-dd HH:mm:ss")
        try {
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            //转成直接返回
            return simpleDateFormat.parse(source);
        } catch (Exception e) {
            e.printStackTrace();
        }
        //如果参数绑定失败返回null
        return null;
    }

}

实现日期串转换成日期类型

springmvc.xml



    
    
    
    
    
    
    

    
    
        
        
            
                
                
            
        
    

    
    
        
        
        
        
    

    
    
    
        
        
            5242880
        
    
    
    
    
    

web.xml



  ssm_redpacket
  
  
    
        contextConfigLocation
        /WEB-INF/classes/spring/applicationContext-*.xml
    
    
        org.springframework.web.context.ContextLoaderListener
    
    
    
    
    
        springmvc
        org.springframework.web.servlet.DispatcherServlet
        
        
            contextConfigLocation
            classpath:spring/springmvc.xml
        
    

    
        springmvc
        
        *.action
    
    
    
    
        CharacterEncodingFilter
        org.springframework.web.filter.CharacterEncodingFilter
        
            encoding
            utf-8
        
    
    
        CharacterEncodingFilter
        /*
    
  
  
    index.html
    index.htm
    index.jsp
    default.html
    default.htm
    default.jsp
  

redpacket.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
         pageEncoding="UTF-8"%>


    
        
        参数
        
        
        
    
    
    
    haha
    
    

模拟30000个用户同时进行抢红包操作

项目结构

此时进行高并发访问数据库不会出现超发问题,因为tomcat将数据库访问线程池中最大线程设置为了150,最小只有15,采用的还是BIO(阻塞型IO),刚并发性能很差,mysql毫无压力。。。

修改最大访问线程为5000,协议选择NIO,参考:

https://blog.csdn.net/dc282614966/article/details/81186783

此时就可以进行高并发访问了

访问http://localhost:8080/ssm_redpacket/redpacket.jsp
开始模拟高并发情况(注意使用火狐浏览器)

使用 SQL 去查询红包的库存、发放红包的总个数、总金额,我们发现了错误,红包总额为 20 万元,两万个小红包,结果发放了 200020元的红包, 20002 个红包。现有库存为-2,超出了之前的限定,这就是高并发的超发现象,这是一个错误的逻辑 。

SELECT
    a.id,
    a.amount,
    a.stock
FROM
    T_RED_PACKET a
WHERE
    a.id = 1
UNION ALL
    SELECT
        max(b.user_id),
        sum(b.amount),
        count(*)
    FROM
        T_USER_RED_PACKET b
    WHERE
        b.red_packet_id = 1;

一共使用了 50 秒的时间,完成 20002 个红包的抢夺,性能一般。。。但是逻辑上存在超发错误,还需要解决超发问题 。

SELECT
    (
        UNIX_TIMESTAMP(max(a.grab_time)) - UNIX_TIMESTAMP(min(a.grab_time)) 
    )  AS lastTime
FROM
    T_USER_RED_PACKET a;

超发问题解决思路:

超发现象是由多线程下数据不一致造成的,对于此类问题,如果采用数据库方案的话,主要通过悲观锁和乐观锁来处理,这两种方法的性能是不一样的。
接下来我们分别使用悲观锁、乐观锁、Redis+lua的方式来解决这个超发问题。

你可能感兴趣的:(ssm环境搭建和红包超发问题)