电商的秒杀、抢购,春运抢票,微信QQ抢红包,从技术的角度来说,这对于Web 系统是一个很大的考验. 高并发场景下,系统的优化和稳定是至关重要的.
互联网的开发包括 Java 后台、 Nosql、数据库、限流、CDN、负载均衡等内容,目前并没有权威性的技术和设计,有的只是长期经验的总结,但是使用这些经验可以有效优化系统,提高系统的并发能力.
进群:548377875 即可获取数十套pdf哦!
我们接下来的几篇博文主要讨论 Java 后台、 Nosql ( Redis )和数据库部分技术.
抢红包案例
主要分以下几大部分:
环境搭建 模拟超量发送的场景-DataBase(MysqL5.7) 悲观锁的实现版本-DataBase(MysqL5.7) 乐观锁的实现版本-DataBase(MysqL5.7) Redis实现抢红包案例关注点
模拟 20 万元的红包,共分为 2 万个可抢的小红包,有 3 万人同时抢夺的场景 ,模拟出现超发和如何保证数据一致性的问题。
在高并发的场景下,除了数据的一致性外,还要关注性能的问题 , 因为一般而言 , 时间太长用户体验就会很差,所以要测试数据一致性和系统的性能 。
工程结构
库表设计
MysqL5.7
/*==============================================================*//* 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));
红包表表示存放红包的是一个大红包的信息,它会分为若干个小红包,为了业务简单,假设每一个红包是等额的。而对于抢红包而言,就是从大红包中抢夺那些剩余的小红包,剩余红包数会被记录在红包表中。 两个表有外键关联 T_RED_PACKET.ID = T_USER_RED_PACKET.red_packet_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',2) not null COMMENT '抢到的红包金额',grab_time timestamp not null DEFAulT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '抢红包时间',primary key clustered (ID));/**
插入一个20万元金额,2万个小红包,每个10元的红包数据
*/
insert into T_REDPACKET(userID,amount,send_date,total,unit_amount,stock,note)
values(1,200000.00,Now(),20000,10.00,'20万元金额,2万个小红包,每个10元');
commit;
这样就建好了两个表,并且将一个 20 万元金额,2 万个小红包,每个 10 元的红包信息插入到了红包表中,用作模拟数据。
Domain
有了这两个表,我们就可以为这两个表建两个 POJO 了,让这两个表和 POJO 对应起来,这两个 POJO 为 RedPacket 和 UserRedPacket,实现类序列化接口。
红包信息
package com.artisan.redpacket.pojo;import java.io.Serializable;import java.sql.Timestamp;/** * * * @Classname: RedPacket * * @Description: 红包表对应的实体类,可序列化 * * @author: Mr.Yang * * @date: 2018年10月8日 下午3:42:58 */public class RedPacket implements Serializable { private static final long serialVersionUID = 9036484563091364939L; // 红包编号 private Long ID; // 发红包的用户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; // 省略set/get}
抢红包信息
package com.artisan.redpacket.pojo;import java.io.Serializable;import java.sql.Timestamp;/** * * * @Classname: UserRedPacket * * @Description: 用户抢红包表 * * @author: Mr.Yang * * @date: 2018年10月8日 下午3:47:40 */public class UserRedPacket implements Serializable { private static final long serialVersionUID = 7049215937937620886L; // 用户红包ID private Long ID; // 红包ID private Long redPacketID; // 抢红包的用户的ID private Long userID; // 抢红包金额 private Double amount; // 抢红包时间 private Timestamp grabTime; // 备注 private String note; // 省略set/get}
Dao层实现
MyBatis Dao接口类及对应的Mapper文件
使用 MyBatis 开发,先来完成大红包信息的查询先来定义一个 DAO 对象
package com.artisan.redpacket.dao;
import org.springframework.stereotype.Repository;
import com.artisan.redpacket.pojo.RedPacket;
@Repository
public interface RedPacketDao {
/**
* 获取红包信息.
* @param ID --红包ID
* @return 红包具体信息
*/
public RedPacket getRedPacket(Long ID);
/**
* 扣减抢红包数.
* @param ID -- 红包ID
* @return 更新记录条数
*/
public int decreaseRedPacket(Long ID);
}
其中的两个方法 , 一个是查询红包,另一个是扣减红包库存。
抢红包的逻辑是,先查询红包的信息,看其是否拥有存量可以扣减。如果有存量,那么可以扣减它,否则就不扣减。
接着将对应的Mapper映射文件编写一下
这里getRedPacket并没有加锁这类动作,目的是为了演示超发红包的情况.
然后是抢红包的设计了 ,先来定义插入抢红包的 DAO ,紧接着是Mapper映射文件
package com.artisan.redpacket.dao;import org.springframework.stereotype.Repository;import com.artisan.redpacket.pojo.UserRedPacket;@Repositorypublic interface UserRedPacketDao { /** * 插入抢红包信息. * @param userRedPacket ——抢红包信息 * @return 影响记录数. */ public int grapRedPacket(UserRedPacket userRedPacket);}
这里使用了 useGeneratedKeys 和 keyPrope町,这就意味着会返回数据库生成的主键信息,这样就可以拿到插入记录的主键了 , 关于 DAO 层就基本完成了。别忘了单元测试!!!
Service层实现
接下来定义两个 Service 层接口,分别是 UserRedPacketService和RedPacketService
package com.artisan.redpacket.service;import com.artisan.redpacket.pojo.RedPacket;public interface RedPacketService {/** * <a href="https://www.jb51.cc/tag/huoqu/" target="_blank" >获取</a>红包 * @p<a href="https://www.jb51.cc/tag/ara/" target="_blank" >ara</a>m <a href="https://m.jb51.cc/tag/ID/" target="_blank" >ID</a> ——编号 * @return 红包信息 */pub<a href="https://m.jb51.cc/tag/li/" target="_blank" >li</a>c RedPacket getRedPacket(Long <a href="https://m.jb51.cc/tag/ID/" target="_blank" >ID</a>);/** * 扣减红包 * @p<a href="https://www.jb51.cc/tag/ara/" target="_blank" >ara</a>m <a href="https://m.jb51.cc/tag/ID/" target="_blank" >ID</a>——编号 * @return 影响条数. */pub<a href="https://m.jb51.cc/tag/li/" target="_blank" >li</a>c int decreaseRedPacket(Long <a href="https://m.jb51.cc/tag/ID/" target="_blank" >ID</a>);
}
package com.artisan.redpacket.service;
public interface UserRedPacketService {/** * 保存抢红包信息. * @p<a href="https://www.jb51.cc/tag/ara/" target="_blank" >ara</a>m redPacket<a href="https://m.jb51.cc/tag/ID/" target="_blank" >ID</a> 红包编号 * @p<a href="https://www.jb51.cc/tag/ara/" target="_blank" >ara</a>m user<a href="https://m.jb51.cc/tag/ID/" target="_blank" >ID</a> 抢红包<a href="https://www.jb51.cc/tag/yonghu/" target="_blank" >用户</a>编号 * @return 影响记录数. */pub<a href="https://m.jb51.cc/tag/li/" target="_blank" >li</a>c int grapRedPacket(Long redPacket<a href="https://m.jb51.cc/tag/ID/" target="_blank" >ID</a>,Long user<a href="https://m.jb51.cc/tag/ID/" target="_blank" >ID</a>);
}
实现类如下:
package com.artisan.redpacket.service.impl;import org.springframework.beans.factory.annotation.autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Isolation;import org.springframework.transaction.annotation.Propagation;import org.springframework.transaction.annotation.Transactional;import com.artisan.redpacket.dao.RedPacketDao;import com.artisan.redpacket.pojo.RedPacket;import com.artisan.redpacket.service.RedPacketService;@Servicepublic class RedPacketServiceImpl implements RedPacketService {@<a href="https://m.jb51.cc/tag/auto/" target="_blank" >auto</a>wiredprivate RedPacketDao redPacketDao;@Overr<a href="https://m.jb51.cc/tag/ID/" target="_blank" >ID</a>e@Transactional(is<a href="https://m.jb51.cc/tag/ol/" target="_blank" >ol</a>ation=Is<a href="https://m.jb51.cc/tag/ol/" target="_blank" >ol</a>ation.READ_COMMITTED,propagation = Propagation.<a href="https://www.jb51.cc/tag/required/" target="_blank" >required</a>)pub<a href="https://m.jb51.cc/tag/li/" target="_blank" >li</a>c RedPacket getRedPacket(Long <a href="https://m.jb51.cc/tag/ID/" target="_blank" >ID</a>) { return redPacketDao.getRedPacket(<a href="https://m.jb51.cc/tag/ID/" target="_blank" >ID</a>);}@Overr<a href="https://m.jb51.cc/tag/ID/" target="_blank" >ID</a>e@Transactional(is<a href="https://m.jb51.cc/tag/ol/" target="_blank" >ol</a>ation=Is<a href="https://m.jb51.cc/tag/ol/" target="_blank" >ol</a>ation.READ_COMMITTED,propagation = Propagation.<a href="https://www.jb51.cc/tag/required/" target="_blank" >required</a>)pub<a href="https://m.jb51.cc/tag/li/" target="_blank" >li</a>c int decreaseRedPacket(Long <a href="https://m.jb51.cc/tag/ID/" target="_blank" >ID</a>) { return redPacketDao.decreaseRedPacket(<a href="https://m.jb51.cc/tag/ID/" target="_blank" >ID</a>);}
}
配置了事务注解@Transactional , 让程序能够在事务中运行,以保证数据的一致性 , 这里采用的是读/写提交的隔离级别 , 之所以不采用更高的级别, 主要是提高数据库的并发能力,而对于传播行为则采用 Propagation.required,这样调用这个方法的时候,如果没有事务则会创建事务, 如果有事务则沿用当前事务。
实现 UserRedPacketService 接口的方法 grapRedPacket,它是核心的接口方法
package com.artisan.redpacket.service.impl;import org.springframework.beans.factory.annotation.autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Isolation;import org.springframework.transaction.annotation.Propagation;import org.springframework.transaction.annotation.Transactional;import com.artisan.redpacket.dao.RedPacketDao;import com.artisan.redpacket.dao.UserRedPacketDao;import com.artisan.redpacket.pojo.RedPacket;import com.artisan.redpacket.pojo.UserRedPacket;import com.artisan.redpacket.service.UserRedPacketService;@Servicepublic class UserRedPacketServiceImpl implements UserRedPacketService {// private Logger logger =// LoggerFactory.getLogger(UserRedPacketServiceImpl.class);@<a href="https://m.jb51.cc/tag/auto/" target="_blank" >auto</a>wiredprivate UserRedPacketDao userRedPacketDao;@<a href="https://m.jb51.cc/tag/auto/" target="_blank" >auto</a>wiredprivate RedPacketDao redPacketDao;// 失败private static final int <a href="https://www.jb51.cc/tag/Failed/" target="_blank" >Failed</a> = 0;@Overr<a href="https://m.jb51.cc/tag/ID/" target="_blank" >ID</a>e@Transactional(is<a href="https://m.jb51.cc/tag/ol/" target="_blank" >ol</a>ation = Is<a href="https://m.jb51.cc/tag/ol/" target="_blank" >ol</a>ation.READ_COMMITTED,propagation = Propagation.<a href="https://www.jb51.cc/tag/required/" target="_blank" >required</a>)pub<a href="https://m.jb51.cc/tag/li/" target="_blank" >li</a>c int grapRedPacket(Long redPacket<a href="https://m.jb51.cc/tag/ID/" target="_blank" >ID</a>,Long user<a href="https://m.jb51.cc/tag/ID/" target="_blank" >ID</a>) { // <a href="https://www.jb51.cc/tag/huoqu/" target="_blank" >获取</a>红包信息 RedPacket redPacket = redPacketDao.getRedPacket(redPacket<a href="https://m.jb51.cc/tag/ID/" target="_blank" >ID</a>); int <a href="https://m.jb51.cc/tag/left/" target="_blank" >left</a>RedPacket = redPacket.getStock(); // 当前小红包库存大于0 if (<a href="https://m.jb51.cc/tag/left/" target="_blank" >left</a>RedPacket > 0) { redPacketDao.decreaseRedPacket(redPacket<a href="https://m.jb51.cc/tag/ID/" target="_blank" >ID</a>); // logger.info("剩余Stock<a href="https://www.jb51.cc/tag/shuliang/" target="_blank" >数量</a>:{}",<a href="https://m.jb51.cc/tag/left/" target="_blank" >left</a>RedPacket); // <a href="https://www.jb51.cc/tag/shengcheng/" target="_blank" >生成</a>抢红包信息 UserRedPacket userRedPacket = new UserRedPacket(); userRedPacket.setRedPacket<a href="https://m.jb51.cc/tag/ID/" target="_blank" >ID</a>(redPacket<a href="https://m.jb51.cc/tag/ID/" target="_blank" >ID</a>); userRedPacket.setUser<a href="https://m.jb51.cc/tag/ID/" target="_blank" >ID</a>(user<a href="https://m.jb51.cc/tag/ID/" target="_blank" >ID</a>); userRedPacket.setAmount(redPacket.getUnitAmount()); userRedPacket.setNote("redpacket- " + redPacket<a href="https://m.jb51.cc/tag/ID/" target="_blank" >ID</a>); // 插入抢红包信息 int res<a href="https://m.jb51.cc/tag/ul/" target="_blank" >ul</a>t = userRedPacketDao.grapRedPacket(userRedPacket); return res<a href="https://m.jb51.cc/tag/ul/" target="_blank" >ul</a>t; } // logger.info("没有红包啦.....剩余Stock<a href="https://www.jb51.cc/tag/shuliang/" target="_blank" >数量</a>:{}",<a href="https://m.jb51.cc/tag/left/" target="_blank" >left</a>RedPacket); // 失败返回 return <a href="https://www.jb51.cc/tag/Failed/" target="_blank" >Failed</a>;}
}
grapRedPacket 方法的逻辑是首先获取红包信息,如果发现红包库存大于 0,则说明还有红包可抢,抢夺红包并生成抢红包的信息将其保存到数据库中。要注意的是,数据库事务方面的设置,代码中使用注解@Transactional , 说明它会在一个事务中运行,这样就能够保证所有的 *** 作都是在一个事务中完成的。在高并发中会发生超发的现象,后面会看到超发的实际测试。
使用全注解搭建SSM 开发环境
我们这里将使用注解的方式来完成 SSM 开发的环境,可以通过继承 AbstractAnnotationConfigdispatcherServletlnitfal izer 去配置其他内 容,因此首先来配置 WebApplnitialize
package com.artisan.redpacket.config;import javax.servlet.MultipartConfigElement;import javax.servlet.ServletRegistration.Dynamic;import org.springframework.web.servlet.support.AbstractAnnotationConfigdispatcherServletinitializer;public class WebAppInitializer extends AbstractAnnotationConfigdispatcherServletinitializer { // Spring IoC环境配置 @OverrIDe protected Class>[] getRootConfigClasses() { // 配置Spring IoC资源 return new Class>[] { RootConfig.class }; } // dispatcherServlet环境配置 @OverrIDe protected Class>[] getServletConfigClasses() { // 加载Java配置类 return new Class>[] { WebConfig.class }; } // dispatchServlet拦截请求配置 @OverrIDe protected String[] getServletMapPings() { return new String[] { "*.do" }; } /** * @param dynamic * Servlet上传文件配置. */ @OverrIDe protected voID customizeRegistration(Dynamic dynamic) { // 配置上传文件路径 String filepath = "D:/"; // 5MB Long singleMax = (long) (5 * Math.pow(2,20)); // 10MB Long totalMax = (long) (10 * Math.pow(2,20)); // 设置上传文件配置 dynamic.setMultipartConfig(new MultipartConfigElement(filepath,singleMax,totalMax,0)); }}
WebAppInitializer继承了 AbstractAnnotationConfigdispatcherServletlnitializer, 重写了 3 个抽象方法 , 并且覆盖了父类的 customizeRegistration 方法 , 作为上传文件的配置。
getRootConfigClasses 是一个配置 Spring IoC 容器的上下文配置 , 此配置在代码中将会由类 RootConfig 完成 getServletConfigClasses 配置 dispatcherServlet 上下文配置,将会由WebConfig完成 getServletMapPings 配置 dispatcherServlet 拦截 内 容 , 这里设置的是拦截所有以 .do 结尾的请求通过这 3 个方法就可以配置 Web 工程中 的 Spring IoC 资源和 dispatcherServlet 的配置内容 , 首先是配置 Spring IoC 容器,配置类 RootConfig
package com.artisan.redpacket.config;import java.util.PropertIEs;import javax.sql.DataSource;import org.apache.commons.dbcp2.BasicdataSourceFactory;import org.mybatis.spring.sqlSessionfactorybean;import org.mybatis.spring.mapper.MapperScannerConfigurer;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.ComponentScan.Filter;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.FilterType;import org.springframework.core.io.ClassPathResource;import org.springframework.core.io.Resource;import org.springframework.jdbc.datasource.DataSourceTransactionManager;import org.springframework.stereotype.Repository;import org.springframework.stereotype.Service;import org.springframework.transaction.PlatformTransactionManager;import org.springframework.transaction.annotation.EnableTransactionManagement;import org.springframework.transaction.annotation.TransactionManagementConfigurer;@Configuration//定义Spring 扫描的包@ComponentScan(value= "com.*",includeFilters= {@Filter(type = FilterType.ANNOTATION,value ={Service.class})})//使用事务驱动管理器@EnableTransactionManagement//实现接口TransactionManagementConfigurer,这样可以配置注解驱动事务public class RootConfig implements TransactionManagementConfigurer {private DataSource dataSource = n<a href="https://m.jb51.cc/tag/ul/" target="_blank" >ul</a>l;/** * 配置<a href="https://www.jb51.cc/tag/shujuku/" target="_blank" >数据库</a>. * @return 数据连接池 */@Bean(<a href="https://m.jb51.cc/tag/name/" target="_blank" >name</a> = "dataSource")pub<a href="https://m.jb51.cc/tag/li/" target="_blank" >li</a>c DataSource initDataSource() { if (dataSource != n<a href="https://m.jb51.cc/tag/ul/" target="_blank" >ul</a>l) { return dataSource; } try { Propert<a href="https://m.jb51.cc/tag/IE/" target="_blank" >IE</a>s props = new Propert<a href="https://m.jb51.cc/tag/IE/" target="_blank" >IE</a>s(); props.load(RootCon<a href="https://www.jb51.cc/tag/fig/" target="_blank" >fig</a>.class.getClassLoader().getResourceA<a href="https://www.jb51.cc/tag/sst/" target="_blank" >sst</a>ream("jdbc.propert<a href="https://m.jb51.cc/tag/IE/" target="_blank" >IE</a>s")); props.setProperty("driverClass<a href="https://m.jb51.cc/tag/name/" target="_blank" >name</a>",props.getProperty("jdbc.driver")); props.setProperty("url",props.getProperty("jdbc.url")); props.setProperty("user<a href="https://m.jb51.cc/tag/name/" target="_blank" >name</a>",props.getProperty("jdbc.user<a href="https://m.jb51.cc/tag/name/" target="_blank" >name</a>")); props.setProperty("password",props.getProperty("jdbc.password")); dataSource = Basi<a href="https://m.jb51.cc/tag/cda/" target="_blank" >cda</a>taSourceFactory.createDataSource(props); } catch (Exception e) { e.<a href="https://www.jb51.cc/tag/printstacktrace/" target="_blank" >printstacktrace</a>(); } return dataSource;}/*** * 配置<a href="https://www.jb51.cc/tag/sql/" target="_blank" >sql</a>Session<a href="https://www.jb51.cc/tag/factorybean/" target="_blank" >factorybean</a> * @return <a href="https://www.jb51.cc/tag/sql/" target="_blank" >sql</a>Session<a href="https://www.jb51.cc/tag/factorybean/" target="_blank" >factorybean</a> */@Bean(<a href="https://m.jb51.cc/tag/name/" target="_blank" >name</a>="<a href="https://www.jb51.cc/tag/sql/" target="_blank" >sql</a>SessionFactory")pub<a href="https://m.jb51.cc/tag/li/" target="_blank" >li</a>c <a href="https://www.jb51.cc/tag/sql/" target="_blank" >sql</a>Session<a href="https://www.jb51.cc/tag/factorybean/" target="_blank" >factorybean</a> init<a href="https://www.jb51.cc/tag/sql/" target="_blank" >sql</a>SessionFactory() { <a href="https://www.jb51.cc/tag/sql/" target="_blank" >sql</a>Session<a href="https://www.jb51.cc/tag/factorybean/" target="_blank" >factorybean</a> <a href="https://www.jb51.cc/tag/sql/" target="_blank" >sql</a>SessionFactory = new <a href="https://www.jb51.cc/tag/sql/" target="_blank" >sql</a>Session<a href="https://www.jb51.cc/tag/factorybean/" target="_blank" >factorybean</a>(); <a href="https://www.jb51.cc/tag/sql/" target="_blank" >sql</a>SessionFactory.setDataSource(initDataSource()); //配置MyBatis<a href="https://www.jb51.cc/tag/peizhiwenjian/" target="_blank" >配置文件</a> Resource resource = new <a href="https://www.jb51.cc/tag/ClassPathResource/" target="_blank" >ClassPathResource</a>("mybatis/mybatis-con<a href="https://www.jb51.cc/tag/fig/" target="_blank" >fig</a>.xml"); <a href="https://www.jb51.cc/tag/sql/" target="_blank" >sql</a>SessionFactory.setCon<a href="https://www.jb51.cc/tag/fig/" target="_blank" >fig</a>Location(resource); return <a href="https://www.jb51.cc/tag/sql/" target="_blank" >sql</a>SessionFactory;}/*** * 通过<a href="https://www.jb51.cc/tag/zidong/" target="_blank" >自动</a>扫描,发现MyBatis Mapper接口 * @return Mapper扫描器 */@Bean pub<a href="https://m.jb51.cc/tag/li/" target="_blank" >li</a>c MapperScannerCon<a href="https://www.jb51.cc/tag/fig/" target="_blank" >fig</a>urer initMapperScannerCon<a href="https://www.jb51.cc/tag/fig/" target="_blank" >fig</a>urer() { MapperScannerCon<a href="https://www.jb51.cc/tag/fig/" target="_blank" >fig</a>urer msc = new MapperScannerCon<a href="https://www.jb51.cc/tag/fig/" target="_blank" >fig</a>urer(); msc.setBasePackage("com.*"); msc.set<a href="https://www.jb51.cc/tag/sql/" target="_blank" >sql</a>Session<a href="https://www.jb51.cc/tag/factorybean/" target="_blank" >factorybean</a><a href="https://m.jb51.cc/tag/name/" target="_blank" >name</a>("<a href="https://www.jb51.cc/tag/sql/" target="_blank" >sql</a>SessionFactory"); msc.setAnnotationClass(Repository.class); return msc;}/** * 实现接口<a href="https://www.jb51.cc/tag/fangfa/" target="_blank" >方法</a>,<a href="https://www.jb51.cc/tag/zhuce/" target="_blank" >注册</a>注解事务,当@Transactional <a href="https://m.jb51.cc/tag/shiyong/" target="_blank" >使用</a>的时候产生<a href="https://www.jb51.cc/tag/shujuku/" target="_blank" >数据库</a>事务 */@Overr<a href="https://m.jb51.cc/tag/ID/" target="_blank" >ID</a>e@Bean(<a href="https://m.jb51.cc/tag/name/" target="_blank" >name</a>="annotationDrivenTransactionManager")pub<a href="https://m.jb51.cc/tag/li/" target="_blank" >li</a>c PlatformTransactionManager annotationDrivenTransactionManager() { DataSourceTransactionManager transactionManager =
new DataSourceTransactionManager();
transactionManager.setDataSource(initDataSource());
return transactionManager;
}}
这个类和之前论述的有所不同 , 它标注了注解@EnableTransactionManagement , 实现了接口 TransactionManagementConfigurer, 这样的配置是为了实现注解式的事务 , 将来可以通过注解@Transactional 配 置数据库事务。
它有一 个方法annotationDrivenTransactionManager这需要将一个事务管理器返回给它就可以了
除了配置数据库事务外 ,还配置了数据源 SqISessionfactorybean 和 MyBatis 的扫描类 , 并把 MyBatis的扫描类通过注解@Repository 和包名"com.*"限定。这样 MyBatis 就会通过 Spring 的机制找到对应的接 口和配置 , Spring 会自动把对应的接口装配到 IoC 容器中 。
有了 Spring IoC 容器后 , 还需要配置 dispatcherServlet 上下文
package com.artisan.redpacket.config;import java.util.ArrayList;import java.util.List;import java.util.concurrent.Executor;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.ComponentScan.Filter;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.FilterType;import org.springframework.http.MediaType;import org.springframework.http.converter.Json.MapPingJackson2httpMessageConverter;import org.springframework.scheduling.annotation.AsyncConfigurerSupport;import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;import org.springframework.stereotype.Controller;import org.springframework.web.servlet.HandlerAdapter;import org.springframework.web.servlet.VIEwResolver;import org.springframework.web.servlet.config.annotation.EnableWebMvc;import org.springframework.web.servlet.mvc.method.annotation.RequestMapPingHandlerAdapter;import org.springframework.web.servlet.vIEw.InternalResourceVIEwResolver;@Configuration//定义Spring MVC扫描的包@ComponentScan(value="com.*",value = Controller.class)})//启动Spring MVC配置@EnableWebMvcpublic class WebConfig{ /*** * 通过注解 @Bean 初始化视图解析器 * @return VIEwResolver 视图解析器 */ @Bean(name="internalResourceVIEwResolver") public VIEwResolver initVIEwResolver() { InternalResourceVIEwResolver vIEwResolver =new InternalResourceVIEwResolver(); vIEwResolver.setPrefix("/WEB-INF/Jsp/"); vIEwResolver.setSuffix(".Jsp"); return vIEwResolver; }/** * 初始化RequestMap<a href="https://m.jb51.cc/tag/Ping/" target="_blank" >Ping</a>HandlerAdapter,并加载<a href="https://m.jb51.cc/tag/http/" target="_blank" >http</a>的<a href="https://m.jb51.cc/tag/Js/" target="_blank" >Js</a>on转换器 * @return RequestMap<a href="https://m.jb51.cc/tag/Ping/" target="_blank" >Ping</a>HandlerAdapter 对象 */@Bean(<a href="https://m.jb51.cc/tag/name/" target="_blank" >name</a>="requestMap<a href="https://m.jb51.cc/tag/Ping/" target="_blank" >Ping</a>HandlerAdapter") pub<a href="https://m.jb51.cc/tag/li/" target="_blank" >li</a>c HandlerAdapter initRequestMap<a href="https://m.jb51.cc/tag/Ping/" target="_blank" >Ping</a>HandlerAdapter() { //<a href="https://m.jb51.cc/tag/chuangjian/" target="_blank" >创建</a>RequestMap<a href="https://m.jb51.cc/tag/Ping/" target="_blank" >Ping</a>HandlerAdapter适配器 RequestMap<a href="https://m.jb51.cc/tag/Ping/" target="_blank" >Ping</a>HandlerAdapter rmhd = new RequestMap<a href="https://m.jb51.cc/tag/Ping/" target="_blank" >Ping</a>HandlerAdapter(); //<a href="https://m.jb51.cc/tag/http/" target="_blank" >http</a> <a href="https://m.jb51.cc/tag/Js/" target="_blank" >Js</a>ON转换器 Map<a href="https://m.jb51.cc/tag/Ping/" target="_blank" >Ping</a>Jackson2<a href="https://m.jb51.cc/tag/http/" target="_blank" >http</a>MessageConverter <a href="https://m.jb51.cc/tag/Js/" target="_blank" >Js</a>onConverter = new Map<a href="https://m.jb51.cc/tag/Ping/" target="_blank" >Ping</a>Jackson2<a href="https://m.jb51.cc/tag/http/" target="_blank" >http</a>MessageConverter(); //Map<a href="https://m.jb51.cc/tag/Ping/" target="_blank" >Ping</a>Jackson2<a href="https://m.jb51.cc/tag/http/" target="_blank" >http</a>MessageConverter接收<a href="https://m.jb51.cc/tag/Js/" target="_blank" >Js</a>ON类型消息的转换 MediaType mediaType = MediaType.APP<a href="https://m.jb51.cc/tag/li/" target="_blank" >li</a>CATION_<a href="https://m.jb51.cc/tag/Js/" target="_blank" >Js</a>ON_UTF8; <a href="https://m.jb51.cc/tag/List/" target="_blank" >List</a><MediaType> mediaTypes = new Array<a href="https://m.jb51.cc/tag/List/" target="_blank" >List</a><MediaType>(); mediaTypes.add(mediaType); //加入转换器的<a href="https://www.jb51.cc/tag/zhichi/" target="_blank" >支持</a>类型 <a href="https://m.jb51.cc/tag/Js/" target="_blank" >Js</a>onConverter.setSupportedMediaTypes(mediaTypes); //往适配器加入<a href="https://m.jb51.cc/tag/Js/" target="_blank" >Js</a>on转换器 rmhd.getMessageConverters().add(<a href="https://m.jb51.cc/tag/Js/" target="_blank" >Js</a>onConverter); return rmhd;}
}
这里配置了一个视图解析器 , 通过它找到对应 JsP 文件,然后使用数据模型进行渲染,采用自定义 创 建 RequestMapPingHandlerAdapter , 为了让它能够支持 JsON 格式(@ResponseBody ) 的转换,所以需要创建一个关于对象和 JsON 的转换消息类MapPingJackson2httpMessageConverter
创建它之后,把它注册给 RequestMapPingHandlerAdapter对象 , 这样当控制器遇到注解@ResponseBody 的时候就知道采用 JsON 消息类型进行应答 , 那么在控制器完成逻辑后 , 由处理器将其和消息转换类型做匹配,找到MapPingJackson2httpMessageConverter 类对象,从而转变为 JsON 数据。
通过上面的 3 个类就搭建好了 Spring MVC 和 Spring 的开发环境,但是没有完成对MyBatis 配置文件. 从RootConfig#initsqlSessionFactory()方法中看到加载的MyBatis 的配置文件为"mybatis/mybatis-config.xml",该配置文件主要是加载mapper映射文件
记得进行Service层的单元测试, 关于后台的逻辑就已经完成 , 接下来继续将Controller层实现,进行页面测试吧。
Controller层
package com.artisan.redpacket.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.artisan.redpacket.service.UserRedPacketService;@Controller@RequestMapPing("/userRedPacket")public class UserRedPacketController { @autowired private UserRedPacketService userRedPacketService; @RequestMapPing(value = "/grapRedPacket") @ResponseBody public MapgrapRedPacket(Long redPacketID,Long 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; } }
对于控制器而言 , 它将抢夺一个红包 , 并且将一个 Map返回,由于使用了注解@ResponseBody 标注方法,所以最后它会转变为一个 JsON 返回给前端请求,编写 JsP 对其进行测试
VIEw层
grap.Jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> 参数