欧美第十页,成人网成人A片,宾馆内激干人妻,偷偷内射,一区二区另类TS

秒殺封裝win10(封裝win10教程)

前沿拓展:


秒殺業(yè)務(wù)的參考實(shí)現(xiàn)

本節(jié)從功能入手重點(diǎn)介紹Spring Cloud秒殺實(shí)戰(zhàn)業(yè)務(wù)處理的3層實(shí)現(xiàn):dao層、service層、controller層。

秒殺的功能模塊和接口設(shè)計(jì)

秒殺系統(tǒng)的實(shí)現(xiàn)有多種多樣的版本,本節(jié)從方便演示的角度出發(fā)設(shè)計(jì)一個(gè)相當(dāng)簡單的秒殺練習(xí)版本,具體分為3個(gè)主要的模塊:

(1)seckill-web模塊:此模塊是一個(gè)**的Spring Boot程序,作為一個(gè)靜態(tài)的Web服務(wù)器**運(yùn)行,主要運(yùn)行秒殺的前端頁面、腳本。在生產(chǎn)場景中,為了提高性能,可以將這個(gè)模塊的所有靜態(tài)資源全部遷移到Nginx高性能Web服務(wù)器。

(2)seckill-provider模塊:秒殺的后端Spring Cloud微服務(wù)提供者主要運(yùn)行獲取秒殺令牌、秒殺訂單等后端相關(guān)接口。

(3)uaa-provider模塊:用戶賬號與認(rèn)證(UAA)的后端SpringCloud微服務(wù)提供者主要運(yùn)行用戶認(rèn)證、用戶信息相關(guān)的后端接口。

以上3個(gè)模塊的關(guān)系為:seckill-web模塊作為靜態(tài)資源程序會將秒殺的**作頁面呈現(xiàn)給用戶,seckill-web的頁面會根據(jù)用戶的**作將相應(yīng)的URL接口,通過Nginx外部**跳過內(nèi)部**Zuul直接發(fā)送給后端的uaa-provider和seckill-provider微服務(wù)提供者。為什么要跳過Zuul內(nèi)部**呢?內(nèi)部**需要對請求的URL進(jìn)行用戶權(quán)限驗(yàn)證,如果請求沒有帶token或者沒有通過驗(yàn)證,請求就會被攔截并返回未授權(quán)的錯誤。

為了在練習(xí)時(shí)調(diào)試方便,建議直接跳過Zuul內(nèi)部**的權(quán)限驗(yàn)證功能,通過Nginx的反向**將請求直接**到后端的微服務(wù)提供者。

在秒殺練習(xí)系統(tǒng)中,三個(gè)模塊的關(guān)系如圖10-7所示。

秒殺封裝win10(封裝win10教程)

圖10-7 秒殺練習(xí)系統(tǒng)中三個(gè)模塊的關(guān)系

本秒殺練習(xí)系統(tǒng)中的秒殺**作流程大致有以下4步:

(1)前端設(shè)置秒殺用戶。

在用戶點(diǎn)擊后,seckill-web的前端頁面將通過請求uaa-provider服務(wù)的/api/user/detail/v1接口獲取用戶信息。在實(shí)際的秒殺場景中這一步是不需要的,因?yàn)檫@一步所獲取的用戶信息就是當(dāng)前登錄用戶本人的信息。

(2)前端設(shè)置秒殺商品。

seckill-web的前端頁面通過請求seckill-provider服務(wù)的/api/seckill/good/detail/v1接口獲取所需要的秒殺商品。而在seckill-provider服務(wù)后端會將商品的庫存信息緩存到Redis,方便下一步的秒殺令牌的獲取。

(3)前端獲取秒殺令牌。

seckill-web的前端頁面通過請求seckill-provider服務(wù)的/api/seckill/redis/token/v1接口獲取商品的秒殺令牌,執(zhí)行秒殺**作,減少商品的Redis庫存。后端接口第一減Redis庫存量,如果減庫存成功,就生成秒殺專用的令牌存入Redis,在下一步用戶下單時(shí)拿來進(jìn)行驗(yàn)證。如果扣減Redis庫存失敗,就返回對應(yīng)的錯誤提示。這一步**作沒有涉及數(shù)據(jù)庫,對庫存的減少**作直接在Redis中完成,所扣減的并不是真正的商品庫存。

(4)前端用戶下單。

seckill-web的前端頁面通過請求seckill-provider服務(wù)的/api/seckill/redis/do/v1接口執(zhí)行真正的下單**作。后端接口會判斷秒殺專用的token令牌是否有效,如果有效,就執(zhí)行真正的下單**作,在數(shù)據(jù)庫中扣減庫存和生成秒殺訂單,第二返回給前端。

秒殺練習(xí)系統(tǒng)的秒殺業(yè)務(wù)流程如圖10-8所示。

秒殺封裝win10(封裝win10教程)

圖10-8 秒殺練習(xí)系統(tǒng)中的秒殺業(yè)務(wù)流程

在開發(fā)過程中,為了使得來自seckill-web前端頁面的請求能夠順利地跳過內(nèi)部**Zuul而直接發(fā)送給后端的微服務(wù)提供者uaa-provider和seckill-provider,這里特意配置了一份專門的Nginx配置文件nginx-seckill.conf,對秒殺練習(xí)的三大模塊進(jìn)行定制化的反向**配置,在啟動Nginx的腳本openresty-start.sh文件中使用這份配置文件即可。

配置文件nginx-seckill.conf的核心配置如下:

server {
listen 80 default;
server_name nginx.server *.nginx.server;
default_type 'text/html';
charset utf-8;
#默認(rèn)的**
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forward-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-Nginx-Proxy true;
#**到配置的上游,zuul**
proxy_pass http://zuul;
}
#用戶服務(wù):開發(fā)調(diào)試的反向**配置
location ^~ /uaa-provider/ {
#**到Windows開發(fā)環(huán)境
#proxy_pass http://192.168.233.1:7702/;
#**到自驗(yàn)證CentOS環(huán)境
proxy_pass http://192.168.233.128:7702/uaa-provider/ ;
}
#秒殺服務(wù):開發(fā)調(diào)試的反向**配置
location ^~ /seckill-provider/ {
#**到Windows開發(fā)環(huán)境
proxy_pass http://192.168.233.1:7701/seckill-provider/ ;
}
#秒殺Web頁面:開發(fā)調(diào)試的反向**配置
location ^~ /seckill-web/ {
#**到Windows開發(fā)環(huán)境
proxy_pass http://192.168.233.1:6601/seckill-web/ ;
}

}

由于筆者在開發(fā)過程中,seckill-web、seckill-provider兩個(gè)進(jìn)程在IDEA中(Windows開發(fā)環(huán)境)啟動,而uaa-provider進(jìn)程運(yùn)行在自驗(yàn)證CentOS環(huán)境(虛擬機(jī))中,因此進(jìn)行了上面的反向**配置。更多有關(guān)環(huán)境和運(yùn)行的內(nèi)容使用視頻方式介紹起來更加直接,所以請查看瘋狂創(chuàng)客圈社群的秒殺練習(xí)演示視頻。

接下來,為大家介紹秒殺練習(xí)的秒殺**作流程的特點(diǎn),有以下3點(diǎn):

(1)增加了獲取秒殺令牌的環(huán)節(jié),將秒殺和下單**作分離。

這樣做的好處有兩方面:一方面,可以讓秒殺**作和下單**作從執(zhí)行上進(jìn)行分離,使得秒殺**作可以**于訂單相關(guān)業(yè)務(wù);另一方面,秒殺接口可以阻擋大部分并發(fā)流程,從而避免讓低效率的下單**作耗費(fèi)大量的計(jì)算資源。

(2)前端缺少了輪詢環(huán)節(jié)。

在生產(chǎn)場景中,用戶獲取令牌后,前端應(yīng)該會自動發(fā)起下單**作,第二通過前端Ajax腳本輪詢是否下單成功。本練習(xí)實(shí)例為了清晰地展示秒殺**作過程,將自動下單**作修改成了手動下單**作,并且,由于后端下單沒有經(jīng)過消息隊(duì)列進(jìn)行異步處理,因此前端也不需要進(jìn)行結(jié)果的輪詢。

(3)后端缺少失效令牌的庫存恢復(fù)**作。

在生產(chǎn)場景中,存在用戶拿到令牌而不去完成下單的情況,導(dǎo)致令牌失效。所以,后端需要有定時(shí)任務(wù)對秒殺令牌進(jìn)行有效性檢查,如果令牌沒有被使用或者生效,就需要恢復(fù)Redis中的秒殺庫存,方便后面的請求去秒殺。無效令牌檢查的定時(shí)任務(wù)可以設(shè)置為每分鐘一次或者每兩分鐘一次,以保障被無效令牌消耗的庫存能夠及時(shí)得到恢復(fù)。

數(shù)據(jù)表和PO實(shí)體類設(shè)計(jì)

秒殺系統(tǒng)的表設(shè)計(jì)相對簡單清晰,主要涉及兩張核心表:秒殺商品表和訂單表。

當(dāng)然,實(shí)際秒殺場景肯定不止這兩張表,還有付款信息相關(guān)的其他配套表等,出于學(xué)習(xí)的目的,這里我們只考慮秒殺系統(tǒng)的核心表,不考慮實(shí)際系統(tǒng)涉及的其他配套表。

與兩個(gè)核心表相對應(yīng),系統(tǒng)中設(shè)計(jì)了兩個(gè)PO實(shí)體類:秒殺商品PO類和秒殺訂單PO類。本 文的命名規(guī)范:Java實(shí)體類統(tǒng)一使用PO作為后綴,Java傳輸類統(tǒng)一使用DTO作為后綴。

由于本案例使用JPA作為持久層框架,可以基于PO類逆向地生成數(shù)據(jù)庫的表,因此這里不對數(shù)據(jù)表的結(jié)構(gòu)進(jìn)行展開說明,而是對PO類進(jìn)行說明。

秒殺商品PO類SeckillGoodPO的代碼如下:

package com.crazymaker.springcloud.seckill.dao.po;
//省略import
/**
*秒殺商品PO
*/
@Entity
@Table(name = "SECKILL_GOOD")
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class SeckillGoodPO implements Serializable
{
//商品ID
@Id
@GenericGenerator(
name = "snowflakeIdGenerator",
strategy = "com.crazymaker.springcloud.standard.hibernate
.CommonSnowflakeIdGenerator")
@Generate**alue(strategy = GenerationType.IDENTITY, generator = "snowflakeIdGenerator")
@Column(name = "GOOD_ID", unique = true, nullable = false, length = 8)
private Long id;
//商品標(biāo)題
@Column(name = "GOOD_TITLE", length = 400)
private String title;
//商品標(biāo)題
@Column(name = "GOOD_IMAGE", length = 400)
private String image;
商品原價(jià)格 //商品原價(jià)格
@Column(name = "GOOD_PRICE")
private BigDecimal price;
//商品秒殺價(jià)格
@Column(name = "COST_PRICE")
private BigDecimal costPrice;
//創(chuàng)建時(shí)間
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Column(name = "CREATE_TIME")
private Date createTime;
//秒殺開始時(shí)間
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Column(name = "START_TIME")
private Date startTime;
//秒殺結(jié)束時(shí)間
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Column(name = "END_TIME")
private Date endTime;
//剩余庫存數(shù)量
@Column(name = "STOCK_COUNT")
private long stockCount;
//原始庫存數(shù)量
@Column(name = "raw_stock")
private long rawStockCount;
}
秒殺訂單PO類SeckillOrderPO的代碼如下:
package com.crazymaker.springcloud.seckill.dao.po;
//省略import
/**
*秒殺訂單PO(對應(yīng)于秒殺訂單表)
*/
@Entity
@Table(name = "SECKILL_ORDER")
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class SeckillOrderPO implements Serializable
{
//訂單ID
@Id
@GenericGenerator(
name = "snowflakeIdGenerator",
strategy = "com.crazymaker.springcloud.standard.hibernate.
CommonSnowflakeIdGenerator")
@Generate**alue(strategy = GenerationType.IDENTITY, generator = "snowflakeIdGenerator")
@Column(name = "ORDER_ID", unique = true, nullable = false, length = 8)
private Long id;
//支付金額
@Column(name = "PAY_MONEY")
private BigDecimal money;
//秒殺的用戶ID
@Column(name = "USER_ID")
private Long userId;
//創(chuàng)建時(shí)間
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") @Column(name = "CREATE_TIME")
private Date createTime;
//支付時(shí)間
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Column(name = "PAY_TIME")
private Date payTime;
//秒殺商品ID
@Column(name = "GOOD_ID")
private Long goodId;
//訂單狀態(tài),-1:無效,0:成功,1:已付款
@Column(name = "STATUS")
private Short status;
}

在秒殺系統(tǒng)中,SECKILL_GOOD商品表的GOOD_ID字段和SECKILL_ORDER訂單表中的GOOD_ID字段在業(yè)務(wù)邏輯上存在一對多的關(guān)系,但是不建議在數(shù)據(jù)庫層面使用表與表之間的外鍵關(guān)系。為什么呢?因?yàn)槿绻霘⒂唵瘟烤薮?,就必須進(jìn)行分庫分表,這時(shí)SECKILL_ORDER表和SECKILL_GOOD表中GOOD_ID相同的數(shù)據(jù)可能分布在不同的數(shù)據(jù)庫中,所以數(shù)據(jù)庫表層面的關(guān)聯(lián)關(guān)系可能會導(dǎo)致維護(hù)起來非常困難。

使用分布式ID生成器

在實(shí)際開發(fā)中,很多項(xiàng)目為了應(yīng)付交付和追求速度,簡單粗暴地使用Java的UUID作為數(shù)據(jù)的ID。實(shí)際上,由于UUID非常長,除了占用大量存儲空間外,主要的問題在索引上,在建立索引和基于索引進(jìn)行查詢時(shí)都存在性能問題。

下面使用主流的ZooKeeper+Snowflake算法實(shí)現(xiàn)高性能的Long類型分布式ID生成器,并且封裝成了一個(gè)通用的Hibernate的ID生成器類CommonSnowflakeIdGenerator,具體的代碼如下:

package com.crazymaker.springcloud.standard.hibernate;

/**
*通用的分布式Hibernate ID生成器
*build by尼恩 @ 瘋狂創(chuàng)客圈
**/
public class CommonSnowflakeIdGenerator extends IncrementGenerator
{
/**
*生成器的map緩存
*key為PO類名,value為分布式ID生成器
*/
private Map<String, SnowflakeIdGenerator> generatorMap =
new LinkedHashMap<>();
/**
*從父類繼承方法:生成分布式ID
*/
@Override
public Serializable generate(
SharedSessionContractImplementor sessionImplementor,
Object object)
throws HibernateException
{
/**
*獲取PO的類名
*作為ID的類型
*/
String type = object.getClass().getSimpleName();
Serializable id = null;
/**
*從map中取得分布式ID生成器
*/
IdGenerator idGenerator = getFromMap(type); /**
*調(diào)用生成器的ZooKeeper+Snowflake算法生成ID
*/
id = idGenerator.nextId();
if (null != id)
{
return id;
}
/**
*如果生成失敗,就通過父類生成
*/
id = sessionImplementor.getEntityPersister(null, object)
.getClas**etadata().getIdentifier(object, sessionImplementor);
return id != null ? id : super.generate(sessionImplementor, object);
}
/**
*從map中獲取緩存的分布式ID生成器,若沒有則創(chuàng)建一個(gè)
*
*@param type生成器的綁定類型,為PO類名
*@return分布式ID生成器
*/
public synchronized IdGenerator getFromMap(String type)
{
if (generatorMap.containsKey(type))
{
return generatorMap.get(type);
}
/**
*創(chuàng)建分布式ID生成器,并且存入map
*/
SnowflakeIdGenerator idGenerator = new SnowflakeIdGenerator(type);
generatorMap.put(type, idGenerator);
return idGenerator;
}
}

以上Hibernate ID生成器只是對ZooKeeper+Snowflake算法分布式ID生成器的簡單封裝。

秒殺的控制層設(shè)計(jì)

本小節(jié)第一介紹秒殺練習(xí)的REST接口設(shè)計(jì),第二介紹它的控制層(controller)的大致實(shí)現(xiàn)邏輯。啟動秒殺服務(wù)seckill-provider,第二通過Swagger UI界面訪問它的REST接口清單,大致如圖10-9所示。

秒殺封裝win10(封裝win10教程)

圖10-9 秒殺練習(xí)的REST接口示意圖

秒殺服務(wù)seckill-provider的控制層的REST接口分為4部分:

(1)秒殺練習(xí)RedisLock版本。此秒殺版本含有兩個(gè)接口:一個(gè)獲取令牌的接口和一個(gè)執(zhí)行秒殺的接口。此版本使用RedisLock分布式鎖保護(hù)秒殺數(shù)據(jù)庫**作。

(2)秒殺練習(xí)ZkLock版本。

此秒殺版本包含兩個(gè)接口:一個(gè)獲取令牌的接口和一個(gè)執(zhí)行秒殺的接口。此版本使用ZkLock分布式鎖保護(hù)秒殺數(shù)據(jù)庫**作。此版本的意義是為大家學(xué)習(xí)和使用ZooKeeper分布式鎖提供案例。

(3)秒殺練習(xí)商品管理。

此部分REST接口主要對秒殺的商品進(jìn)行CRUD**作。

(4)秒殺練習(xí)訂單管理。

此部分REST接口主要對秒殺的訂單進(jìn)行查詢、清除**作。

由于各部分REST接口涉及的知識體系大致相同,因此本文只介紹秒殺練習(xí)RedisLock版本控制層的實(shí)現(xiàn),其他的控制層接口可自行分析和研究。

秒殺練習(xí)RedisLock版本的控制層類的代碼如下:

package com.crazymaker.springcloud.seckill.controller;
//省略import
@RestController
@RequestMapping("/api/seckill/redis/")
@Api(tags = "秒殺練習(xí) RedisLock版本")
public class SeckillByRedisLockController
{
/**
*秒殺服務(wù)實(shí)現(xiàn)Bean
*/
@Resource
RedisSeckillServiceImpl redisSeckillServiceImpl;
/**
*獲取秒殺的令牌
*/
@ApiOperation(value = "獲取秒殺的令牌") @PostMapping("/token/v1")
RestOut<String> getSeckillToken(
@RequestBody SeckillDTO dto)
{
String result = redisSeckillServiceImpl.getSeckillToken(
dto.getSeckillGoodId(),
dto.getUserId());
return RestOut.success(result).setRespMsg("這是獲取的結(jié)果");
}
/**
*執(zhí)行秒殺的**作
*
*@return
*/
@ApiOperation(value = "秒殺")
@PostMapping("/do/v1")
RestOut<SeckillOrderDTO> executeSeckill(@RequestBody SeckillDTO dto)
{
SeckillOrderDTO orderDTO = redisSeckillServiceImpl
.executeSeckill(dto);
return RestOut.success(orderDTO).setRespMsg("秒殺成功");
}
}

以上SeckillByRedisLockController僅僅做了REST服務(wù)的發(fā)布,真正的秒殺邏輯在服務(wù)層的RedisSeckillServiceImpl類中實(shí)現(xiàn)。

service層邏輯:獲取秒殺令牌

本文的秒殺案例特意刪除了服務(wù)層的接口類,只剩下了服務(wù)層的實(shí)現(xiàn)類,表面上違背了“面向接口編程”的原則,實(shí)際上這樣做能使代碼更加干凈和簡潔,也減少了代碼維護(hù)的工作量。之所以這樣簡化,主要的原因是:刪除的那些接口類都是單實(shí)現(xiàn)類接口(一個(gè)接口只有一個(gè)實(shí)現(xiàn)類),那些接口在使用時(shí)不會存在多種實(shí)現(xiàn)對象賦值給同一個(gè)接口變量的多態(tài)情況。筆者從事開發(fā)這么多年,可謂經(jīng)歷項(xiàng)目無數(shù),發(fā)現(xiàn)不知道有多少實(shí)際項(xiàng)目,出于“面向接口編程”的原則,寫了無數(shù)個(gè)單實(shí)現(xiàn)類接口,將“面向接口編程”的編程原則僵化和教條化。

回到主題,下面給大家介紹RedisSeckillServiceImpl秒殺實(shí)現(xiàn)類,該類主要有兩個(gè)功能:獲取秒殺令牌和完成秒殺下單。

本小節(jié)介紹其中的第一個(gè)功能——獲取秒殺令牌,該功能由getSeckillToken方法實(shí)現(xiàn),具體的流程圖如圖10-10所示。

秒殺封裝win10(封裝win10教程)

圖10-10 獲取秒殺令牌流程圖

獲取秒殺令牌的輸入為用戶的userId和秒殺商品的seckillGoodId,其輸出為一個(gè)代表秒殺令牌的UUID字符串,獲取秒殺令牌的重點(diǎn)是進(jìn)行3個(gè)判斷:

(1)判斷秒殺的商品是否存在,如果不存在,就拋出對應(yīng)異常。

(2)判斷秒殺商品的庫存是否足夠,如果沒有足夠庫存,就拋出對應(yīng)異常。

(3)判斷用戶是否已經(jīng)獲取過商品的秒殺令牌,如果獲取過,就拋出對應(yīng)異常。

只有秒殺商品存在、庫存足夠而且之前沒有被userId代表的用戶秒殺過這3個(gè)條件都滿足,才能允許用戶獲取商品的秒殺令牌。

獲取秒殺令牌的代碼節(jié)選如下:

package com.crazymaker.springcloud.seckill.service.impl;
//省略import
@Configuration
@Slf4j
@Service
public class RedisSeckillServiceImpl
{
/**
*秒殺商品的DAO數(shù)據(jù)**作類
*/
@Resource
SeckillGoodDao seckillGoodDao;
/**
*秒殺訂單的DAO數(shù)據(jù)**作類
*/
@Resource
SeckillOrderDao seckillOrderDao;
/**
*Redis分布式鎖實(shí)現(xiàn)類
*/
@Autowired
RedisLockService redisLockService;
/**
*緩存數(shù)據(jù)**作類
*/
@Resource
RedisRepository redisRepository;
/**
*秒殺令牌**作的腳本 */
static String seckillLua = "script/seckill.lua";
static RedisScript<Long> seckillScript = null;
{
String script = IOUtil.loadJarFile(RedisLockService
.class.getClassLoader(), seckillLua);
seckillScript = new DefaultRedisScript<>(script, Long.class);
}
/**
*獲取秒殺令牌
*
*@param seckillGoodId秒殺id
*@param userId用戶id
*@return令牌信息
*/
public String getSeckillToken(Long seckillGoodId, Long userId)
{
String token = UUID.randomUUID().toString();
Long res = redisRepository.executeScript(
seckillScript, //lua腳本對象
Collections.singletonList("setToken"), //執(zhí)行l(wèi)ua腳本的key
String.valueOf(seckillGoodId), //執(zhí)行l(wèi)ua腳本的value1
String.valueOf(userId), //執(zhí)行l(wèi)ua腳本的value2
token //執(zhí)行l(wèi)ua腳本的value3
);
if (res == 2)
{
throw BusinessException.builder()
.errMsg("秒殺商品沒有找到").build();
}
if (res == 4)
{
throw BusinessException.builder()
.errMsg("庫存不足,稍后再來").build();
}
if (res == 5)
{
throw BusinessException.builder().errMsg("已經(jīng)排隊(duì)過了").build();
}
if (res != 1)
{
throw BusinessException.builder()
.errMsg("排隊(duì)失敗,未知錯誤").build();
}
return token;
}
//省略下單部分代碼
}

通過上面的代碼可以看出,getSeckillToken方法并沒有獲取令牌的核心邏輯,僅僅調(diào)用緩存在Redis內(nèi)部的seckill.lua腳本的setToken方法判斷和設(shè)置秒殺令牌,第二對seckill.lua腳本的返回值進(jìn)行判斷,并根據(jù)不同的返回值做出不同的反應(yīng)。設(shè)置令牌的核心邏輯存在于seckill.lua腳本中。為什么要用Lua腳本呢?

(1)由于Redis腳本作為一個(gè)整體來執(zhí)行,中間不會被其他命令插入,天然具備分布式鎖的特點(diǎn),因此不需要使用專門的分布式鎖對設(shè)置令牌的邏輯進(jìn)行并發(fā)控制。

(2)秒殺令牌在Redis中進(jìn)行緩存,在設(shè)置新令牌之前需要查找舊令牌并且進(jìn)行是否存在的判斷,如果這些邏輯都編寫在Java程序中,那么完成查找舊令牌和設(shè)置新令牌需要多次的Redis往返**作,也就是說需要進(jìn)行多次網(wǎng)絡(luò)傳輸。大家知道,網(wǎng)絡(luò)的傳輸延遲是損耗性能的大戶,所以使用Lua腳本能減少網(wǎng)絡(luò)傳輸次數(shù),從而提高性能。

在seckill.lua腳本中,除了有setToken令牌的設(shè)置方法外,還有其他的方法,如checkToken令牌檢查方法,該腳本稍后再為大家統(tǒng)一介紹。

service層邏輯:執(zhí)行秒殺下單

前面講到RedisSeckillServiceImpl秒殺實(shí)現(xiàn)類主要有兩個(gè)功能:

獲取秒殺令牌和完成秒殺下單。下面來看秒殺下單的業(yè)務(wù)邏輯。

秒殺下單很簡單、清晰,只有兩點(diǎn):減庫存和存儲用戶秒殺訂單明細(xì)。但是其中涉及兩個(gè)問題:

(1)數(shù)據(jù)一致性問題:同一商品在秒殺商品表中的庫存數(shù)和在訂單表中的訂單數(shù)需要保持一致。

(2)超賣問題:秒殺商品的剩余庫存數(shù)不能為負(fù)數(shù)。

以上兩個(gè)問題主要借助Redis分布式鎖解決。另外,由于代碼中存在減庫存和存訂單兩次數(shù)據(jù)庫**作,為了防止出現(xiàn)一次失敗一次成功的情況,需要通過數(shù)據(jù)庫事務(wù)對這兩次**作進(jìn)行數(shù)據(jù)一致性保護(hù)。

秒殺下單的執(zhí)行流程如圖10-11所示。

秒殺封裝win10(封裝win10教程)

圖10-11 秒殺下單的流程圖

由于存在數(shù)據(jù)庫事務(wù),因此將秒殺下單的整體流程分成兩個(gè)方法實(shí)現(xiàn):

(1)executeSeckill(SeckillDTO):負(fù)責(zé)下單前的分布式鎖獲取和庫存的檢查。

(2)doSeckill(SeckillDTO):負(fù)責(zé)真正的下單**作(減庫存和存儲秒殺訂單)。

秒殺下單流程的實(shí)現(xiàn)代碼如下:

package com.crazymaker.springcloud.seckill.service.impl;
//省略import
@Configuration
@Slf4j
@Service
public class RedisSeckillServiceImpl
{
/**
*秒殺商品的DAO數(shù)據(jù)**作類
*/
@Resource
SeckillGoodDao seckillGoodDao;
/**
*秒殺訂單的DAO數(shù)據(jù)**作類
*/
@Resource
SeckillOrderDao seckillOrderDao;
/**
*Redis分布式鎖實(shí)現(xiàn)類
*/
@Autowired
RedisLockService redisLockService;
/**
*執(zhí)行秒殺下單
*
*@param inDto
*@return
*/
public SeckillOrderDTO executeSeckill(SeckillDTO inDto)
{
long goodId = inDto.getSeckillGoodId();
Long userId = inDto.getUserId();
//判斷令牌是否有效
Long res = redisRepository.executeScript(
seckillScript, Collections.singletonList("checkToken"),
String.valueOf(inDto.getSeckillGoodId()),
String.valueOf(inDto.getUserId()),
inDto.getSeckillToken()
); if (res != 5)
{
throw BusinessException.builder().errMsg("請?zhí)崆芭抨?duì)").build();
}
/**
*創(chuàng)建訂單對象
*/
SeckillOrderPO order = SeckillOrderPO.builder()
.goodId(goodId).userId(userId).build();
Date nowTime = new Date();
order.setCreateTime(nowTime);
order.setStatus(SeckillConstants.ORDER_VALID);
String lockValue = UUID.randomUUID().toString();
SeckillOrderDTO dto = null;
/**
*創(chuàng)建重復(fù)性檢查的訂單對象
*/
SeckillOrderPO checkOrder = SeckillOrderPO.builder().goodId(
order.getGoodId()).userId(order.getUserId()).build();
//記錄秒殺訂單信息
long insertCount = seckillOrderDao.count(Example.of(checkOrder));
//唯一性判斷:goodId,id保證一個(gè)用戶只能秒殺一件商品
if (insertCount >= 1)
{
//重復(fù)秒殺
log.error("重復(fù)秒殺");
throw BusinessException.builder().errMsg("重復(fù)秒殺").build();
}
/**
*獲取分布式鎖
*/
String lockKey = "seckill:lock:" + String.valueOf(goodId);
boolean locked = redisLockService.acquire(lockKey,
lockValue, 1, TimeUnit.SECONDS);
/**
*執(zhí)行秒殺,秒殺前先搶到分布式鎖
*/
if (locked)
{
Optional<SeckillGoodPO> optional = seckillGoodDao.findById
(order.getGoodId());
if (!optional.isPresent())
{
//秒殺不存在
throw BusinessException.builder()
.errMsg("秒殺不存在").build();
}
//查詢庫存
SeckillGoodPO good = optional.get();
if (good.getStockCount() <= 0)
{
//重復(fù)秒殺
throw BusinessException.builder()
.errMsg("秒殺商品被搶光").build();
}
order.setMoney(good.getCostPrice());
try
{
/**
*進(jìn)入秒殺事務(wù)
*執(zhí)行秒殺邏輯:1.減庫存;2.存儲秒殺訂單
*/
doSeckill(order);
dto = new SeckillOrderDTO(); BeanUtils.copyProperties(order, dto);
} finally
{
try
{
/**
*釋放分布式鎖
*/
redisLockService.release(lockKey, lockValue);
} catch (Exception e)
{
e.printStackTrace();
}
}
} else
{
throw BusinessException.builder()
.errMsg("獲取分布式鎖失敗").build();
}
return dto;
}
/**
*下單**作,加上了數(shù)據(jù)庫事務(wù)
*
*@param order訂單
*/
@Transactional
public void doSeckill(SeckillOrderPO order)
{
/**
*插入秒殺訂單
*/
seckillOrderDao.save(order);
//減庫存
seckillGoodDao.updateStockCountById(order.getGoodId());
}
}

executeSeckill在執(zhí)行秒殺前調(diào)用seckill.lua腳本中的checkToken方法判斷令牌是否有效。如果Lua腳本的checkToken方法的返回值不是5(令牌有效標(biāo)識),就拋出運(yùn)行時(shí)異常。

秒殺的Lua腳本設(shè)計(jì)

前面講到,在seckill.lua腳本中完成設(shè)置令牌和令牌檢查的工作有兩大優(yōu)勢:一是在Redis內(nèi)部執(zhí)行Lua腳本天然具備分布式鎖的特點(diǎn);二是能減少網(wǎng)絡(luò)傳輸次數(shù),提高性能。

在seckill.lua腳本中定義了兩個(gè)方法:setToken令牌設(shè)置方法和checkToken令牌檢查方法。其中,setToken令牌設(shè)置方法的執(zhí)行流程如下:

(1)檢查token秒殺令牌是否存在,如果存在,就返回標(biāo)志5,表明排隊(duì)過了。

(2)檢查以JSON格式緩存的秒殺商品的庫存是否足夠,如果庫存不夠,就返回標(biāo)志4,表明庫存不足。

(3)為秒殺商品減少一個(gè)庫存,并編碼成JSON格式,再一次緩存起來。

(4)使用hset命令將用戶的秒殺令牌保存在Redis哈希表結(jié)構(gòu)中,其hash key為用戶的userId。

(5)最終返回標(biāo)志1,表明排隊(duì)成功。

checkToken令牌檢查方法的執(zhí)行流程如下:

(1)使用hget命令從保存秒殺令牌的Redis哈希表結(jié)構(gòu)中,以用戶的userId作為hash key,取出之前緩存的秒殺令牌。

(2)如果令牌獲取成功,就返回標(biāo)志5,表明排隊(duì)成功。

(3)如果令牌不存在,就返回標(biāo)志-1,表明沒有排隊(duì)。

seckill.lua腳本的源碼如下:

— 返回值說明
— 1 排隊(duì)成功
— 2 排隊(duì)商品沒有找到
— 3 人數(shù)超過限制
— 4 庫存不足
— 5 排隊(duì)過了
— 6 秒殺過了
— -2 Lua方法不存在
local function setToken(goodId, userId, token)
–檢查token秒殺令牌是否存在
local oldToken = redis.call("hget", "seckill:queue:" .. goodId, userId);
if oldToken then
return 5; –返回 5 之前已經(jīng)排隊(duì)過了
end
–獲取商品緩存次數(shù)
local goodJson = redis.call("get", "seckill:goods:" .. goodId);
if not goodJson then
–redis.debug("秒殺商品沒有找到")
return 2; –返回2秒殺商品沒有找到
end
–redis.log(redis.LOG_NOTICE, goodJson)
local goodDto = cjson.decode(goodJson);
–redis.log(redis.LOG_NOTICE, "good title=" .. goodDto.title)
local stockCount = tonumber(goodDto.stockCount);
–redis.log(redis.LOG_NOTICE, "stockCount=" .. stockCount)
if stockCount <= 0 then
return 4; –返回4庫存不足
end
stockCount = stockCount – 1;
goodDto.stockCount = stockCount;
redis.call("set", "seckill:goods:" .. goodId, cjson.encode(goodDto));
redis.call("hset", "seckill:queue:" .. goodId, userId, token);
return 1; –返回1排隊(duì)成功
end
— 返回值說明
— 5 排隊(duì)過了
— -1 沒有排隊(duì)
local function checkToken(goodId, userId, token)
–檢查token是否存在
local oldToken = redis.call("hget", "seckill:queue:" .. goodId, userId);
if oldToken and (token == oldToken) then
–return 1 ;
return 5; –5 排隊(duì)過了
end
return -1; —1 沒有排隊(duì)
end
local method = KEYS[1] –執(zhí)行l(wèi)ua腳本時(shí)傳入的key1
local goodId = ARGV[1] –執(zhí)行l(wèi)ua腳本時(shí)傳入的value1
local userId = ARGV[2] –執(zhí)行l(wèi)ua腳本時(shí)傳入的value2
local token = ARGV[3] –執(zhí)行l(wèi)ua腳本時(shí)傳入的value3
if method == 'setToken' then return setToken(goodId, userId, token)
elseif method == 'checkToken' then
return checkToken(goodId, userId, token)
else
return -2; –Lua方法不存在
end

以上seckill.lua腳本在Java中可以通過spring-data-redis包的以下方法來執(zhí)行:

RedisTemplate.execute(RedisScript<T> script, List<K> keys, Object,…, args)在開發(fā)腳本的過程中往往需要進(jìn)行腳本調(diào)試,可以通過Shell指令redis-cli–eval直接執(zhí)行seckill.lua腳本,具體的調(diào)試執(zhí)行過程可查看瘋狂創(chuàng)客圈社群的秒殺練習(xí)演示視頻。

BusinessException定義

減庫存**作和插入購買明細(xì)**作都會產(chǎn)生很多業(yè)務(wù)異常,比如庫存不足、重復(fù)秒殺等,這些業(yè)務(wù)異常與crazy-springcloud腳手架中的其他業(yè)務(wù)異常一樣,全部被封裝成BusinessException通用業(yè)務(wù)異常實(shí)例拋出。

一般項(xiàng)目怎么劃分自定義異常呢?大致有兩種方式:

(1)按異常來源所處的controller、service、dao層劃分業(yè)務(wù)異常,例如DaoException、ServiceException、ControllerException等。

(2)按異常來源所處的模塊組件(如數(shù)據(jù)庫、消息中間件、業(yè)務(wù)模塊)劃分業(yè)務(wù)異常,例如MysqlExceptioin、RedisException、ElasticSearchException、SeckillException等。

無論按照哪個(gè)維度劃分都出于同一個(gè)目標(biāo):一旦出現(xiàn)異常,就可以很容易**到是哪個(gè)層或組件出現(xiàn)了問題。

在實(shí)際開發(fā)過程中,定義太多異常類型之后,需要不厭其煩地將異常一層層拋出、一層層捕獲,反而會加大代碼的復(fù)雜度。所以,雖然crazyspringcloud腳手架和其他項(xiàng)目一樣定義了一個(gè)自己的全局異?;怋usinessException,但是crazy-springcloud腳手沒有定義太多業(yè)務(wù)異常子類。一般情況下,重新定義一個(gè)異常的子類其實(shí)沒有太大必要,因?yàn)榭梢愿鶕?jù)異常的編碼和異常的消息進(jìn)行區(qū)分。

crazy-springcloud腳手架的基礎(chǔ)業(yè)務(wù)異常類BusinessException的代碼如下:

package com.crazymaker.springcloud.common.exception;
//省略import
@Builder
@Data
@AllArgsConstructor
public class BusinessException extends RuntimeException
{
private static final long serialVersionUID = 1L;
/**
*默認(rèn)的錯誤編碼
*/ private static final int DEFAULT_CODE = -1;
/**
*默認(rèn)的錯誤提示
*/
private static final String DEFAULT_MSG = "業(yè)務(wù)異常";
/**
*業(yè)務(wù)錯誤編碼
*/
@lombok.Builder.Default
private int errCode = DEFAULT_CODE;
/**
*錯誤的提示信息
*/
@lombok.Builder.Default
private String errMsg = DEFAULT_MSG;
public BusinessException()
{
super(DEFAULT_MSG);
}
/**
*帶格式設(shè)置異常消息
*@param format 格式
*@param objects 替換的對象
*/
public BusinessException setDetail(String format, Object… objects) {
format = StringUtils.replace(format, "{}", "%s");
this.errMsg = String.format(format, objects);
return this;
}
}

該類有errCode、errMsg兩個(gè)屬性,errCode屬性用于存放異常的編碼,errMsg屬性用于存放一些錯誤附加信息。

特別注意,該類繼承了RuntimeException運(yùn)行時(shí)異常類,而不是Exception受檢異?;悾砻鰾usinessException類其實(shí)是一個(gè)非受檢的運(yùn)行時(shí)異常類。

為什么要這樣呢?有兩個(gè)原因:

(1)默認(rèn)情況下,Spring Boot事務(wù)只有檢查到RuntimeException運(yùn)行時(shí)異常才會回滾,如果檢查到的是普通的受檢異常,那么Spring Boot事務(wù)是不會回滾的,除非經(jīng)過特殊配置。

(2)簡化編程的代碼,如果沒有必要,就不需要在業(yè)務(wù)程序中對異常進(jìn)行捕獲,而是由項(xiàng)目中的全局異常解析器統(tǒng)一負(fù)責(zé)處理。

crazy-springcloud腳手架的全局異常解析器ExceptionResolver的代碼如下:

package com.crazymaker.springcloud.standard.config;
//省略import
/**
*ExceptionResolver
*/
@Slf4j
@RestControllerA**ice
public class ExceptionResolver
{
/**
*其他異常
*/
private static final String OTHER_EXCEPTION_MESSAGE = "其他異常";
/**
*業(yè)務(wù)異常
*/
private static final String BUSINESS_EXCEPTION_MESSAGE = "業(yè)務(wù)異常";
/**
*業(yè)務(wù)異常處理
*
*@param request請求體
*@param e 異常實(shí)例
*@return RestOut
*/
@Order(1)
@ExceptionHandler(BusinessException.class)
public RestOut<String> businessException(HttpServletRequest request, BusinessException e)
{
log.info(BUSINESS_EXCEPTION_MESSAGE + ":" + e.getErrMsg());
return RestOut.error(e.getErrMsg());
}
/**
*業(yè)務(wù)異常之外的其他異常處理
*
*@param request請求體
*@param e 異常實(shí)例
*@return RestOut
*/
@Order(2)
@ExceptionHandler(Exception.class)
public RestOut<String> finalException(HttpServletRequest request, Exception e)
{
e.printStackTrace();
log.error(OTHER_EXCEPTION_MESSAGE + ":" + e.getMessage());
return RestOut.error(e.getMessage());
}
}

上面的ExceptionResolver全局異常解析器使用了Spring Boot的@RestControllerA**ice注解,該注解第一會對系統(tǒng)的異常進(jìn)行攔截,并且交給對應(yīng)的異常處理方法進(jìn)行處理,第二將異常處理結(jié)果返回給客戶端。

ExceptionResolver的每個(gè)異常處理方法都使用@ExceptionHandler注解配置自己希望處理的異常類型,傳入的參數(shù)為異常類型的class實(shí)例,如果要處理多個(gè)異常類型,那么其參數(shù)可以是一個(gè)異常類型class實(shí)例數(shù)組。需要注意的是,不能在兩個(gè)異常處理方法的@ExceptionHandler注解中配置同一個(gè)異常類型,如果存在一種異常類型被處理多次,在初始化全局異常解析器時(shí)就會失敗。

本文給大家講解的內(nèi)容是高并發(fā)核心編程,Spring Cloud+Nginx秒殺實(shí)戰(zhàn),秒殺業(yè)務(wù)的參考實(shí)現(xiàn)下篇文章給大家講解的是高并發(fā)核心編程,Spring Cloud+Nginx秒殺實(shí)戰(zhàn),Zuul內(nèi)部**實(shí)現(xiàn)秒殺限流;覺得文章不錯的朋友可以轉(zhuǎn)發(fā)此文關(guān)注小編;感謝大家的支持!

拓展知識:

原創(chuàng)文章,作者:九賢生活小編,如若轉(zhuǎn)載,請注明出處:http://m.cxzzxj.cn/130073.html

嘿咻视频免费无码专区观看| 国产在线自在拍91| 精品人妻少妇一区二区三| 四虎网站免费大全| 亚洲1234五码高清| 高清视频色| 免费无码在线观看| 日本高清不卡免费v视频| 亚洲av尤物在线| 欧美精品自慰| 三级片九九网站| 久久中文热字幕| 在线视频一区樱桃精品| 亚洲日韩一区二区精品| 久久精品国产成人午夜| 精品无码一区二区高潮久久国产 | 亚洲熟妇综合久久久久久| 亚洲寂寞一区| 国产成人aa视频在线观看| 欧美爆乳免费看| 最新国产在线不卡AV| 中文有码vs人妻| 色v在线| 国产精品无码专区| 水蜜桃精品一二三| 国产精品三级国产精品高| 久久精品国产再热青青青| 2024亚洲理论片| 人妻久久精品天天中文字幕| 五月丁香婷婷久久久| 日韩AV老司机| 另类亚洲综合区图片区小说 | 天堂99| 伊人涩涩久| 久久久久久自慰| 久久99精品日韩| 亚洲砖码专| 4444.色色色色| 国产精品无码白浆高潮| 精品无码一区二区视频男人吃奶 | 99RE6在线视频精品免费|