設計與開發
表結構
CREATE TABLE `t_product` (
`id` int(12) NOT NULL AUTO_INCREMENT COMMENT '產品編號',
`product_name` varchar(60) NOT NULL COMMENT '產品名稱',
`stock` int(10) NOT NULL COMMENT '庫存',
`price` decimal(16,2) NOT NULL COMMENT '單價',
`version` int(10) NOT NULL DEFAULT '0' COMMENT '版本號',
`note` varchar(255) DEFAULT NULL COMMENT '備註',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='產品信息表';
CREATE TABLE `t_purchase_record` (
`id` int(12) NOT NULL AUTO_INCREMENT COMMENT '編號',
`user_id` int(12) NOT NULL COMMENT '用戶編號',
`product_id` int(12) NOT NULL COMMENT '產品編號',
`price` decimal(16,2) NOT NULL COMMENT '價格',
`quantity` int(12) NOT NULL COMMENT '數量',
`sum` decimal(12,2) NOT NULL COMMENT '總價',
`purchase_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '購買日期',
`note` varchar(512) DEFAULT NULL COMMENT '備註',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='購買信息表';
- 判斷產品表 的產品 有沒有足夠的庫存 支持用戶的購買,如果有 則對產品 減庫存
- 然後在 將 購買信息 插入到購買記錄中,如果庫存不足,則返回交易失敗。
pom引用
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<!--不依賴Redis的異步客戶端lettuce -->
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--引入Redis的客戶端驅動jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
//連接池,可以不引用
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
//web 必須引用
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
//mybatis 必須引用
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
//驅動必須引用
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
MyBatis 開發持久層
pojo
@Alias("product")
public class ProductPo implements Serializable {
private static final long serialVersionUID = 3L;
private Long id;
private String productName;
private int stock;
private double price;
private int version;
private String note;
}
@Alias("purchaseRecord")
public class PurchaseRecordPo implements Serializable {
private static final long serialVersionUID = -3L;
private Long id;
private Long userId;
private Long productId;
private double price;
private int quantity;
private double sum;
private Timestamp purchaseTime;
private String note;
}
- 定義 Alias 的別名
mapper文件
ProductMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.springboot.chapter15.dao.ProductDao">
<!-- 獲取產品 -->
<select id="getProduct" parameterType="long" resultType="product">
select id, product_name as productName,
stock, price, version, note from t_product
where id=#{id}
</select>
<!-- 減庫存 -->
<update id="decreaseProduct">
update t_product set stock = stock - #{quantity},
version = version +1
where id = #{id}
</update>
</mapper>
PurchaseRecordMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.springboot.chapter15.dao.PurchaseRecordDao">
<insert id="insertPurchaseRecord" parameterType="purchaseRecord">
insert into t_purchase_record(
user_id, product_id, price, quantity, sum, purchase_date, note)
values(#{userId}, #{productId}, #{price}, #{quantity},
#{sum}, now(), #{note})
</insert>
</mapper>
dao
@Mapper
public interface ProductDao {
// 獲取產品
public ProductPo getProduct(Long id);
//減庫存,而@Param標明MyBatis參數傳遞給後臺
public int decreaseProduct(@Param("id") Long id, @Param("quantity") int quantity);
// public int decreaseProduct(@Param("id") Long id,
// @Param("quantity") int quantity, @Param("version") int version);
}
@Mapper
public interface PurchaseRecordDao {
public int insertPurchaseRecord(PurchaseRecordPo pr);
}
開發業務層 和 控制層
service
public interface PurchaseService {
/**
* 處理購買業務
* @param userId 用戶編號
* @param productId 產品編號
* @param quantity 購買數量
* @return 成功or失敗
*/
public boolean purchase(Long userId, Long productId, int quantity);
boolean purchaseRedis(Long userId, Long productId, int quantity);
boolean dealRedisPurchase(List<PurchaseRecordPo> prpList);
}
@Service
public class PurchaseServiceImpl implements PurchaseService {
@Autowired
private ProductDao productDao = null;
@Autowired
private PurchaseRecordDao purchaseRecordDao = null;
@Override
// 啓動Spring數據庫事務機制
@Transactional
public boolean purchase(Long userId, Long productId, int quantity) {
// 獲取產品
ProductPo product = productDao.getProduct(productId);
// 比較庫存和購買數量
if (product.getStock() < quantity) {
// 庫存不足
return false;
}
// 扣減庫存
productDao.decreaseProduct(productId, quantity);
// 初始化購買記錄
PurchaseRecordPo pr = this.initPurchaseRecord(userId, product, quantity);
// 插入購買記錄
purchaseRecordDao.insertPurchaseRecord(pr);
return true;
}
// 初始化購買信息
private PurchaseRecordPo initPurchaseRecord(Long userId, ProductPo product, int quantity) {
PurchaseRecordPo pr = new PurchaseRecordPo();
pr.setNote("購買日誌,時間:" + System.currentTimeMillis());
pr.setPrice(product.getPrice());
pr.setProductId(product.getId());
pr.setQuantity(quantity);
double sum = product.getPrice() * quantity;
pr.setSum(sum);
pr.setUserId(userId);
return pr;
}
//第一版完畢
}
controller
// REST風格控制器
@RestController
public class PurchaseController {
@Autowired
PurchaseService purchaseService = null;
// 定義JSP視圖
@GetMapping("/test")
public ModelAndView testPage() {
ModelAndView mv = new ModelAndView("test");
return mv;
}
@PostMapping("/purchase")
public Result purchase(Long userId, Long productId, Integer quantity) {
boolean success = purchaseService.purchaseRedis(userId, productId, quantity);
String message = success ? "搶購成功" : "搶購失敗";
Result result = new Result(success, message);
return result;
}
// 響應結果
class Result {
private boolean success = false;
private String message = null;
public Result() {
}
public Result(boolean success, String message) {
this.success = success;
this.message = message;
}
/**** setter and getter ****/
}
}
配置和測試
jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>購買產品測試</title>
<script type="text/javascript"
src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
</head>
<!--後面需要改寫這段JavaScript腳本進行測試-->
<script type="text/javascript">
var params = {
userId : 1,
productId : 1,
quantity : 3
};
// 通過POST請求後端
$.post("./purchase", params, function(result) {
alert(result.message);
});
for (var i = 1; i <= 50000; i++) {
var params = {
userId: 1,
productId: 1,
quantity: 1
};
// 通過POST請求後端,這裏的JavaScript會採用異步請求
$.post("./purchase", params, function (result) {
});
}
</script>
<body>
<h1>搶購產品測試</h1>
</body>
</html>
配置
spring.mvc.view.prefix=/WEB-INF/jsp/
spring.mvc.view.suffix=.jsp
########## 數據庫配置 ##########
spring.datasource.url=jdbc:mysql://localhost:3306/spring_boot_chapter15
spring.datasource.username=root
spring.datasource.password=123456
#spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.tomcat.max-idle=10
spring.datasource.tomcat.max-active=50
spring.datasource.tomcat.max-wait=10000
spring.datasource.tomcat.initial-size=5
# 採用隔離級別爲讀寫提交
spring.datasource.tomcat.default-transaction-isolation=2
########## MyBatis配置 ##########
# 映射文件
mybatis.mapper-locations=classpath:com/springboot/chapter15/mapper/*.xml
# 掃描別名
mybatis.type-aliases-package=com.springboot.chapter15.pojo
main方法
// 定義掃描包
@SpringBootApplication(scanBasePackages = "com.springboot.chapter15")
// 定義掃描MyBatis接口
@MapperScan(annotationClass = Mapper.class, basePackages = "com.springboot.chapter15")
@EnableScheduling
public class Chapter15Application {
public static void main(String[] args) {
SpringApplication.run(Chapter15Application.class, args);
}
}
高併發測試
-
線程1:讀取庫存爲1,可購買
-
線程2:讀取庫存爲1,可購買
-
線程1,扣減庫存。此時庫存爲0
-
線程2:扣減庫存。此時庫存爲 -1 。超發了。
-
線程1:插入交易記錄。
-
線程2:插入交易記錄,錯誤,庫存已經不足。
-
線程2,此時並不會 感知 線程1 的這個操作。而是按照 原來讀取到的1,進行扣減。
-
這樣就會 出現 -1 。
悲觀鎖
- 共享的數據 被 多個線程 所 修改,無法保證 其 執行順序。
- 如果一個數據庫事務 讀到 產品後,就將數據 直接鎖定,不允許其他線程讀寫,直到 當前事務完成後,才釋放這條數據,則不會出現。超發的問題。
<!-- 獲取產品 -->
<select id="getProduct" parameterType="long" resultType="product">
select id, product_name as productName,
stock, price, version, note from t_product
where id=#{id} for update
</select>
- for update ,這樣 數據庫事務 執行的過程中,就會鎖定 查詢出來的數據,其他事務將 不能再對其進行讀寫(其他線程執行這行代碼的時候,就會進入等待)。
- 不加鎖用28s,加悲觀鎖 用了 33秒。
- 加入事務2 得到商品信息的鎖,那麼事務 1,3,n 就必須等待 持有 商品信息的 事務 2,結束後 釋放商品信息,才能去搶奪 商品信息,這樣就會有大量的線程 被 掛機 和 等待。
- 悲觀鎖:使用數據庫內部的鎖,對記錄進行加鎖。
- 悲觀鎖:也成 獨佔鎖 或 排他鎖
樂觀鎖
樂觀鎖設計
-
雖然 悲觀鎖 可以解決 高併發的超發 現象,但並不是一個高效的方案
-
樂觀鎖:是一種,不使用 數據庫鎖 和 不阻塞 線程 併發 的方案
-
非獨佔鎖, 無阻塞鎖
-
一個線程 先讀取 既有的商品 庫存數據,保存起來,(舊值)
-
等到 需要對 共享數據 做修改時,會事先 將 保存的舊值庫存 與 當前數據庫的 庫存進行比較。
-
如果 一致,就認爲沒有被修改過, 否則就認爲 已經被修改過,(當前計算不被信任,不在修改數據)
-
保存 舊值——處理業務——舊值 與 當前數據庫存 一致
- ——是 扣減庫存
- ——否 不執行邏輯
ABA現象 (先A在B,又A)
-
這個方案 就是 多線程的概念,CAS compare and swap
-
會引發 ABA 問題
-
線程1 讀取到 A件,保存爲A件
-
線程2 讀取到A件,保存爲A件
-
線程2 扣減庫存C件,剩下B件。(當前數據庫爲A件,與線程舊值一致,成功)
-
線程1,計算剩餘商品的價格(總價),會 按照剩餘B件 計算。
-
線程2,取消購買,庫存回退A件。
- 此時 線程 1 的結果是錯的。
-
線程1 計算商品總價格的時候,當前庫存 會被線程 2 所修改。
-
稱爲 ABA問題
-
線程1 在計算商品 總價格時,
-
當前 庫存是一個變化的值,這樣就可能出現計算錯誤。
-
共享值回退,導致了數據的不一致。
-
引入版本號
解決ABA,引入版本號
-
規定:只要操作 過程中 修改共享值,無論 業務正常 回退 還是異常
-
版本號 只增不減
-
線程1 讀取版本號爲1
-
線程2 讀取版本號爲 1
-
線程2 扣減庫存C件, 剩下B件。 版本爲2
-
線程2 取消購買,庫存回退爲A件。 版本爲 3
-
線程1 ,計算商品價格 記錄的是 版本爲1 ,當前已經爲 3 了。所以 取消業務。
<!-- 減庫存 -->
<update id="decreaseProduct">
update t_product set stock = stock - #{quantity},
version = version +1
where id = #{id} and version = #{version}
</update>
- and version = #{version} 判斷,有沒有別的事務已經修改過數據
- 一旦 版本號 修改失敗,則什麼數據 也不會 觸發更新
使用樂觀鎖,版本號處理
public int decreaseProduct(@Param("id") Long id, @Param("quantity") int quantity, @Param("version") int version);
UPDATE t_product //更新這個表
SET stock = stock - 1, //設置爲:剩餘數量 -1 ,
version = version + 1 //版本號 +1
WHERE
id = '1' //id 爲 1 的值
AND version = 1 //並且 版本號,也是 1 ,才更新。
// 啓動Spring數據庫事務機制
@Transactional(isolation =Isolation.READ_COMMITTED)
public boolean purchase(Long userId, Long productId, int quantity) {
// 獲取產品(線程舊值)
ProductPo product = productDao.getProduct(productId);
// 比較庫存和購買數量
if (product.getStock() < quantity) {
// 庫存不足
return false;
}
// 獲取當前版本號
int version = product.getVersion();
// 扣減庫存,同時將當前版本號發送給後臺去比較
int result = productDao.decreaseProduct(productId, quantity, version);
// 如果更新數據失敗,說明數據在多線程中被其他線程修改,導致失敗返回
if (result == 0) {
return false;
}
// 初始化購買記錄
PurchaseRecordPo pr = this.initPurchaseRecord(userId, product, quantity);
// 插入購買記錄
purchaseRecordDao.insertPurchaseRecord(pr);
return true;
}
- 耗時 27s ,5萬個請求過去,還有庫存。沒有超發。
- 因爲加入了版本號的判斷,大量的請求得到失敗的結果。
- 這個失敗率比較高。
樂觀鎖 加入 重入機制
- 一旦更新失敗,就重新做 一次,稱樂觀鎖爲 可重入的鎖
- 其原理:一單發現 版本號被更新,不是結束請求,而是重新做一次流程。直到成功爲止
- 會帶來另一個問題:造成大量的SQL被執行
- 一個請求需要執行3條SQL,重入需要 3次,那麼就要12條sql ,會給數據帶來壓力
- 爲了克服:使用 限制 時間 或 重入次數。壓制過多的SQL
使用是時間戳 限制重入
-
一個請求 限制 100ms的生存期
-
100ms 內發生版本號衝突,則重試
@Override // 啓動Spring數據庫事務機制 @Transactional(isolation = Isolation.READ_COMMITTED) public boolean purchase(Long userId, Long productId, int quantity) { // 當前時間 long start = System.currentTimeMillis(); // 循環嘗試直至成功 while (true) { // 循環時間 long end = System.currentTimeMillis(); // 如果循環時間大於100毫秒返回終止循環 if (end - start > 100) { return false; } // 獲取產品 ProductPo product = productDao.getProduct(productId); // 獲取當前版本號 int version = product.getVersion(); // 比較庫存和購買數量 if (product.getStock() < quantity) { // 庫存不足 return false; } // 扣減庫存,同時將當前版本號發送給後臺去比較 int result = productDao.decreaseProduct(productId, quantity, version); // 如果更新數據失敗,說明數據在多線程中被其他線程修改, // 導致失敗,則通過循環重入嘗試購買商品 if (result == 0) { continue; } // 初始化購買記錄 PurchaseRecordPo pr = this.initPurchaseRecord(userId, product, quantity); // 插入購買記錄 purchaseRecordDao.insertPurchaseRecord(pr); return true; } } // 當前時間 long start = System.currentTimeMillis(); // 循環嘗試直至成功 while (true) { // 循環時間 long end = System.currentTimeMillis(); // 如果循環時間大於100毫秒返回終止循環 if (end - start > 100) { return false; } // 導致失敗,則通過循環重入嘗試購買商品 if (result == 0) { continue; } return true; }
-
按照時間戳 重入 也有一個弊端:系統會隨自身的忙碌,而大大減少重入的次數
-
因此有時候也會採用 按照次數重入
按照限定次數 重入的樂觀鎖
@Override
// 啓動Spring數據庫事務機制,並將隔離級別設置爲讀寫提交
@Transactional(isolation = Isolation.READ_COMMITTED)
public boolean purchase(Long userId, Long productId, int quantity) {
// 循環嘗試直至成功
for (int i = 0; i < 3; i++) {
// 獲取產品
ProductPo product = productDao.getProduct(productId);
// 比較庫存和購買數量
if (product.getStock() < quantity) {
// 庫存不足
return false;
}
// 獲取當前版本號
int version = product.getVersion();
// 扣減庫存,同時將當前版本號發送給後臺去比較
int result = productDao.decreaseProduct(productId, quantity,version);
// 如果更新數據失敗,說明數據在多線程中被其他線程修改,
// 導致失敗,則通過循環重入嘗試購買商品
if (result == 0) {
continue;
}
// 初始化購買記錄
PurchaseRecordPo pr = this.initPurchaseRecord(userId, product, quantity);
// 插入購買記錄
purchaseRecordDao.insertPurchaseRecord(pr);
return true;
}
return false;
}
- 樂觀鎖:不使用 數據庫鎖的機制
- 不會造成線程的阻塞,只是採用多版本號 機制來實現
- 因爲版本的衝突造成了 請求失敗的概率增加 ——往往需要重入的機制機制。
- 重入又會造成 多執行SQL,可以時間戳 或限制重入次數。
- 或者用 redis
使用redis處理高併發
-
數據庫 是 寫入磁盤的過程。
-
redis : 寫入內存 (是 數據庫的 幾倍 或 數十倍)
- 其命令方式,運算能力比較薄弱(redis lua命令代替)。
- redis lua 執行中 ,具備 原子性
- 使用 redis 去 替代 數據庫作爲 響應用戶的數據載體
- 要處理 redis 存儲的不穩定,還需要 有一定的機制 將redis 存儲的數據刷入數據庫中
-
設計思路
- 先用 redis響應高併發用戶的請求
- 及時的將 數據保存到數據庫,啓用定時任務去查找redis,將它們保存到數據庫中
redis 配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<!--不依賴Redis的異步客戶端lettuce -->
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--引入Redis的客戶端驅動jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
#最小 活躍 最大 最大等待
spring.redis.jedis.pool
.min-idle=5
.max-active=10
.max-idle=10
.max-wait=2000
# port host pwd timeout
spring.redis
.port=6379
.host=192.168.2.198
.password=123456
.timeout=1000
- 自動生成,redistTemplate,StringRedisTemplate
Redis 的 Lua編程
@Autowired
StringRedisTemplate stringRedisTemplate = null;
String purchaseScript =
// 先將產品編號保存到集合中
" redis.call('sadd', KEYS[1], ARGV[2]) \n"
// 購買列表
+ "local productPurchaseList = KEYS[2]..ARGV[2] \n"
// 用戶編號
+ "local userId = ARGV[1] \n"
// 產品key
+ "local product = 'product_'..ARGV[2] \n"
// 購買數量
+ "local quantity = tonumber(ARGV[3]) \n"
// 當前庫存
+ "local stock = tonumber(redis.call('hget', product, 'stock')) \n"
// 價格
+ "local price = tonumber(redis.call('hget', product, 'price')) \n"
// 購買時間
+ "local purchase_date = ARGV[4] \n"
// 庫存不足,返回0
+ "if stock < quantity then return 0 end \n"
// 減庫存
+ "stock = stock - quantity \n"
+ "redis.call('hset', product, 'stock', tostring(stock)) \n"
// 計算價格
+ "local sum = price * quantity \n"
// 合併購買記錄數據
+ "local purchaseRecord = userId..','..quantity..','"
+ "..sum..','..price..','..purchase_date \n"
// 保存到將購買記錄保存到list裏
+ "redis.call('rpush', productPurchaseList, purchaseRecord) \n"
// 返回成功
+ "return 1 \n";
// Redis購買記錄集合前綴
private static final String PURCHASE_PRODUCT_LIST = "purchase_list_";
// 搶購商品集合
private static final String PRODUCT_SCHEDULE_SET = "product_schedule_set";
// 32位SHA1編碼,第一次執行的時候先讓Redis進行緩存腳本返回
private String sha1 = null;
@Override
public boolean purchaseRedis(Long userId, Long productId, int quantity) {
// 購買時間
Long purchaseDate = System.currentTimeMillis();
Jedis jedis = null;
try {
// 獲取原始連接
jedis = (Jedis) stringRedisTemplate
.getConnectionFactory().getConnection().getNativeConnection();
// 如果沒有加載過,則先將腳本加載(緩存)到Redis服務器,讓其返回sha1
if (sha1 == null) {
sha1 = jedis.scriptLoad(purchaseScript);
}
// 執行腳本,返回結果
Object res = jedis.evalsha(sha1, 2, PRODUCT_SCHEDULE_SET,
PURCHASE_PRODUCT_LIST, userId + "", productId + "",
quantity + "", purchaseDate + "");
//sha1 代表32位 的 SHA1編碼
//2 代表 將前面 兩個參數 以鍵 的形式 傳遞到 腳本中
//後面兩個常量是鍵 。 lua 腳本中 用 keys[index] 表示。keys[1] 第一個鍵。
//第二個參數 之後,則都是腳本的參數 Lua argv[index]表示,同樣 keys[1] 表示
//
Long result = (Long) res;
return result == 1;
} finally {
// 關閉jedis連接
if (jedis != null && jedis.isConnected()) {
jedis.close();
}
}
}
- 保存購買信息
- 將購買記錄 保存到 數據庫中
@Override
// 當運行方法啓用新的獨立事務運行。回滾時,只會回滾 這個方法的內部事務。
@Transactional(propagation = Propagation.REQUIRES_NEW)
public boolean dealRedisPurchase(List<PurchaseRecordPo> prpList) {
for (PurchaseRecordPo prp : prpList) {
purchaseRecordDao.insertPurchaseRecord(prp);
productDao.decreaseProduct(prp.getProductId(), prp.getQuantity());
}
return true;
}
定時任務,把redis中數據保存到數據
-
@EnableScheduling
@Service
public class TaskServiceImpl implements TaskService {
@Autowired
private StringRedisTemplate stringRedisTemplate = null;
@Autowired
private PurchaseService purchaseService = null;
private static final String PRODUCT_SCHEDULE_SET = "product_schedule_set";
private static final String PURCHASE_PRODUCT_LIST = "purchase_list_";
// 每次取出1000條,避免一次取出消耗太多內存
private static final int ONE_TIME_SIZE = 1000;
// @Override
// 每天半夜1點鐘開始執行任務
// @Scheduled(cron = "0 0 1 * * ?") 秒 分 時 天 月 星期
// 下面是用於測試的配置,每分鐘執行一次任務
@Scheduled(fixedRate = 1000 * 60)
public void purchaseTask() {
System.out.println("定時任務開始......");
Set<String> productIdList
= stringRedisTemplate.opsForSet().members(PRODUCT_SCHEDULE_SET);
List<PurchaseRecordPo> prpList = new ArrayList<>();
for (String productIdStr : productIdList) {
//轉換成 Long
Long productId = Long.parseLong(productIdStr);
//常量 + 上 long
String purchaseKey = PURCHASE_PRODUCT_LIST + productId;
//綁定這個 list 操作
BoundListOperations<String, String> ops
= stringRedisTemplate.boundListOps(purchaseKey);
// 計算記錄數
long size = stringRedisTemplate.opsForList().size(purchaseKey);
//如果 長度 / 1000 == 0 ,就長度/100 ,否則就 長度 +1
Long times = size % ONE_TIME_SIZE == 0 ?
size / ONE_TIME_SIZE : size / ONE_TIME_SIZE + 1;
for (int i = 0; i < times; i++) {
// 獲取至多TIME_SIZE個搶紅包信息
List<String> prList = null;
if (i == 0) {
prList = ops.range(i * ONE_TIME_SIZE,
(i + 1) * ONE_TIME_SIZE);
} else {
prList = ops.range(i * ONE_TIME_SIZE + 1,
(i + 1) * ONE_TIME_SIZE);
}
for (String prStr : prList) {
PurchaseRecordPo prp
= this.createPurchaseRecord(productId, prStr);
prpList.add(prp);
}
try {
// 採用該方法採用新建事務的方式,這樣不會導致全局事務回滾
purchaseService.dealRedisPurchase(prpList);
} catch (Exception ex) {
ex.printStackTrace();
}
// 清除列表爲空,等待重新寫入數據
prpList.clear();
}
// 刪除購買列表
stringRedisTemplate.delete(purchaseKey);
// 從商品集合中刪除商品
stringRedisTemplate.opsForSet()
.remove(PRODUCT_SCHEDULE_SET, productIdStr);
}
System.out.println("定時任務結束......");
}
private PurchaseRecordPo createPurchaseRecord(
Long productId, String prStr) {
String[] arr = prStr.split(",");
Long userId = Long.parseLong(arr[0]);
int quantity = Integer.parseInt(arr[1]);
double sum = Double.valueOf(arr[2]);
double price = Double.valueOf(arr[3]);
Long time = Long.parseLong(arr[4]);
Timestamp purchaseTime = new Timestamp(time);
PurchaseRecordPo pr = new PurchaseRecordPo();
pr.setProductId(productId);
pr.setPurchaseTime(purchaseTime);
pr.setPrice(price);
pr.setQuantity(quantity);
pr.setSum(sum);
pr.setUserId(userId);
pr.setNote("購買日誌,時間:" + purchaseTime.getTime());
return pr;
}
}
測試
-
rieds命令 執行 命令
-
hmset product_1 id 1 stock 3000 price 5.00
- redis裏面會存在鍵:product_1
- 有3列:第一列:id 1 。 stock 2997。 price 5:00
-
從性能上來講,只需要6s的時間,比鎖 快了 數倍
-
使用redis 建議使用 獨立的Redis 服務器,做好備份,容災。
// Redis購買記錄集合前綴
private static final String PURCHASE_PRODUCT_LIST = "purchase_list_";
// 搶購商品集合
private static final String PRODUCT_SCHEDULE_SET = "product_schedule_set";
-
執行過之後redis,product_schedule_set 爲 1 (row) 1 (value)
-
purchase_list_1 爲: 1 1,1,5,5,1593581880782
-
腳本的執行返回值爲 1
-
每次搶購 product_1 stock會減少
-
purchase_list_1 會增加一行
-
定時任務結束後 product_1 之外的兩個 redis 清楚