一、前言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>
<dependencies>
<dependency>
<groupid>org.springframework/<groupid>
<artifactid>spring-context/<artifactid>
<version>${spring.version}/<version>
<dependency>
<groupid>org.springframework/<groupid>
<artifactid>spring-beans/<artifactid>
<version>${spring.version}/<version>
<dependency>
<groupid>org.springframework/<groupid>
<artifactid>spring-webmvc/<artifactid>
<version>${spring.version}/<version>
<dependency>
<groupid>org.springframework/<groupid>
<artifactid>spring-jdbc/<artifactid>
<version>${spring.version}/<version>
<dependency>
<groupid>org.springframework/<groupid>
<artifactid>spring-aspects/<artifactid>
<version>${spring.version}/<version>
<dependency>
<groupid>org.springframework/<groupid>
<artifactid>spring-jms/<artifactid>
<version>${spring.version}/<version>
<dependency>
<groupid>org.springframework/<groupid>
<artifactid>spring-context-support/<artifactid>
<version>${spring.version}/<version>
<dependency>
<groupid>org.springframework/<groupid>
<artifactid>spring-test/<artifactid>
<version>${spring.version}/<version>
<dependency>
<groupid>junit/<groupid>
<artifactid>junit/<artifactid>
<version>4.9/<version>
<dependency>
<groupid>com.alibaba/<groupid>
<artifactid>fastjson/<artifactid>
<version>1.2.28/<version>
<dependency>
<groupid>javassist/<groupid>
<artifactid>javassist/<artifactid>
<version>3.11.0.GA/<version>
<dependency>
<groupid>commons-codec/<groupid>
<artifactid>commons-codec/<artifactid>
<version>1.10/<version>
<dependency>
<groupid>com.github.pagehelper/<groupid>
<artifactid>pagehelper/<artifactid>
<version>${pagehelper.version}/<version>
<dependency>
<groupid>org.mybatis/<groupid>
<artifactid>mybatis/<artifactid>
<version>${mybatis.version}/<version>
<dependency>
<groupid>org.mybatis/<groupid>
<artifactid>mybatis-spring/<artifactid>
<version>${mybatis.spring.version}/<version>
<dependency>
<groupid>com.github.miemiedev/<groupid>
<artifactid>mybatis-paginator/<artifactid>
<version>${mybatis.paginator.version}/<version>
<dependency>
<groupid>mysql/<groupid>
<artifactid>mysql-connector-java/<artifactid>
<version>${mysql.version}/<version>
<dependency>
<groupid>com.alibaba/<groupid>
<artifactid>druid/<artifactid>
<version>${druid.version}/<version>
<dependency>
<groupid>redis.clients/<groupid>
<artifactid>jedis/<artifactid>
<version>2.8.1/<version>
<dependency>
<groupid>org.springframework.data/<groupid>
<artifactid>spring-data-redis/<artifactid>
<version>1.7.2.RELEASE/<version>
<dependency>
<groupid>dom4j/<groupid>
<artifactid>dom4j/<artifactid>
<version>1.6.1/<version>
<dependency>
<groupid>xml-apis/<groupid>
<artifactid>xml-apis/<artifactid>
<version>1.4.01/<version>
4. 數據訪問層
利用mybatis逆向工程生成POJO,以及mapper接口和mapper映射文件。該部分自行操作
mybatis核心配置文件SqlMapConfig.xml
<configuration>
<plugins>
<plugin>
<property>
數據訪問 db.properties
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
<property-placeholder>
<bean>
destroy-method="close">
<property>
<property>
<property>
<property>
<property>
<property>
/<bean>
<bean>
<property>
<property>
<bean>
<property>
applicationContext-redis.xml
<property-placeholder>
<bean> /<bean>
<property>
<property>
<property>
<property>
<bean>
p:host-name="${redis.host}" p:port="${redis.port}" p:password="${redis.pass}" p:pool-config-ref="poolConfig"/>
<bean>
<property>
<property>
<bean>
<property>
<bean>
<property>
<bean>
<property>
<bean>
/<bean>
applicationContext-service.xml
<component-scan><component-scan>
三、代碼實現
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);/<goods>
for(Goods goods : goodsList){
for(int i=0;i<goods.getnum>
//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;
}
}
}
3. 秒殺測試類
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<>();/<thread>
for(int i=0;i<bfcount>
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個商品在幾秒之內處理完成。本文並不是真是的秒殺業務場景,至少提供一種秒殺的解決思路,如果業務存在某些不確切的地方,歡迎留言交流,相互學習。希望本文能夠對您有所幫助
![成都校區*精品*redis令牌機制實現秒殺](http://p2.ttnews.xyz/loading.gif)
閱讀更多 黑馬程序員成都中心 的文章