一、準(zhǔn)備
//記錄 成功記錄
CREATE TABLE `product_record` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`product_id` varchar(20) NOT NULL COMMENT '商品id',
`user_id` varchar(20) NOT NULL COMMENT '買主id',
`num` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COMMENT='購(gòu)買記錄表';
//庫(kù)存表
CREATE TABLE `product_storage` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`product_id` varchar(20) NOT NULL COMMENT '商品ID',
`amount` bigint(20) NOT NULL COMMENT '庫(kù)存數(shù)',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=latin1;
并發(fā)測(cè)試工具 JMeter
二辛蚊、單機(jī)并發(fā)控制
@Transactional
public synchronized String buySth(String userId, String productId) {
int num = random.nextInt(10) + 1;
// 查庫(kù)存
ProductStorage storage = storageMapper.getByProductId(productId);
Long oldAmount = storage.getAmount();
if (num > oldAmount) {
log.info(userId + " request " + num + " fail");
return userId + " request " + num + " fail";
}
storage.setAmount(oldAmount - num);
log.info(userId + " request " + num + " " + productId + "當(dāng)前 " + oldAmount);
// 減庫(kù)存
int res = storageMapper.updateByPrimaryKeySelective(storage);
if (res > 0) {
ProductRecord productRecord = new ProductRecord();
productRecord.setProductId(productId);
productRecord.setUserId(userId);
productRecord.setNum(num);
recordMapper.insert(productRecord);
return userId + "request " + num + " success" + " 當(dāng)前 " + oldAmount;
} else {
return userId + "request " + num + " fail";
}
}
tips
:sychronized
加在靜態(tài)方法上粤蝎,表示sychronized(類.class){} 。加在非靜態(tài)方法上等同于對(duì)整個(gè)方法體使用sychroniced(this){}袋马。
為什么會(huì)出現(xiàn)鎖不住的情況呢初澎,還是有多扣的情況。這是因?yàn)槲覀兪褂昧?code>@Transactional注解加入了事務(wù)控制虑凛。Spring的事務(wù)控制是通過(guò)AOP實(shí)現(xiàn)的碑宴,在業(yè)務(wù)開始前后會(huì)添加/提交事務(wù)软啼。流程大概如下:
- 開啟事務(wù)(aop)
- 加鎖(進(jìn)入synchronized方法)
- 釋放鎖(退出synchronized方法)
- 提交事務(wù)(aop)
所以T2進(jìn)入的時(shí)候,T1的事務(wù)還沒有提交延柠,就可能導(dǎo)致讀到的還是T2秒殺前的庫(kù)存祸挪。試著注釋掉@Transactional
注釋再試一下。
可以看到不會(huì)有重復(fù)扣減的情況贞间。
針對(duì) Spring事務(wù)提交與鎖釋放不同步的問(wèn)題匕积,可以使用Spring手動(dòng)事務(wù)控制方式解決:
@Autowired
private PlatformTransactionManager platformTransactionManager;
@Autowired
private TransactionDefinition transactionDefinition;
引入事務(wù)控制管理類。
// @Transactional
public synchronized String buySth(String userId, String productId) {
int num = random.nextInt(10) + 1;
// 查庫(kù)存
ProductStorage storage = storageMapper.getByProductId(productId);
Long oldAmount = storage.getAmount();
if (num > oldAmount) {
log.info(userId + " request " + num + " fail");
return userId + " request " + num + " fail";
}
//獲取事務(wù)狀態(tài)對(duì)象榜跌,開啟事務(wù)
TransactionStatus transaction = dataSourceTransactionManager.getTransaction(transactionDefinition);
try {
storage.setAmount(oldAmount - num);
log.info(userId + " request " + num + " " + productId + " 當(dāng)前 " + oldAmount);
// 減庫(kù)存
int res = storageMapper.updateByPrimaryKeySelective(storage);
if (res > 0) {
ProductRecord productRecord = new ProductRecord();
productRecord.setProductId(productId);
productRecord.setUserId(userId);
productRecord.setNum(num);
recordMapper.insert(productRecord);
//事務(wù)提交
dataSourceTransactionManager.commit(transaction);
return userId + "request " + num + " success" + " 當(dāng)前 " + oldAmount;
} else {
//事務(wù)回滾
dataSourceTransactionManager.rollback(transaction);
return userId + "request " + num + " fail";
}
} catch (Exception e) {
dataSourceTransactionManager.rollback(transaction);
} finally {
return userId + "request " + num + " fail";
}
}
這樣也是可以實(shí)現(xiàn)單機(jī) 并發(fā)控制的。
上面的例子說(shuō)明synchronize
關(guān)鍵字是可以解決并發(fā)問(wèn)題的盅粪。但是這種方式會(huì)有很大的缺陷:
-
synchronize
不夠靈活钓葫,一次性鎖住了一整個(gè)方法,如果我們都是有多個(gè)商品的話票顾,會(huì)被一次性全部鎖住础浮,效率太低。 -
synchronize
只可以鎖住當(dāng)前的項(xiàng)目實(shí)例奠骄,如果項(xiàng)目發(fā)展到集群部署豆同,就失去作用了。
為了解決上面的問(wèn)題含鳞,就引入了分布式鎖的概念影锈。
二、使用Redis 分布式鎖模擬秒殺
@Transactional
public String buySth(String userId, String productId) {
int num = random.nextInt(10) + 1;
//獲取鎖
Boolean lock = redis.opsForValue().setIfAbsent("skill::" + productId, userId, 3, TimeUnit.SECONDS);
if (lock) {
try {
// 查庫(kù)存
ProductStorage storage = storageMapper.getByProductId(productId);
Long oldAmount = storage.getAmount();
if (num > oldAmount) {
return userId + " request " + num + " fail";
}
storage.setAmount(oldAmount - num);
log.info(userId + " request " + num + " " + productId + " 當(dāng)前 " + oldAmount + " success");
// 減庫(kù)存
int res = storageMapper.updateByPrimaryKeySelective(storage);
if (res > 0) {
ProductRecord productRecord = new ProductRecord();
productRecord.setProductId(productId);
productRecord.setUserId(userId);
productRecord.setNum(num);
recordMapper.insert(productRecord);
return userId + "request " + num + " success" + " 當(dāng)前 " + oldAmount;
} else {
return userId + "request " + num + " fail";
}
} finally {
String currUser = redis.opsForValue().get("skill::" + productId);
if (userId.equals(currUser)) {
redis.delete("skill" + productId);
}
}
} else {
// log.info(userId + "request lock fail");
return userId + "request " + num + " fail";
}
}
一個(gè)簡(jiǎn)單的 nginx 配置請(qǐng)求分發(fā)