????????不管是jvm鎖還是mysql鎖,為了保證線程的并發安全,都提供了悲觀獨占排他鎖。所以獨占排他也是分布式鎖的基本要求。
可以利用唯一鍵索引不能重復插入的特點實現。設計表如下:
CREATE TABLE `tb_lock` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`lock_name` varchar(50) NOT NULL COMMENT '鎖名',`class_name` varchar(100) DEFAULT NULL COMMENT '類名',`method_name` varchar(50) DEFAULT NULL COMMENT '方法名',`server_name` varchar(50) DEFAULT NULL COMMENT '服務器ip',`thread_name` varchar(50) DEFAULT NULL COMMENT '線程名',`create_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '獲取鎖時間',`desc` varchar(100) DEFAULT NULL COMMENT '描述',PRIMARY KEY (`id`),UNIQUE KEY `idx_unique` (`lock_name`)
) ENGINE=InnoDB AUTO_INCREMENT=1332899824461455363 DEFAULT CHARSET=utf8;
Lock實體類:
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("tb_lock")
public class Lock {private Long id;private String lockName;private String className;private String methodName;private String serverName;private String threadName;private Date createTime;private String desc;
}
LockMapper接口:
public interface LockMapper extends BaseMapper<Lock> {
}
4.1. 基本思路
????????synchronized關鍵字和ReetrantLock鎖都是獨占排他鎖,即多個線程爭搶一個資源時,同一時刻只有一個線程可以搶占該資源,其他線程只能阻塞等待,直到占有資源的線程釋放該資源。
-
線程同時獲取鎖(insert)
-
獲取成功,執行業務邏輯,執行完成釋放鎖(delete)
-
其他線程等待重試
4.2. 代碼實現
改造StockService:
@Service
public class StockService {@Autowiredprivate StockMapper stockMapper;@Autowiredprivate LockMapper lockMapper;/*** 數據庫分布式鎖*/public void checkAndLock() {// 加鎖Lock lock = new Lock(null, "lock", this.getClass().getName(), new Date(), null);try {this.lockMapper.insert(lock);} catch (Exception ex) {// 獲取鎖失敗,則重試try {Thread.sleep(50);this.checkAndLock();} catch (InterruptedException e) {e.printStackTrace();}}// 先查詢庫存是否充足Stock stock = this.stockMapper.selectById(1L);// 再減庫存if (stock != null && stock.getCount() > 0){stock.setCount(stock.getCount() - 1);this.stockMapper.updateById(stock);}// 釋放鎖this.lockMapper.deleteById(lock.getId());}
}
加鎖:
// 加鎖
Lock lock = new Lock(null, "lock", this.getClass().getName(), new Date(), null);
try {this.lockMapper.insert(lock);
} catch (Exception ex) {// 獲取鎖失敗,則重試try {Thread.sleep(50);this.checkAndLock();} catch (InterruptedException e) {e.printStackTrace();}
}
解鎖:
// 釋放鎖
this.lockMapper.deleteById(lock.getId());
使用Jmeter壓力測試結果:
可以看到性能感人。mysql數據庫庫存余量為0,可以保證線程安全。
4.3. 缺陷及解決方案
缺點:
-
這把鎖強依賴數據庫的可用性,數據庫是一個單點,一旦數據庫掛掉,會導致業務系統不可用。
解決方案:給 鎖數據庫 搭建主備
-
這把鎖沒有失效時間,一旦解鎖操作失敗,就會導致鎖記錄一直在數據庫中,其他線程無法再獲得到鎖。
解決方案:只要做一個定時任務,每隔一定時間把數據庫中的超時數據清理一遍。
-
這把鎖是非重入的,同一個線程在沒有釋放鎖之前無法再次獲得該鎖。因為數據中數據已經存在了。
解決方案:記錄獲取鎖的主機信息和線程信息,如果相同線程要獲取鎖,直接重入。
-
受制于數據庫性能,并發能力有限。
解決方案:無法解決。