文章目录
- 前言
- 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结构去存储结构化的数据 例如 本质都是一种缓存的思想
网页的首页展示 (你想想不可能去数据库查询吧 响应太慢了)
电商系统 库存管理
用户系统 用户信息权限管理
排行榜 管理点赞数量
配置中心 …