redisson+springboot 實現分布式鎖
在一些場景時,需要保證數據的不重復,以及數據的準確性,特別是特定下,某些數據的準確性顯得尤為重要,所以這個時候要保證某個方法同一時刻只能有一個線程執行。在單機情況下可以用jdk的樂觀鎖進行保證數據的準確性。而在分布式系統中,這種jdk的鎖就無法滿足這種場景。
所以需要使用redssion實現分布式鎖,它不僅可以實現分布式鎖,也可以在某些情況下保證不重復提交,保證接口的冪等性。
redisson是基于redis實現的分布式鎖,因為redis執行命令操作時是單線程,所以可以保證線程安全。當然還有其他實現分布式鎖的方案,例如zk,MongoDB等。
簡單來聊一下各自優缺點
方案 | 實現原理 | 優點 | |
---|---|---|---|
MongoDB | 1.加鎖:執行findAndModify原子命令查找document,若不存在則新增<br />2.解鎖:刪除document | 實現較為簡單 | 1.大部分公司數據庫用MySQL,可能缺乏相應的MongoDB運維、開發人員 2.鎖無超時自動失效機制 |
ZooKeepe | 1.加鎖:在/lock目錄下創建臨時有序節點,判斷創建的節點序號是否最小。若是,則表示獲取到鎖;否,則則watch /lock目錄下序號比自身小的前一個節點 2.解鎖:刪除節點 |
1.由zk保障系統高可用 2.Curator框架已原生支持系列分布式鎖命令,使用簡單 |
需單獨維護一套zk集群,維保成本高 |
redis | 1. 加鎖:執行setnx,若成功再執行expire添加過期時間 2. 解鎖:執行delete命令 |
實現簡單,相比數據庫和分布式系統的實現,該方案最輕,性能最好 | 1.setnx和expire分2步執行,非原子操作;若setnx執行成功,但expire執行失敗,就可能出現死鎖 2.delete命令存在誤刪除非當前線程持有的鎖的可能 3.不支持阻塞等待、不可重入 |
redis Lua腳本能力 | 1. 加鎖:執行SET lock_name random_value EX seconds NX 命令 <br />2. 解鎖:執行Lua腳本,釋放鎖時驗證random_value -- ARGV[1]為random_value, KEYS[1]為lock_name | 同上;實現邏輯上也更嚴謹,除了單點問題,生產環境采用用這種方案,問題也不大。 | 不支持鎖重入,不支持阻塞等待 |
redisson | redisson這個框架重度依賴了Lua腳本和Netty,加鎖、解鎖Lua腳本是redisson分布式鎖 |
分布式鎖需滿足四個條件
首先,為了確保分布式鎖可用,我們至少要確保鎖的實現同時滿足以下四個條件:
- 互斥性。在任意時刻,只有一個客戶端能持有鎖。
- 不會發生死鎖。即使有一個客戶端在持有鎖的期間崩潰而沒有主動解鎖,也能保證后續其他客戶端能加鎖。
- 解鈴還須系鈴人。加鎖和解鎖必須是同一個客戶端,客戶端自己不能把別人加的鎖給解了,即不能誤解鎖。
- 具有容錯性。只要大多數Redis節點正常運行,客戶端就能夠獲取和釋放鎖
redisson實現分布式鎖案例
1、導入依賴
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1</version>
</dependency>
<!--<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>-->
<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
<!-- <dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.0</version>
</dependency>-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.9.0</version>
</dependency>
</dependencies>
2、配置redisson-single(單機)
#單機
singleServerConfig:
idleConnectionTimeout: 10000
pingTimeout: 1000
connectTimeout: 10000
timeout: 3000
retryAttempts: 3
retryInterval: 1500
reconnectionTimeout: 3000
failedAttempts: 3
subscriptionsPerConnection: 5
clientName: null
address: "redis://localhost:6379"
subscriptionConnectionMinimumIdleSize: 1
subscriptionConnectionPoolSize: 50
connectionMinimumIdleSize: 32
connectionPoolSize: 64
database: 0
#在最新版本中dns的檢查操作會直接報錯 所以我直接注釋掉了
#dnsMonitoring: false
dnsMonitoringInterval: 5000
threads: 0
nettyThreads: 0
codec: !<org.redisson.codec.JsonJacksonCodec> {}
transportMode : "NIO"
3、配置application
server:
port: 8080
spring:
redis:
host: localhost
port: 6379
database: 3
timeout: 2000
4、編寫redisson配置類
@Configuration
public class RedissonConfig {
@Bean(destroyMethod="shutdown")
public RedissonClient redisson() throws IOException {
return Redisson.create(
Config.fromYAML(new ClassPathResource("redisson-single.yml").getInputStream()));
}
}
5、具體業務實現
@Slf4j
@Service
public class GoodsServiceImpl extends ServiceImpl<GoodsMapper, Goods> implements GoodsService {
@Autowired
private RedissonClient redissonClient;
public static final String LOCK_KEY = "lock";
/**
* 庫存遞減
*
* @param id id
* @param num 數量
* @return
*/
@Override
public boolean killGoods(Long id, Integer num) {
String key = LOCK_KEY + id;
RLock lock = redissonClient.getLock(key);
try {
//上鎖
lock.lock();
Goods goods = this.getById(id);
if (goods.getQuantity()<=0){
return false;
}
log.info("庫存數量======"+goods.getQuantity());
//將庫存減操作
goods.setQuantity(goods.getQuantity()-1);
this.updateById(goods);
} catch (Exception e) {
return false;
} finally {
//解鎖
lock.unlock();
}
return true;
}
6、接口實現
@RequestMapping
@RestController
public class GoodsController {
@Resource
private GoodsService goodsService;
@GetMapping("test")
public String createOrderTest() {
if (!goodsService.killGoods(1405065181720055809L, 1)) {
return "庫存不足";
}
return "創建訂單成功";
}
}
7、測試,用ab測試工具
模擬200個并發測試
D:\develop\Apache24\bin>ab -n 200 -c 200 "http://localhost:8080/test"
結果:
1.png
2.png
沒有庫存變成負數的情況,說明分布式鎖已生效