更新时间:2018年12月13日14时24分 来源:传智播客 浏览次数:
一、前言1. 秒杀介绍
秒杀是电商系统非常常见的功能模块,是商家进行相关促销推广的常用方式。主要特点是商品库存有限,抢购时间有限。那么在系统设计之初就应该考虑在数量和时间有限的情况下导致的一个高并发以及高并发所带来的库存超卖的问题。
秒杀需要解决的问题:
1) 库存超卖
解决方案:
1) 悲观锁:synchronize 、 Lock
2) 乐观锁:数据库乐观锁版本号控制
2) 高并发情况下系统压力以及用户体验
解决方案: redis
本教程采用:redis中list类型达到令牌机制完成秒杀。用户抢redis中的令牌,抢到令牌的用户才能进行支付,支付成功之后可以生成订单,如果一定时间之内没有支付那么就由定时任务来归还令牌
2. 开发介绍
1) 开发工具: IntelliJ IDEA2017.3.5
2) JDK版本:1.7+
3) 数据库: mysql5.7 、 Redis
4) 技术:Spring、Spring Data Redis、mybatis
二、环境搭建1. 数据库表创建
/*商品表 */
CREATE
TABLE
`goods` (
`goods_id`
int
(11)
NOT
NULL
AUTO_INCREMENT,
`num`
int
(11)
DEFAULT
NULL
,
`goods_name`
varchar
(50)
DEFAULT
NULL
,
PRIMARY
KEY
(`goods_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2
DEFAULT
CHARSET=utf8;
insert
into
`goods`(`goods_id`,`num`,`goods_name`)
values
(1,100,
'iphone X'
);
/*订单表 */
CREATE
TABLE
`orders` (
`order_id`
int
(11)
NOT
NULL
AUTO_INCREMENT,
`good_id`
int
(11)
DEFAULT
NULL
,
`
user
`
varchar
(50)
DEFAULT
NULL
,
PRIMARY
KEY
(`order_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1163
DEFAULT
CHARSET=utf8;
2. redis安装 ( 略 )
3. 创建mavne项目,打包方式jar,pom.xml如下
<
properties
>
<
project.build.sourceEncoding
>UTF-8</
project.build.sourceEncoding
>
<
junit.version
>4.12</
junit.version
>
<
spring.version
>4.2.4.RELEASE</
spring.version
>
<
pagehelper.version
>4.0.0</
pagehelper.version
>
<
mybatis.version
>3.2.8</
mybatis.version
>
<
mybatis.spring.version
>1.2.2</
mybatis.spring.version
>
<
mybatis.paginator.version
>1.2.15</
mybatis.paginator.version
>
<
mysql.version
>5.1.32</
mysql.version
>
<
druid.version
>1.0.9</
druid.version
>
</
properties
>
<
dependencies
>
<!-- Spring -->
<
dependency
>
<
groupId
>org.springframework</
groupId
>
<
artifactId
>spring-context</
artifactId
>
<
version
>${spring.version}</
version
>
</
dependency
>
<
dependency
>
<
groupId
>org.springframework</
groupId
>
<
artifactId
>spring-beans</
artifactId
>
<
version
>${spring.version}</
version
>
</
dependency
>
<
dependency
>
<
groupId
>org.springframework</
groupId
>
<
artifactId
>spring-webmvc</
artifactId
>
<
version
>${spring.version}</
version
>
</
dependency
>
<
dependency
>
<
groupId
>org.springframework</
groupId
>
<
artifactId
>spring-jdbc</
artifactId
>
<
version
>${spring.version}</
version
>
</
dependency
>
<
dependency
>
<
groupId
>org.springframework</
groupId
>
<
artifactId
>spring-aspects</
artifactId
>
<
version
>${spring.version}</
version
>
</
dependency
>
<
dependency
>
<
groupId
>org.springframework</
groupId
>
<
artifactId
>spring-jms</
artifactId
>
<
version
>${spring.version}</
version
>
</
dependency
>
<
dependency
>
<
groupId
>org.springframework</
groupId
>
<
artifactId
>spring-context-support</
artifactId
>
<
version
>${spring.version}</
version
>
</
dependency
>
<
dependency
>
<
groupId
>org.springframework</
groupId
>
<
artifactId
>spring-test</
artifactId
>
<
version
>${spring.version}</
version
>
</
dependency
>
<
dependency
>
<
groupId
>junit</
groupId
>
<
artifactId
>junit</
artifactId
>
<
version
>4.9</
version
>
</
dependency
>
<
dependency
>
<
groupId
>com.alibaba</
groupId
>
<
artifactId
>fastjson</
artifactId
>
<
version
>1.2.28</
version
>
</
dependency
>
<
dependency
>
<
groupId
>javassist</
groupId
>
<
artifactId
>javassist</
artifactId
>
<
version
>3.11.0.GA</
version
>
</
dependency
>
<
dependency
>
<
groupId
>commons-codec</
groupId
>
<
artifactId
>commons-codec</
artifactId
>
<
version
>1.10</
version
>
</
dependency
>
<
dependency
>
<
groupId
>com.github.pagehelper</
groupId
>
<
artifactId
>pagehelper</
artifactId
>
<
version
>${pagehelper.version}</
version
>
</
dependency
>
<!-- Mybatis -->
<
dependency
>
<
groupId
>org.mybatis</
groupId
>
<
artifactId
>mybatis</
artifactId
>
<
version
>${mybatis.version}</
version
>
</
dependency
>
<
dependency
>
<
groupId
>org.mybatis</
groupId
>
<
artifactId
>mybatis-spring</
artifactId
>
<
version
>${mybatis.spring.version}</
version
>
</
dependency
>
<
dependency
>
<
groupId
>com.github.miemiedev</
groupId
>
<
artifactId
>mybatis-paginator</
artifactId
>
<
version
>${mybatis.paginator.version}</
version
>
</
dependency
>
<!-- MySql -->
<
dependency
>
<
groupId
>mysql</
groupId
>
<
artifactId
>mysql-connector-java</
artifactId
>
<
version
>${mysql.version}</
version
>
</
dependency
>
<!-- 连接池 -->
<
dependency
>
<
groupId
>com.alibaba</
groupId
>
<
artifactId
>druid</
artifactId
>
<
version
>${druid.version}</
version
>
</
dependency
>
<
dependency
>
<
groupId
>redis.clients</
groupId
>
<
artifactId
>jedis</
artifactId
>
<
version
>2.8.1</
version
>
</
dependency
>
<
dependency
>
<
groupId
>org.springframework.data</
groupId
>
<
artifactId
>spring-data-redis</
artifactId
>
<
version
>1.7.2.RELEASE</
version
>
</
dependency
>
<
dependency
>
<
groupId
>dom4j</
groupId
>
<
artifactId
>dom4j</
artifactId
>
<
version
>1.6.1</
version
>
</
dependency
>
<
dependency
>
<
groupId
>xml-apis</
groupId
>
<
artifactId
>xml-apis</
artifactId
>
<
version
>1.4.01</
version
>
</
dependency
>
</
dependencies
>
4. 数据访问层
利用mybatis逆向工程生成POJO,以及mapper接口和mapper映射文件。该部分自行操作
mybatis核心配置文件SqlMapConfig.xml
<?
xml
version
=
"1.0"
encoding
=
"UTF-8"
?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<
configuration
>
<
plugins
>
<!-- com.github.pagehelper 为 PageHelper 类所在包名 -->
<
plugin
interceptor
=
"com.github.pagehelper.PageHelper"
>
<!-- 设置数据库类型 Oracle,Mysql,MariaDB,SQLite,Hsqldb,PostgreSQL 六种数据库-->
<
property
name
=
"dialect"
value
=
"mysql"
/>
</
plugin
>
</
plugins
>
</
configuration
>
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql:
//localhost:3306/miaosha?characterEncoding=utf-8
jdbc.username=root
jdbc.password=root
redis配置属性文件redis-config.propertiesproperties
# Redis settings
# server IP
redis.host=127.0.0.1
# server port
redis.port=6379
# server pass
redis.pass=
# use dbIndex
redis.database=0
redis.maxIdle=1000
redis.maxWait=3000
5. spring配置文件
applicationContext-dao.xml
<!-- 加载配置文件 -->
<
context:property-placeholder
location
=
"classpath*:properties/*.properties"
/>
<!-- 数据库连接池 -->
<
bean
id
=
"dataSource"
class
=
"com.alibaba.druid.pool.DruidDataSource"
destroy-method
=
"close"
>
<
property
name
=
"url"
value
=
"${jdbc.url}"
/>
<
property
name
=
"username"
value
=
"${jdbc.username}"
/>
<
property
name
=
"password"
value
=
"${jdbc.password}"
/>
<
property
name
=
"driverClassName"
value
=
"${jdbc.driver}"
/>
<
property
name
=
"maxActive"
value
=
"10"
/>
<
property
name
=
"minIdle"
value
=
"5"
/>
</
bean
>
<!-- 让spring管理sqlsessionfactory 使用mybatis和spring整合包中的 -->
<
bean
id
=
"sqlSessionFactory"
class
=
"org.mybatis.spring.SqlSessionFactoryBean"
>
<!-- 数据库连接池 -->
<
property
name
=
"dataSource"
ref
=
"dataSource"
/>
<!-- 加载mybatis的全局配置文件 -->
<
property
name
=
"configLocation"
value
=
"classpath:mybatis/SqlMapConfig.xml"
/>
</
bean
>
<
bean
class
=
"org.mybatis.spring.mapper.MapperScannerConfigurer"
>
<
property
name
=
"basePackage"
value
=
"com.miaosha.demo.mapper"
/>
</
bean
>
applicationContext-redis.xml
<
context:property-placeholder
location
=
"classpath*:properties/*.properties"
ignore-unresolvable
=
"true"
/>
<!-- redis 相关配置 -->
<
bean
id
=
"poolConfig"
class
=
"redis.clients.jedis.JedisPoolConfig"
>
<
property
name
=
"maxIdle"
value
=
"${redis.maxIdle}"
/>
<
property
name
=
"maxTotal"
value
=
"2000"
/>
<
property
name
=
"maxWaitMillis"
value
=
"${redis.maxWait}"
/>
<
property
name
=
"testOnBorrow"
value
=
"true"
/>
</
bean
>
<
bean
id
=
"JedisConnectionFactory"
class
=
"org.springframework.data.redis.connection.jedis.JedisConnectionFactory"
p:host-name
=
"${redis.host}"
p:port
=
"${redis.port}"
p:password
=
"${redis.pass}"
p:pool-config-ref
=
"poolConfig"
/>
<
bean
id
=
"redisTemplate"
class
=
"org.springframework.data.redis.core.RedisTemplate"
>
<!--<!–开启事务支持–>-->
<!--<property name="enableTransactionSupport" value="true"/>-->
<
property
name
=
"connectionFactory"
ref
=
"JedisConnectionFactory"
/>
<!-- 序列化方式 建议key/hashKey采用StringRedisSerializer。 -->
<
property
name
=
"keySerializer"
>
<
bean
class
=
"org.springframework.data.redis.serializer.StringRedisSerializer"
/>
</
property
>
<
property
name
=
"hashKeySerializer"
>
<
bean
class
=
"org.springframework.data.redis.serializer.StringRedisSerializer"
/>
</
property
>
<
property
name
=
"valueSerializer"
>
<
bean
class
=
"org.springframework.data.redis.serializer.StringRedisSerializer"
/>
</
property
>
<
property
name
=
"hashValueSerializer"
>
<
bean
class
=
"org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"
/>
</
property
>
</
bean
>
三、代码实现
1.定义秒杀业务接口
/**
* 秒杀的接口
*/
public
interface
MiaoShaService {
/**
* 初始化所有商品的令牌
* @return
*/
public
boolean
initTokenToRedis();
/**
* 抢购令牌
* @param goodsId
* @param user
* @param num
* @return
*/
public
boolean
miaoshaTokenFromRedis(Integer goodsId,String user,Integer num);
/**
* 用户未支付 退还令牌
* @param user
* @return
*/
public
boolean
returnToken(String user);
/**
* 支付生成订单保存到数据库
* @param user
* @return
*/
public
boolean
payTokenToOrder(String user);
}
2.秒杀业务实现类
@Service
public
class
MiaoShaServiceImpl implements MiaoShaService
{
@Autowired
private GoodsMapper goodsMapper;
@Autowired
private OrderMapper orderMapper;
@Autowired
RedisTemplate redisTemplate;
@Override
public
boolean
initTokenToRedis
(
)
{
/
/
查询所有商品
/
/
根据时间 startTime
<
=
now
<
=
endTime
/
/
根据状态 已审核状态
List
<
Goods
>
goodsList
=
goodsMapper.selectByExample
(
null
)
;
for
(
Goods goods
:
goodsList
)
{
for
(
int i
=
0
;i
<
goods.getNum
(
)
;i
+
+
)
{
/
/
token_goods_
1
:
[token_
1
_
0
,
token_
1
_
1
,
token_
1
_
2
... token_
1
_
99
]
/
/
token_goods_
2
:
[token_
2
_
0
,
token_
2
_
1
,
token_
2
_
2
... token_
2
_
99
]
redisTemplate.boundListOps
(
"token_goods_"
+
goods.getGoodsId
(
)
)
.leftPush
(
"token_"
+
goods.getGoodsId
(
)
+
"_"
+
i
)
;
}
}
return
false
;
}
@Override
public
boolean
miaoshaTokenFromRedis
(
Integer goodsId
,
String user
,
Integer num
)
{
/
/
获取令牌
String token
=
(
String
)
redisTemplate.boundListOps
(
"token_goods_"
+
goodsId
)
.rightPop
(
)
;
if
(
token
=
=
null || token.
equals
(
""
)
)
{
return
false
;
}
else
{
/
/
记录当前用户已经抢购到令牌,证明当前这个用户可以取支付
/
/
用redis记录
String
yes
=
(
String
)
redisTemplate.boundValueOps
(
token
)
.
get
(
)
;
if
(
yes
!
=
null
&
&
yes
.
equals
(
"yes"
)
)
{
System.out.println
(
"当前token已经被支付,不能再抢购"
)
;
redisTemplate.boundListOps
(
"token_goods_"
+
goodsId
)
.
remove
(
1
,
token
)
;
return
false
;
}
System.out.println
(
user
)
;
redisTemplate.boundHashOps
(
"user_token"
)
.put
(
user
,
token
)
;
return
true
;
}
}
@Override
public
boolean
returnToken
(
String user
)
{
/
/
获得当前用户的令牌
String token
=
(
String
)
redisTemplate.boundHashOps
(
"user_token"
)
.
get
(
user
)
;
if
(
token
=
=
null || token.
equals
(
""
)
)
{
return
false
;
}
else
{
/
/
得到商品
id
String goodsId
=
token.split
(
"_"
)
[
1
];
redisTemplate.boundListOps
(
"token_goods_"
+
goodsId
)
.leftPush
(
token
)
;
return
true
;
}
}
@Override
public
boolean
payTokenToOrder
(
String user
)
{
/
/
获得当前用户的令牌
String token
=
(
String
)
redisTemplate.boundHashOps
(
"user_token"
)
.
get
(
user
)
;
if
(
token
=
=
null || token.
equals
(
""
)
)
{
return
false
;
}
else
{
/
/
如果在当前token已经被购买过,那么别人就不能抢当前的token或者不能再对该token进行支付
/
/
采用redis记录当前token已经被支付
/
/
redisTemplate.boundValueOps
(
"key"
)
.setIfAbsent
(
"value"
)
/
/
如果当前这个
key
有值,该方法就会返回
false
,
如果没有值,就会s设置为对应
value
,同时返回
true
boolean
flag
=
redisTemplate.boundValueOps
(
token
)
.setIfAbsent
(
"yes"
)
;
/
/
当前token第一次被支付 flag
=
true
if
(
flag
)
{
/
/
得到商品
id
String goodsId
=
token.split
(
"_"
)
[
1
];
Order order
=
new
Order
(
)
;
order.setUser
(
user
)
;
order.setGoodId
(
Integer.parseInt
(
goodsId
)
)
;
orderMapper.insert
(
order
)
;
/
/
用户刚好在支付时,定时任务执行
redisTemplate.boundHashOps
(
"user_token"
)
.
delete
(
user
)
;
/
/
有可能已经归还token
redisTemplate.boundListOps
(
"token_goods_"
+
goodsId
)
.
remove
(
1
,
token
)
;
/
/
移除token
,
有可能被别人抢到
}
return
true
;
}
}
}
package
com.miaosha.test;
import
com.miaosha.demo.service.MiaoShaService;
import
org.junit.Test;
import
org.junit.runner.RunWith;
import
org.springframework.beans.factory.annotation.Autowired;
import
org.springframework.data.redis.core.RedisTemplate;
import
org.springframework.test.context.ContextConfiguration;
import
org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import
java.util.ArrayList;
import
java.util.List;
import
java.util.concurrent.CountDownLatch;
@RunWith
(SpringJUnit4ClassRunner.
class
)
@ContextConfiguration
(locations = {
"classpath:spring/applicationContext-*.xml"
})
public
class
MiaoShaTest {
@Autowired
private
MiaoShaService miaoShaService;
//并发数量
public
static
int
bfCount =
1000
;
//记录成功个数
public
int
count =
0
;
//多线程辅助类,控制并发访问数量
CountDownLatch countDownLatch =
new
CountDownLatch(bfCount);
@Test
public
void
miaoshaToOrder()
throws
Exception{
boolean
flag = miaoShaService.initTokenToRedis();
if
(flag){
System.out.println(
"初始化token成功"
);
}
long
startTime = System.currentTimeMillis();
List<Thread> tList =
new
ArrayList<>();
for
(
int
i=
0
;i<bfCount;i++){
MiaoShaThread miaoShaThread =
new
MiaoShaThread(
1
,
"user_"
+i,
1
);
Thread thread =
new
Thread(miaoShaThread);
thread.start();
tList.add(thread);
countDownLatch.countDown();
}
for
(Thread t : tList){
t.join();
}
long
endTime = System.currentTimeMillis();
System.out.println(
"执行时间:"
+(endTime-startTime) );
System.out.println(
"成功的个数:"
+count);
}
@Autowired
RedisTemplate redisTemplate;
@Test
public
void
getTokenFromRefis(){
Long count = redisTemplate.boundListOps(
"token_goods_"
+
1
).size();
System.out.println(
"令牌数量:"
+count);
}
@Test
public
void
payToOrder(){
int
count_ =
1
;
for
(
int
i=
0
;i<
200
;i++){
boolean
b = miaoShaService.payTokenToOrder(
"user_"
+i);
if
(b){
count_++;
}
}
System.out.println(
"支付成功的人数:"
+count_);
}
//定时任务每秒执行退还令牌的操作
// 如果在规定时间5分钟之内没有支付就需要退还
@Test
public
void
returnToken(){
for
(
int
i=
0
;i<
1000
;i++) {
boolean
flag = miaoShaService.returnToken(
"user_"
+i);
if
(flag){
System.out.println(
"退还令牌成功"
);
}
}
}
class
MiaoShaThread
implements
Runnable{
private
Integer goodsId;
private
String user;
private
Integer num;
public
MiaoShaThread(Integer goodsId,String user,Integer num){
this
.goodsId=goodsId;
this
.user=user;
this
.num=num;
}
public
void
run() {
try
{
countDownLatch.await();
//操作redis 抢购token
boolean
flag = miaoShaService.miaoshaTokenFromRedis(goodsId, user, num);
if
(flag){
synchronized
(
this
){
count++;
}
}
}
catch
(Exception e){
e.printStackTrace();
}
}
}
}
注意:随着CountDownLatch设置并发数量越高,需要调整redis-config.properties属性中的redis.maxIdle属性
四、总结
本文介绍了利用redis的list数据类型模拟令牌队列来完成秒杀,主要解决库存超卖、高并发降低系统压力提高用户体验、解决乐观锁不能先到先得的问题。在单机上运行能够构建上万的请求利用redis抢购100个商品在几秒之内处理完成。本文并不是真是的秒杀业务场景,至少提供一种秒杀的解决思路,如果业务存在某些不确切的地方,欢迎留言交流,相互学习。