文章目錄
- 前言
- hash 對比 String
- 簡單存儲對象
- 【秒殺系統】- 商品庫存管理
- 【用戶會話管理】- 分布式Session存儲
- 【信息預熱】- 首頁信息預熱
- 降級策略
- 總結
前言
上文我們分析了String類型 在多并發下的應用 本文該輪到 Hash了,期不期待 兄弟們 hhh
Redis常用數據結構以及多并發場景下的使用分析:String類型
okok 那么hash 相對于String類型有哪些優勢呢?
hash 對比 String
舉一個簡單的例子:
你可以看到 hash 在面對存在結構化的數據 會更有優勢 方便統一管理
適合存儲對象
// 單獨String
redisTemplate.opsForValue().set("user:123:name", "張三");
redisTemplate.opsForValue().set("user:123:age", "25");
redisTemplate.opsForValue().set("user:123:city", "北京");
// 問題:3次網絡往返 + 3個key占用更多內存// 一個HashMap
Map<String, Object> userInfo = new HashMap<>();
userInfo.put("name", "張三");
userInfo.put("age", "25");
userInfo.put("city", "北京");redisTemplate.opsForHash().putAll("user:123", userInfo);
// 優勢:1次網絡往返 + 內存更緊湊
簡單存儲對象
@Service
@RequiredArgsConstructor
public class HashRedisService {private final RedisTemplate<String, Object> redisTemplate;// 商品屬性存儲public void saveProduct(String productId, Product product) {String key = "product:" + productId;Map<String, Object> productMap = new HashMap<>();productMap.put("name", product.getName());productMap.put("price", product.getPrice().toString());productMap.put("category", product.getCategory());productMap.put("stock", product.getStock().toString());redisTemplate.opsForHash().putAll(key, productMap);}}
【秒殺系統】- 商品庫存管理
設計思路:
使用Hash存儲商品庫存信息,避免熱點key問題
Key: seckill:stock:date
Field: productId
Value: stockLua腳本 原子化扣減庫存Value
·1. 獲取當前庫存(HGET key field)
·2. 判斷商品是否存在
·3. 判斷庫存是否足夠
·4. 使用 HINCRBY 扣減庫存(負數)
使用 HMGET 命令批量獲取庫存 例如
·HMGET seckill:stock:2025-07-08 1001 1002 1003
// 1. 【秒殺系統】- 商品庫存管理
@Service
@RequiredArgsConstructor
@Slf4j
public class SeckillStockService {private final RedisTemplate<String, Object> redisTemplate;/*** 使用Hash存儲商品庫存信息,避免熱點key問題* Key: seckill:stock:date* Field: productId* Value: stock*/public void initSeckillStock(String date, Map<String, Integer> productStocks) {String stockKey = "seckill:stock:" + date;// 批量初始化庫存,比逐個set效率高很多Map<String, Object> stockMap = new HashMap<>();productStocks.forEach((productId, stock) -> {stockMap.put(productId, stock);});redisTemplate.opsForHash().putAll(stockKey, stockMap);redisTemplate.expire(stockKey, Duration.ofDays(1));log.info("初始化秒殺庫存完成,商品數量: {}", productStocks.size());}/*** 高并發扣減庫存 - 使用Lua腳本保證原子性*/public boolean decrementStock(String date, String productId, int quantity) {String stockKey = "seckill:stock:" + date;String luaScript = """local stockKey = KEYS[1]local productId = KEYS[2] local quantity = tonumber(ARGV[1])local currentStock = redis.call('HGET', stockKey, productId)if currentStock == false thenreturn -1 -- 商品不存在endcurrentStock = tonumber(currentStock)if currentStock < quantity thenreturn 0 -- 庫存不足endredis.call('HINCRBY', stockKey, productId, -quantity)return 1 -- 扣減成功""";DefaultRedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);Long result = redisTemplate.execute(script, Arrays.asList(stockKey, productId),quantity);return result != null && result == 1;}/*** 批量查詢庫存狀態 - 單次查詢多個商品*/public Map<String, Integer> batchGetStock(String date, List<String> productIds) {String stockKey = "seckill:stock:" + date;// 使用HMGet批量獲取,比多次HGet效率高List<Object> stocks = redisTemplate.opsForHash().multiGet(stockKey,new ArrayList<>(productIds));Map<String, Integer> result = new HashMap<>();for (int i = 0; i < productIds.size(); i++) {Object stock = stocks.get(i);result.put(productIds.get(i), stock != null ? (Integer) stock : 0);}return result;}
}
寫一個測試類 去測試 這個扣減庫存的邏輯
@SpringBootTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class SeckillStockServiceTest {@Autowiredprivate SeckillStockService seckillStockService;@Autowiredprivate RedisTemplate<String, Object> redisTemplate;private static final String TEST_DATE = "2025-07-07";@Test@Order(1)void testInitSeckillStock() {Map<String, Integer> stockMap = new HashMap<>();stockMap.put("p1001", 10);stockMap.put("p1002", 5);stockMap.put("p1003", 0);seckillStockService.initSeckillStock(TEST_DATE, stockMap);Map<String, Integer> result = seckillStockService.batchGetStock(TEST_DATE, List.of("p1001", "p1002", "p1003"));assertEquals(10, result.get("p1001"));assertEquals(5, result.get("p1002"));assertEquals(0, result.get("p1003"));}@Test@Order(2)void testDecrementStockSuccess() {boolean success = seckillStockService.decrementStock(TEST_DATE, "p1001", 2);assertTrue(success);Map<String, Integer> result = seckillStockService.batchGetStock(TEST_DATE, List.of("p1001"));assertEquals(8, result.get("p1001"));}@Test@Order(3)void testDecrementStockFailDueToNotEnough() {boolean success = seckillStockService.decrementStock(TEST_DATE, "p1003", 1);assertFalse(success); // 原庫存是 0,無法扣減}@Test@Order(4)void testDecrementStockFailDueToNonExistProduct() {boolean success = seckillStockService.decrementStock(TEST_DATE, "p9999", 1);assertFalse(success); // 商品不存在,Lua 返回 -1,也處理為 false}}
【用戶會話管理】- 分布式Session存儲
設計思路:
用戶登錄 - 創建分布式Session
Key: session:userId
Fields: token, loginTime, lastActiveTime, deviceInfo, permissions…
Value: token_value,loginTime_value…
Lua腳本 權限檢查 - 快速獲取用戶權限
·1. HGET session:{userId} permissions
·2. 判斷權限信息是否存在
Lua腳本 更新用戶活躍時間 - 只更新單個字段
·1. HSET session:123 lastActiveTime 1720492341255
// 2. 【用戶會話管理】- 分布式Session存儲
@Service
@RequiredArgsConstructor
public class UserSessionService {private final RedisTemplate<String, Object> redisTemplate;/*** 用戶登錄 - 創建分布式Session* Key: session:userId* Fields: token, loginTime, lastActiveTime, deviceInfo, permissions...*/public String createUserSession(String userId, String deviceInfo, Set<String> permissions) {String sessionKey = "session:" + userId;String token = generateToken();long currentTime = System.currentTimeMillis();Map<String, Object> sessionData = new HashMap<>();sessionData.put("token", token);sessionData.put("loginTime", currentTime);sessionData.put("lastActiveTime", currentTime);sessionData.put("deviceInfo", deviceInfo);sessionData.put("permissions", String.join(",", permissions));sessionData.put("status", "active");// 一次性存儲所有session數據redisTemplate.opsForHash().putAll(sessionKey, sessionData);redisTemplate.expire(sessionKey, Duration.ofHours(24));return token;}/*** 更新用戶活躍時間 - 只更新單個字段*/public void updateLastActiveTime(String userId) {String sessionKey = "session:" + userId;redisTemplate.opsForHash().put(sessionKey, "lastActiveTime", System.currentTimeMillis());// 延長session過期時間redisTemplate.expire(sessionKey, Duration.ofHours(24));}/*** 權限檢查 - 快速獲取用戶權限*/public boolean hasPermission(String userId, String permission) {String sessionKey = "session:" + userId;Object permissions = redisTemplate.opsForHash().get(sessionKey, "permissions");if (permissions == null) return false;String permissionStr = (String) permissions;return Arrays.asList(permissionStr.split(",")).contains(permission);}/*** 批量獲取在線用戶信息*/public Map<String, Map<Object, Object>> batchGetUserSessions(List<String> userIds) {Map<String, Map<Object, Object>> result = new HashMap<>();// 使用Pipeline批量獲取,避免多次網絡往返List<Object> sessionData = redisTemplate.executePipelined(new SessionCallback<Object>() {@Override@SuppressWarnings("unchecked")public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {for (String userId : userIds) {operations.opsForHash().entries((K) ("session:" + userId));}return null;}});for (int i = 0; i < userIds.size(); i++) {@SuppressWarnings("unchecked")Map<Object, Object> session = (Map<Object, Object>) sessionData.get(i);if (session != null && !session.isEmpty()) {result.put(userIds.get(i), session);}}return result;}private String generateToken() {return UUID.randomUUID().toString().replace("-", "");}
}
寫一個測試類去測試獲取權限信息
@SpringBootTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class UserSessionServiceTest {@Autowiredprivate UserSessionService userSessionService;@Autowiredprivate RedisTemplate<String, Object> redisTemplate;@Test@Order(1)void testCreateUserSession() {String userId = "user123";String deviceInfo = "iPhone 15 Pro";Set<String> permissions = Set.of("read", "write", "admin");String token = userSessionService.createUserSession(userId, deviceInfo, permissions);assertNotNull(token);assertFalse(token.isEmpty());// 驗證session數據是否正確存儲String sessionKey = "session:" + userId;Map<Object, Object> sessionData = redisTemplate.opsForHash().entries(sessionKey);assertEquals(token, sessionData.get("token"));assertEquals(deviceInfo, sessionData.get("deviceInfo"));
// assertEquals("read,write,admin", sessionData.get("permissions"));assertEquals("active", sessionData.get("status"));System.out.println("用戶會話創建成功,Token: " + token);}@Test@Order(2)void testUpdateLastActiveTime() throws InterruptedException {String userId = "user123";// 獲取初始時間Object initialTime = redisTemplate.opsForHash().get("session:" + userId, "lastActiveTime");Thread.sleep(10); // 等待一小段時間// 更新活躍時間userSessionService.updateLastActiveTime(userId);// 驗證時間是否更新Object updatedTime = redisTemplate.opsForHash().get("session:" + userId, "lastActiveTime");assertNotEquals(initialTime, updatedTime);System.out.println("用戶活躍時間更新成功");}@Test@Order(3)void testHasPermission() {String userId = "user123";assertTrue(userSessionService.hasPermission(userId, "read"));assertTrue(userSessionService.hasPermission(userId, "write"));assertTrue(userSessionService.hasPermission(userId, "admin"));assertFalse(userSessionService.hasPermission(userId, "delete"));System.out.println("權限檢查功能正常");}@Test@Order(4)void testBatchGetUserSessions() {// 創建多個用戶會話String userId2 = "user456";String userId3 = "user789";userSessionService.createUserSession(userId2, "Android Phone", Set.of("read"));userSessionService.createUserSession(userId3, "MacBook Pro", Set.of("read", "write"));// 批量獲取用戶會話List<String> userIds = List.of("user123", userId2, userId3, "nonexistent");Map<String, Map<Object, Object>> sessions = userSessionService.batchGetUserSessions(userIds);assertEquals(3, sessions.size()); // 應該返回3個存在的用戶會話assertTrue(sessions.containsKey("user123"));assertTrue(sessions.containsKey(userId2));assertTrue(sessions.containsKey(userId3));assertFalse(sessions.containsKey("nonexistent"));// 驗證批量獲取的數據正確性Map<Object, Object> user123Session = sessions.get("user123");assertEquals("iPhone 15 Pro", user123Session.get("deviceInfo"));assertEquals("active", user123Session.get("status"));System.out.println("批量獲取用戶會話功能正常");System.out.println("獲取到 " + sessions.size() + " 個用戶會話");}// @AfterAll
// static void cleanup(@Autowired RedisTemplate<String, Object> redisTemplate) {
// // 清理測試數據
// redisTemplate.delete("session:user123");
// redisTemplate.delete("session:user456");
// redisTemplate.delete("session:user789");
// System.out.println("測試數據清理完成");
// }
}
【信息預熱】- 首頁信息預熱
設計思路:
緩存預熱就是在系統啟動時就“主動把常用數據裝進 Redis”,讓用戶訪問時“直接命中緩存” 一下是一個簡單的使用場景
// 【緩存預熱】- 提升系統啟動速度
@Service
@RequiredArgsConstructor
public class CacheWarmupService {private final RedisTemplate<String, Object> redisTemplate;private final ProductService productService;private final UserService userService;/*** 商品信息緩存預熱*/@EventListener(ApplicationReadyEvent.class)public void warmupProductCache() {log.info("開始商品緩存預熱...");// 首先從數據庫 獲取熱門商品列表List<Product> hotProducts = productService.getHotProducts(1000);// 批量緩存商品信息Map<String, Map<String, Object>> productBatch = new HashMap<>();for (Product product : hotProducts) {Map<String, Object> productInfo = new HashMap<>();productInfo.put("name", product.getName());productInfo.put("price", product.getPrice().toString());productInfo.put("category", product.getCategory());productInfo.put("brand", product.getBrand());productInfo.put("stock", product.getStock());productInfo.put("sales", product.getSales());productInfo.put("rating", product.getRating());productBatch.put("product:" + product.getId(), productInfo);}// 使用Pipeline批量預熱redisTemplate.executePipelined(new SessionCallback<Object>() {@Overridepublic <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {productBatch.forEach((key, productInfo) -> {operations.opsForHash().putAll((K) key, productInfo);operations.expire((K) key, Duration.ofHours(6));});return null;}});log.info("商品緩存預熱完成,緩存商品數量: {}", hotProducts.size());}}
降級策略
那么如果當redis不可用了 該怎么處理呢?那么就應該使用多級緩存的思想 樓主后續也會專門寫一個文章去講解多級緩存 請期待 這里首先給出一個簡單的 降級策略 代碼片段
public Map<String, Object> getProductInfoWithFallback(String productId) {try {// 優先從 Redis 緩存中讀取商品詳情return redisTemplate.opsForHash().entries("product:" + productId);} catch (Exception e) {// Redis 報錯時(如連接失敗、超時等),降級處理log.warn("Redis查詢失敗,降級到數據庫", e);// 從數據庫中查詢return productService.getFromDatabase(productId);}
}
總結
使用hash結構去存儲結構化的數據 例如 本質都是一種緩存的思想
網頁的首頁展示 (你想想不可能去數據庫查詢吧 響應太慢了)
電商系統 庫存管理
用戶系統 用戶信息權限管理
排行榜 管理點贊數量
配置中心 …