redisson+springboot 實現分布式鎖

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分布式鎖

分布式鎖需滿足四個條件

首先,為了確保分布式鎖可用,我們至少要確保鎖的實現同時滿足以下四個條件:

  1. 互斥性。在任意時刻,只有一個客戶端能持有鎖。
  2. 不會發生死鎖。即使有一個客戶端在持有鎖的期間崩潰而沒有主動解鎖,也能保證后續其他客戶端能加鎖。
  3. 解鈴還須系鈴人。加鎖和解鎖必須是同一個客戶端,客戶端自己不能把別人加的鎖給解了,即不能誤解鎖。
  4. 具有容錯性。只要大多數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

沒有庫存變成負數的情況,說明分布式鎖已生效

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容