微信小程序網(wǎng)站開發(fā)教程旅游seo整站優(yōu)化
目錄
一、鎖的使用場景
二、如何實現(xiàn)控制?
三、單臺服務器使用鎖的場景
四、分布式鎖
五、Redis 實現(xiàn)分布式鎖及存在問題
六、Redisson 實現(xiàn)分布式鎖
七、定時任務+鎖
一、鎖的使用場景
1. 控制定時任務執(zhí)行
- 定時任務多次執(zhí)行浪費資源:多臺服務器到同一時間都執(zhí)行緩存預熱
- 臟數(shù)據(jù):多臺服務器重復插入數(shù)據(jù)
2. 買票場景
- 只有一百張票,用戶買票時判斷剩余票的數(shù)量,還有剩余執(zhí)行票數(shù)減一的操作
- 只剩一張票了,此時多個用戶來買票,判斷票都是有剩余,但是第一個用戶買完之后就沒票了,其他用戶也執(zhí)行了票數(shù)減一的操作,出現(xiàn)超賣現(xiàn)象
3. 需求:控制定時任務 / 需要加鎖的任務在同一時間只能有一臺服務器執(zhí)行
二、如何實現(xiàn)控制?
以定時任務為例
1.?分離定時任務程序和主程序,只在一臺服務器運行定時任務=>成本太大
2.?寫死配置,每個服務器都執(zhí)行定時任務,但是只有 IP?符合配置的服務器才真實執(zhí)行業(yè)務邏輯,其他的直接返回。成本最低;但是我們的 IP 可能是不固定的,把 IP 寫的太死了
3.?動態(tài)配置,配置是可以輕松的、很方便地更新的(代碼無需重啟,項目無需重新部署),但是只有 IP?符合配置的服務器才真實執(zhí)行業(yè)務邏輯。
-
讀取數(shù)據(jù)庫中的配置
-
Redis
-
配置中心(Nacos、Apollo、Spring Cloud Config)
-
問題:服務器多了、IP 不可控還是很麻煩,還是要人工修改
4.?分布式鎖:只有搶到鎖的服務器才能執(zhí)行業(yè)務邏輯
- 壞處:增加成本
- 好處:不用手動配置,多少個服務器都一樣
三、單臺服務器使用鎖的場景
1. Java 實現(xiàn)同步鎖:synchronized 關(guān)鍵字
2. 鎖存在 JVM 中,每臺 JVM 獨立,不共享鎖,多機部署鎖會失效(多個線程都會獲取到不同 JVM 中的同一名稱的鎖)
3. 單機就會存在單點故障
四、分布式鎖
1. 為什么需要分布式鎖?
- 普通鎖的缺點:JVM 機分配的鎖在多臺 Tomcat 中不共享,鎖只對單個服務器有效
- 加鎖的重要性:資源有限 / 特定情況下只能有有限 / 唯一的線程獲取到鎖,執(zhí)行操作
- 分布式鎖:多進程可見且并且互斥的鎖
2. 如何實現(xiàn)分布式鎖?
實現(xiàn)分布式鎖的核心思想 /?怎么保證同一時間只有一臺服務器能搶到鎖?
- 先來的人先把數(shù)據(jù)改成自己的標識(服務器 IP),后來的人發(fā)現(xiàn)標識已存在,就搶鎖失敗,繼續(xù)等待
- 等先來的人執(zhí)行方法結(jié)束,把標識清空(釋放鎖),其他的人繼續(xù)搶鎖
- MySQL 數(shù)據(jù)庫:select for update 行級鎖(最簡單)
- 樂觀鎖(實際上沒有加鎖):樂觀鎖認為線程安全問題只在少數(shù)情況下會發(fā)生,所以只要在數(shù)據(jù)更新時判斷是否有其他線程修改了數(shù)據(jù)
- Redis 實現(xiàn)互斥鎖:基于內(nèi)存,讀寫速度快
- set nx ex:原子性、設(shè)置過期時間
- lua 腳本:保證多條語句的原子性
- Zookeeper
五、Redis 實現(xiàn)分布式鎖及存在問題(誤刪鎖)
1. set nx ex
2. 釋放鎖
- 手動釋放:del lock
- 意外:服務器宕機,手動釋放鎖還未執(zhí)行
- 優(yōu)化:設(shè)置過期時間,若未手動釋放則等到過期時間到了就會自動釋放鎖
3. 誤刪鎖
- 線程 A 在執(zhí)行時阻塞,過了鎖的過期時間,鎖自動釋放
- 線程 B 嘗試獲取鎖,獲取成功,執(zhí)行業(yè)務
- A 阻塞之后繼續(xù)執(zhí)行,執(zhí)行結(jié)束,釋放當前正在被線程 B 占有的鎖
- 線程 C 嘗試獲取鎖,獲取成功,執(zhí)行業(yè)務
- 出現(xiàn)線程 B 和線程 C 并發(fā)執(zhí)行的情況
4. 解決誤刪鎖
- 判斷當前鎖的占有線程是不是本線程
- 如果不是自己占有的鎖,就不去釋放(別人的鎖)
5. 改進鎖之后仍然存在問題:判斷鎖和釋放鎖的原子性問題
- 判斷鎖時是自己正在占有鎖
- 判斷鎖標識后,執(zhí)行釋放鎖之前,線程出現(xiàn)了阻塞,鎖到了過期時間,自動釋放
- 其他線程嘗試獲取鎖,獲取成功
- 阻塞之后執(zhí)行釋放鎖,還是把別人的鎖給釋放了
- 需要保證判斷鎖和釋放鎖操作的原子性:Lua 腳本
六、Redisson 實現(xiàn)分布式鎖
Github:https://github.com/redisson/redisson
官網(wǎng):Redisson: Easy Redis Java client with features of In-Memory Data Grid
1. 定義
- Redisson 是一個在 Redis 基礎(chǔ)上實現(xiàn)的 Java 駐內(nèi)存數(shù)據(jù)網(wǎng)格
- 提供了一系列分布式的 Java 常用對象,還提供了許多分布式服務(各種分布式鎖的實現(xiàn))
2. 自己編寫 Redisson 的配置,創(chuàng)建 RedissonClient
- 不推薦使用 spring-boot-starter 整合的 Redisson,版本迭代較快,容易發(fā)生沖突
- 創(chuàng)建 config 對象,添加 Redis 配置:讀取 application.yml 中的配置信息
- 創(chuàng)建 Redisson 實例,返回 Redisson 客戶端實例
/*** Redisson 配置* @author 樂小鑫* @version 1.0* @Date 2024-01-21-15:44*/
@Configuration
@ConfigurationProperties(prefix = "spring.redis")
@Data
public class RedissonConfig {private String host;private String port;private String password;@Beanpublic RedissonClient getRedissonClient() {// 1. 創(chuàng)建配置Config config = new Config();String redisAddress = String.format("redis://%s:%s", host, port);config.useSingleServer().setAddress(redisAddress).setPassword(password).setDatabase(3);// 2. 創(chuàng)建 Redisson 客戶端實例并返回RedissonClient redisson = Redisson.create(config);return redisson;}
}
3. 測試 Redisson 的功能實現(xiàn)
/*** @author 樂小鑫* @version 1.0* @Date 2024-01-21-15:53*/
@SpringBootTest
public class RedissonTest {@Resourceprivate RedissonClient redissonClient;@Testvoid test() {// listList<String> list = new ArrayList<>();list.add("ghost");System.out.println("List:" + list.get(0));RList<Object> rList = redissonClient.getList("test-list");rList.add("ghost");System.out.println("rList:" + rList.get(0));}
}
4. 看門狗機制的原理
- 監(jiān)聽當前線程,當前線程沒有執(zhí)行結(jié)束就每十秒續(xù)期一次
- 如果線程掛了(注意 Debug 模式時斷點過久也會被當成服務器宕機來處理),看門狗機制失效,則不會續(xù)期
- 參考文章:Redisson 分布式鎖的watch dog自動續(xù)期機制_redisson續(xù)期-CSDN博客
七、定時任務+鎖
1. getLock():獲取 Redisson 的鎖對象,需要指定鎖的名稱
2. tryLock():嘗試獲取鎖(分布式鎖),獲取成功返回 true,可以指定重試獲取鎖的等待時間和鎖的釋放時間
- waitTime 設(shè)置為 0:嘗試獲取鎖獲取失敗,等待時間為 0,直接放棄獲取鎖(只嘗試一次),因為這里是用戶推薦列表的緩存預熱定時任務,如果獲取鎖失敗,說明已經(jīng)有服務器去執(zhí)行定時任務了,只要執(zhí)行一次就好了,所以不用再去嘗試獲取鎖
3. unlock():釋放鎖,放到 finally 語句塊中執(zhí)行,如果 try 語句塊中的內(nèi)容出現(xiàn)異常,也會釋放鎖,避免發(fā)生死鎖的情況
/*** 緩存預熱定時任務* @author 樂小鑫* @version 1.0*/
@Component
@Slf4j
public class PreCacheUser {@Resourceprivate RedisTemplate redisTemplate;@Resourceprivate UserService userService;@Resourceprivate RedissonClient redissonClient;List<Long> mainUserList = Arrays.asList(3L);// 重要用戶列表,為該列表的用戶開啟緩存預熱@Scheduled(cron = "0 59 21 ? * * ")// 每天 21:59 執(zhí)行定時任務進行用戶數(shù)據(jù)緩存預熱public void doPreCacheUser() {// 獲取鎖對象RLock lock = redissonClient.getLock("langhua:precachejob:doprecache:lock");try {if (lock.tryLock(0,30000L,TimeUnit.MILLISECONDS)) {log.info("get redisson lock" + Thread.currentThread().getId());// 查出用戶存到 Redis 中for (Long userId : mainUserList) {QueryWrapper<User> queryWrapper = new QueryWrapper<>();Page<User> userPage = userService.page(new Page<>(1, 20), queryWrapper);// 查詢所有用戶String key = String.format("langhua:user:recommend:%s", userId);ValueOperations valueOperations = redisTemplate.opsForValue();// 將查詢出來的數(shù)據(jù)寫入緩存try {valueOperations.set(key,userPage,24, TimeUnit.HOURS);} catch (Exception e) {log.error("redis key set error", e);}}}} catch (InterruptedException e) {log.error("redisson precache user error", e);} finally {// 釋放鎖log.info("redisson unlock" + Thread.currentThread().getId());lock.unlock();}}
}