项目描述
所谓的秒杀就是类似双11的商品抢购操作,秒杀项目是一种高并发项目,它要求能处理很多人对一个商品的秒杀操作,这就要求你的打码需要有一定的优化和改进,在这个项目中我们利用了三点优化来提高并发性。
1、通过redis缓存商品信息。在项目中需要多次进行数据库商品信息的查询,例如取得商品id,开始时间和结束时间等等,这个时候如果都去访问数据库无疑会给数据库增加很大的压力,我们在这里做的一个优化就是通过redis来将商品信息缓存下来,以商品的id作为key,这样一来每次需要取得商品信息的时候直接访问redis就好了
2、调整sql语句的执行顺序。项目中有一个业务逻辑是这样的:减库存和将秒杀单插入成功秒杀表,这里涉及了两个数据库操作,update和insert。在进行update处理一行数据时,会获得行级锁,这在一定的程度上影响了insert操作的速度,为此我们将insert放在前面,先进行数据的插入,如果失败了回滚,成功了就执行update操作,这样能提高系统并发量
3、使用存储过程来执行sql语句以减少网络延迟和gc操作。存储过程是数据库的操作过程,它是作为解决复杂逻辑,如需要连接多张表或者进行多项操作的一种优化方法出现的,在存储过程执行的过程中,它把很多个逻辑统一执行后返回,不需要每执行一个逻辑就访问一次数据库并写回,这在很大程度上避免了数据传输过程中的网络延迟和gc操作
ssm框架搭建
1. 用eclipse创建meaven项目
2. 编写pom.xml文件,导入所需要的依赖,这里列举一下此项目所需要用到的包依赖(一般的ssm项目也会用到)
测试
junit 的 junit
日志
org.slf4j 的 slf4j-api
ch.qos.logback 的 logback-core
ch.qos.logback 的 logback-classic
数据库相关
mysql 的 mysql-connector-java
c3p0 的 c3p0
org.mybatis 的 mybatis
org.mybatis 的 mybatis-spring
servlet web相关
jstl 的 jstl
com.fasterxml.jackson.core 的 jackson-databin
avax.servlet 的 javax.servlet-api
spring核心
org.springframework 的 spring-core
org.springframework 的 spring-beans
org.springframework 的 spring-context
spring dao层
org.springframework 的 spring-jdbc
org.springframework 的 spring-tx
spring web层
org.springframework 的 spring-web
org.springframework 的 spring-webmvc
spring测试
org.springframework 的 spring-test
redis
redis.clients 的 jedis
protostuff序列化
com.dyuproject.protostuff 的 protostuff-core
com.dyuproject.protostuff 的 protostuff-runtime
处理map的工具
commons-collections 的 commons-collections
3. 创建三个大的包
4. 进行dao层的编程
在进行dao层的编写前,要根据数据库表来实现entity类的编写(实体),
dao层主要是一些单一的逻辑,编写的是接口,每一张表对应一个接口,每个接口中定义了若干个数据库操作方法,这些方法的实现是在resources的mapper包下的xml中。因此你有多少个接口就应该对应有多少个xml文件。
xml文件用的是mybatis的语法,例子如下,如果你要用update语句,那么图中的select标签应该改为update
需要注意的是如果你的dao接口中定义了多个参数,然后在xml中使用到了这些参数,mybatis是不能识别的,此时你需要使用@param(“参数名字”)来标识这个参数,例子如下
List<Seckill> queryAll(@Param("offset")int offset,@Param("limit")int limit);
5.dao层编写完毕后,需要编写配置文件
在resources包下新建一个spring的文件夹,里面放spring的配置
在spring文件夹中新建一个spring-dao.xml文件,里面放dao的配置
dao的配置一共有四步:
1、获取写有数据库相关参数的properties文件
<context:property-placeholder location="classpath:jdbc.properties"/>
2、配置数据库连接池
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${driver}"/>
<property name="jdbcUrl" value="${url}"/>
<property name="user" value="${jdbc.username}"/>
<property name="password" value="${password}"/>
<property name="maxPoolSize" value="30"></property>
<property name="minPoolSize" value="10"></property>
<!-- 关闭连接后不自动commit -->
<property name="autoCommitOnClose" value="false"></property>
<!-- 获取连接超时时间 -->
<property name="checkoutTimeout" value="1000"></property>
<!-- 获取连接失败重试次数 -->
<property name="acquireRetryAttempts" value="2"></property>
</bean>
3、配置sqlSessionFactory对象
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"></property>
<property name="configLocation" value="classpath:mybatis-config.xml"></property>
<property name="typeAliasesPackage" value="org.seckill.entity"></property>
<property name="mapperLocations" value="classpath:mapper/*.xml"></property>
</bean>
这里使用到了一个mybatis-config.xml的配置文件,这个配置文件是用来配置mybatis的,常用的配置如下
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 配置全局属性 -->
<settings>
<!-- 使用jdbc的getGeneratedKeys 获取数据库自增主键值 -->
<setting name="useGeneratedKeys" value="true"/>
<!-- 使用列别名替换列名 默认:true -->
<setting name="useColumnLabel" value="true"/>
<!-- 开启驼峰命名转换 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
</configuration>
4、配置扫描dao接口的包
由于dao写的都是接口,因此不能直接交由spring管理,需要mapper进行管理并注入spring容器中
<!-- 配置扫描Dao接口包,动态实现Dao接口,注入到spring容器中 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>
<property name="basePackage" value="org.seckill.dao"></property>
</bean>
到这里dao层基本编写完毕,可以进行dao层的测试
4. 进行service层的编程
service层主要是运用dao层提供的接口来实现业务逻辑,它的核心包括两方面的设计,一是service的接口,二是service的实现类
接口自然没什么说的,就是将业务的逻辑方法定义好
关键是service的实现类,这里要开发好service的实现类主要多设计了三个包,分别是enums(枚举)、exception(异常)、dto(数据传输)
首先看一下dto(数据传输包),你需要将你的传输数据封装成类放在这个包中,例如,秒杀的结果,是否暴露秒杀地址等等,这里还可以设计一个SeckillResult类来封装所有ajax的返回类型,它将json结果进行了封装
再来是exception(异常包),这个包是用来设计你的项目所有可能会碰到的异常的,在秒杀的项目里面,主要有重复秒杀异常(SeckillRepeatException)和秒杀关闭异常(SecillCloseException),这两个异常都继承自一个大的异常SeckillException(秒杀异常),而这个大的异常继承自RuntimeException。所有我们自己定义的异常类都有两个构造方法,一个的参数是message,一个的参数是message和throwable类型的cause,他们都调用了父类的同参构造函数。我们都知道异常其实是一个项目中比较重要的部分,一些异常如果一旦出现而我们程序没有进行捕获处理的话,就会导致线程停止,那么我们需要捕获这种异常并且往上抛出我们自身定义的异常来方便调用者处理,让程序继续走下去,至于为什么要定义这么多异常,是因为要保持我们的逻辑清晰,整个程序的结构更明了
enums(枚举包)是用来存放状态码常量的,我们可以通过约定某一个值代表某一个状态来给相应的整数赋予状态码,在之后的编程中就可以通过值来得到状态
service层的配置
编写完service接口及实现类后,开始进行service层的配置工作
service层的配置比较简单,一共两步
1、配置包扫描,用到context:component-scan标签
2、配置事务管理器
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 注入数据库连接池 -->
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 配置基于注解的声明式事务 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
由于这里的dataSource我们在spring-dao.xml中已经进行了配置,所以只要把spring-dao.xml也加入到spring容器中就可以使用
这里我们用到的是注解型的事务管理,在我们需要的service方法上加上@transaction即可进行事务控制,要知道的是事务控制是通过是否抛出异常来判断的,如果抛出了异常则回滚(rollback),否则即提交(commit)
进行web层的编程
web层的原理主要是用到了springmvc,而需要程序员进行编写的部分就是Controller,也就是我们的Handler(处理器)
Controller主要是运用service的方法对业务逻辑进行最后的整理和实现,他实现的机制主要有两个
1、返回的是一个ModelAndView类型的结果。运用其实例的addObject(“key”,value)方法可以对某一个你指定的key赋值,然后用其实例的setViewName(“jsp路径”)的方法指定jsp,这样在你指定的jsp上就可以用{key.value}的方法使用key中的属性,例子如下
// 调用service查找 数据库,查询商品列表
List<ItemsCustom> itemsList = itemsService.findItemsList(null);
// 返回ModelAndView
ModelAndView modelAndView = new ModelAndView();
// 相当 于request的setAttribut,在jsp页面中通过itemsList取数据
modelAndView.addObject("itemsList", itemsList);
// 指定视图
// 下边的路径,如果在视图解析器中配置jsp路径的前缀和jsp路径的后缀,修改为
// modelAndView.setViewName("/WEB-INF/jsp/items/itemsList.jsp");
// 上边的路径配置可以不在程序中指定jsp路径的前缀和jsp路径的后缀
modelAndView.setViewName("items/itemsList");
return modelAndView;
2、返回的是一个String类型的结果,方法与上面类似,只不过方法变成了通过addAttribute(“key”, value)来存储value到相应的key中,直接返回String类型的jsp路径就可以了,例子如下
List<Seckill> list = seckillService.getSeckillList();
model.addAttribute("list", list);
// /WEB-INF/jsp/"list".jsp
return "list";
Controller部分通过@RequestMapping(“url”)来指定每一个方法对应的url,要注意的是我们这里的url设计最好使用Restful风格,也就是 /模块/资源/{id}/细分
因此,我们的RequestMapping就可以变成下面这样
@RequestMapping(value="/{seckillId}/exposer",method=RequestMethod.POST,
produces={"application/json;charset=UTF-8"})
@ResponseBody
public SeckillResult<Exposer> exposer(Long seckillId){
//dosomething
}
进行web层的配置
web层的配置主要分为两大步
1、配置spring-web.xml
首先声明开启mvc注解模式
<mvc:annotation-driven />
这个xml标签虽然简单可是提供了以下的多种作用:
(1)自动注册DefaultAnnotationHandlerMapping,AnnotationMethodHandlerAdapter
(2)提供一系列功能:数据绑定,数字和日期的format @NumberFormt,@DataTimeFormat, xml,json默认读写支持
然后进行静态资源默认servlet配置
<!-- 静态资源默认servlet配置
1、加入对静态资源的处理:js,gif,png
2、允许使用"/"做整体映射 -->
<mvc:default-servlet-handler/>
配置jsp 显示ViewResolver,这个的作用是在你的Controller中不需要写前缀后缀,而是在这里进行统一的注册
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView"></property>
<property name="prefix" value="/WEB-INF/jsp/"></property>
<property name="suffix" value=".jsp"></property>
</bean>
最后别忘了配置包扫描
<!-- 扫描web相关的bean -->
<context:component-scan base-package="org.seckill.web"/>
2、配置webapp/WEB-INF/web.xml
<!-- 配置DispatcherServlet -->
<servlet>
<servlet-name>seckill-dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- 配置springMVC需要加载的配置文件
spring-dao.xml,spring-servie.xml,spring-web.xml -->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/spring-*.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>seckill-dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
配置web.xml主要是配置DispatcherServlet(前端控制器),然后将配置文件都注册进去,最后跟普通servlet一样要配置servlet-mapping,并且标注他进行处理的url格式
到这里总的ssm框架已经变成完毕,接下来是并发优化部分
redis并发优化
在文章一开始我们就提到了三点可改进的地方,下面我们只说一下redis的实现
这里需要新建一个dao类,实现两个方法,一个是getSeckill()一个是putSeckill()。顾名思义,一个是存库一个是取数据
首先看一下取数据,要注意的是redis提供给我们的Jedis类并没有实现内部序列化操作,内部序列化操作需要我们自己完成,我们需要借助一个类ProtostuffIOUtil,以下是实现方法
private final JedisPool jedisPool;
public RedisDao(String ip,int port){
jedisPool = new JedisPool(ip,port);
}
private RuntimeSchema<Seckill> schema = RuntimeSchema.createFrom(Seckill.class);
public Seckill getSeckill(long seckillId){
try {
Jedis jedis = jedisPool.getResource();
try{
String key = "seckill:" + seckillId;
//并没有实现内部序列化操作
//get->byte[]->反序列化->Object(Seckill)
//采用自定义序列化
byte[] bytes = jedis.get(key.getBytes());
if(bytes!=null){
//空对象
Seckill seckill = schema.newMessage();
ProtostuffIOUtil.mergeFrom(bytes, seckill, schema);
//seckill被反序列化
return seckill;
}
} finally {
jedis.close();
}
} catch (Exception e) {
logger.error(e.getMessage(),e);
}
return null;
}
public String putSeckill(Seckill seckill){
//set Object(Seckill)->序列化->byte[]
Jedis jedis = jedisPool.getResource();
try{
String key = "seckill:"+seckill.getSeckillId();
byte[] bytes = ProtostuffIOUtil.toByteArray(seckill, schema, LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE));
//超时缓存
int timeout = 60*60;
String result = jedis.setex(key.getBytes(),timeout,bytes);
return result;
}finally{
jedis.close();
}
}