目录
[分布式锁 Redis SETNX](#分布式锁 Redis SETNX)
缓存
缓存中的数据应满足:
- 高频访问,低频更新
- 即时性,一致性 要求不高
本地缓存
java
private Map<String,Object> cache = new HashMap<>();
在service层中引入 hashMap 作为 本地缓存。获取数据时,先访问map,map中没有对应数据,再去查询 数据库。
优点:
-
访问速度快,延迟极低。
-
实现简单,无需额外依赖。
缺点:
-
数据一致性难以保证,多实例环境下,每个实例都有自己 独立的本地缓存。
-
缓存容量有限。
-
实例重启或崩溃会导致缓存丢失。
分布式缓存
将数据存储在独立的缓存服务(如Redis)中,多个应用实例共享同一缓存数据。

优点:
-
数据一致性较好,多个实例共享同一份数据。
-
缓存容量可扩展,支持大规模数据。
-
缓存服务独立,实例重启不会丢失数据。
缺点:
-
访问速度受网络延迟影响。
-
需要额外的缓存服务,增加了系统复杂性。
-
可能存在单点故障问题(需通过集群解决)。
引入缓存
redis-quick-start
1.引入spring-boot-starter-data-redis,自动配置Redis
XML
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2.配置连接信息
XML
redis:
host: 192.168.40.128
port: 6379
3.使用 RedisTemplate 或 StringRedisTemplate
java
public class GulimallProductApplicationTests {
@Autowired
StringRedisTemplate stringRedisTemplate;
@Test
public void redisTest(){
ValueOperations<String,String> valueOperations = stringRedisTemplate.opsForValue();
valueOperations.set("hello","world_" + UUID.randomUUID().toString());
System.out.println(valueOperations.get("hello"));
}
}
三级分类,引入cache
java
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public Map<String, List<Level2CategoryVo>> getLevel2AndLevel3Category() {
//1.先从缓存中获取 catalogJson
ValueOperations<String,String> valueOperations = redisTemplate.opsForValue();
String catalogJson = valueOperations.get("catalogJson");
Map<String, List<Level2CategoryVo>> res = null;
if(StringUtils.isEmpty(catalogJson)){
//缓存中无对应数据,查询数据库
res = getCatalogJsonFromDB();
//查询后 加入缓存
valueOperations.set("catalogJson", JSON.toJSONString(res));
}else{
res = JSON.parseObject(catalogJson,new TypeReference<Map<String, List<Level2CategoryVo>> >(){});
}
return res;
}
缓存穿透、击穿、雪崩
缓存系统中常见的三种问题,它们都会导致缓存失效,进而对数据库造成压力,甚至引发系统崩溃。
缓存穿透
缓存穿透是指查询一个不存在的数据,由于缓存和数据库中都没有,导致每次请求都直接访问数据库。
解决方案:缓存空值
对于查询结果为空的 Key,缓存一个空值(如 null
),并设置较短的过期时间。
缓存雪崩
缓存雪崩是指大量缓存数据在同一时间过期,导致大量请求直接访问数据库,造成数据库压力过大甚至崩溃。
解决方案:分散过期时间
为缓存 Key 设置随机的过期时间,避免同时过期。
缓存击穿
缓存击穿是指某个热点数据在缓存中过期时,大量请求同时涌入数据库,导致数据库压力骤增。
解决方案:互斥锁
当缓存失效时,使用分布式锁(如 Redis 的 SETNX
)确保只有一个线程去加载数据,其他线程等待。
锁
加锁,解决缓存击穿,只有获得锁的线程可以访问数据。
本地锁
service在容器中总是单例的,可以使用当前 Service
对象作为锁来实现本地锁。
synchronized
是 Java 中最简单的锁机制,可以直接锁定当前对象(this
)。
java
@Override
public Map<String, List<Level2CategoryVo>> getLevel2AndLevel3Category() {
//1.先从缓存中获取 catalogJson
ValueOperations<String,String> valueOperations = redisTemplate.opsForValue();
String catalogJson = valueOperations.get("catalogJson");
Map<String, List<Level2CategoryVo>> res = null;
if(StringUtils.isEmpty(catalogJson)){
//缓存中无对应数据,查询数据库
res = getCatalogJsonFromDB();
//查询后 加入缓存
valueOperations.set("catalogJson", JSON.toJSONString(res));
}else{
res = JSON.parseObject(catalogJson,new TypeReference<Map<String, List<Level2CategoryVo>> >(){});
}
return res;
}
/**
* 从数据库中 获取 2 3 级 分类
* @return
*/
private Map<String, List<Level2CategoryVo>> getCatalogJsonFromDB(){
synchronized (this){
//再次确认,有无缓存
ValueOperations<String,String> valueOperations = redisTemplate.opsForValue();
String catalogJson = valueOperations.get("catalogJson");
Map<String, List<Level2CategoryVo>> res = null;
if(StringUtils.isEmpty(catalogJson)) {
//缓存中无对应数据,查询数据库
res = getCatalogJsonFromDB();
//查询后 加入缓存
valueOperations.set("catalogJson", JSON.toJSONString(res));
return res;
}
List<CategoryEntity> selectList = this.baseMapper.selectList(null);
//一级分类
List<CategoryEntity> level1 = this.getByParentCid(selectList,0L);
Map<String, List<Level2CategoryVo>> categoryMap = level1.stream().collect(Collectors.toMap(k -> k.getCatId().toString(),v ->{
//查询该一级分类下的二级分类
List<CategoryEntity> level2List = getByParentCid(selectList,v.getCatId());
List<Level2CategoryVo> level2CategoryVos = null;
if (level2List != null){
level2CategoryVos = level2List.stream().map(level2 -> {
//查询 二级分类 下的 三级分类
List<CategoryEntity> level3List = getByParentCid(selectList,level2.getCatId());
List<Level2CategoryVo.Level3Category> collect = null;
if (level3List != null) {
collect = level3List.stream().map(level3 -> {
Level2CategoryVo.Level3Category level3Category = new Level2CategoryVo.Level3Category(level2.getCatId(), level3.getCatId(), level3.getName());
return level3Category;
}).collect(Collectors.toList());
}
Level2CategoryVo level2CategoryVo = new Level2CategoryVo(v.getCatId(), collect, level2.getCatId(), level2.getName());
return level2CategoryVo;
}).collect(Collectors.toList());
}
return level2CategoryVos;
}));
return categoryMap;
}
}
由于网络io延迟,在redis保存缓存的过程中,会少量查询DB。
**缺点:**本地锁仅适用于单机环境。分布式中各主机获取到锁后,会各自查询数据库。
分布式系统需要使用分布式锁
分布式锁 Redis SETNX
全称为 SET if Not eXists ,用于在键不存在时设置键的值。它是一个原子操作,可用于实现分布式锁。成功set,即为成功获取到 锁

- 先从缓存中获取数据
- 缓存无对应数据,申请锁
- 成功获取锁后,查询数据库
- 释放锁
1.申请锁时,同时指定uuid与过期时间
java
String uuid = UUID.randomUUID().toString();
//必须设置 过期时间,避免死锁,set和过期 必须是原子的
Boolean lock = valueOperations.setIfAbsent("lock",uuid,300,TimeUnit.SECONDS);
设置过期时间,避免发生异常(如断电,程序异常等),导致锁未释放,死锁。
插入键值对和设置过期时间,应为原子操作,不能拆开。
2.释放锁时,匹配uuid
java
if (lock){
System.out.println("成功获取lock...");
//成功获取到锁
Map<String, List<Level2CategoryVo>> res = null;
try {
res = getCatalogJsonFromDB();
}finally {
String script = "if redis.call('get',KEYS[1]) == ARGV[1] " +
"then return redis.call('del',KEYS[1]) " +
"else return 0 " +
"end";
redisTemplate.execute(new DefaultRedisScript<Long>(script,Long.class),Arrays.asList("lock"),uuid);
System.out.println("释放lock");
}
return res;
匹配uuid,只能释放自己的锁,防止在执行业务时间过长,锁过期,误删别人的锁。
使用script,才能同时匹配并删除键值对,原子操作。
3.自旋等待锁
java
System.out.println("自旋等待lock...");
//没有获取到锁,自旋等待
try {
Thread.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return getCatalogJsonFromDBWithLock();
完整核心代码
java
@Override
public Map<String, List<Level2CategoryVo>> getLevel2AndLevel3Category() {
//1.先从缓存中获取 catalogJson
ValueOperations<String,String> valueOperations = redisTemplate.opsForValue();
String catalogJson = valueOperations.get("catalogJson");
Map<String, List<Level2CategoryVo>> res = null;
if(StringUtils.isEmpty(catalogJson)){
//缓存中无对应数据,查询数据库
res = getCatalogJsonFromDBWithLock();
}else{
res = JSON.parseObject(catalogJson,new TypeReference<Map<String, List<Level2CategoryVo>> >(){});
}
return res;
}
/**
* 获取分布式锁,查询数据库
* @return
*/
private Map<String, List<Level2CategoryVo>> getCatalogJsonFromDBWithLock(){
ValueOperations<String,String> valueOperations = redisTemplate.opsForValue();
String uuid = UUID.randomUUID().toString();
//必须设置 过期时间,避免死锁,set和过期 必须是原子的
Boolean lock = valueOperations.setIfAbsent("lock",uuid,300,TimeUnit.SECONDS);
if (lock){
System.out.println("成功获取lock...");
//成功获取到锁
Map<String, List<Level2CategoryVo>> res = null;
try {
res = getCatalogJsonFromDB();
}finally {
String script = "if redis.call('get',KEYS[1]) == ARGV[1] " +
"then return redis.call('del',KEYS[1]) " +
"else return 0 " +
"end";
redisTemplate.execute(new DefaultRedisScript<Long>(script,Long.class),Arrays.asList("lock"),uuid);
System.out.println("释放lock");
}
return res;
}else{
System.out.println("自旋等待lock...");
//没有获取到锁,自旋等待
try {
Thread.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return getCatalogJsonFromDBWithLock();
}
}
/**
* 从数据库中 获取 2 3 级 分类
* @return
*/
private Map<String, List<Level2CategoryVo>> getCatalogJsonFromDB(){
//获得锁后,再次确认,有无缓存
ValueOperations<String,String> valueOperations = redisTemplate.opsForValue();
String catalogJson = valueOperations.get("catalogJson");
Map<String, List<Level2CategoryVo>> res = null;
if(!StringUtils.isEmpty(catalogJson)) {
//缓存有数据
res = JSON.parseObject(catalogJson,new TypeReference<Map<String, List<Level2CategoryVo>> >(){});
return res;
}
System.out.println("查询数据库...");
//缓存中无数据,再查询数据库
List<CategoryEntity> selectList = this.baseMapper.selectList(null);
//一级分类
List<CategoryEntity> level1 = this.getByParentCid(selectList,0L);
Map<String, List<Level2CategoryVo>> categoryMap = level1.stream().collect(Collectors.toMap(k -> k.getCatId().toString(),v ->{
//查询该一级分类下的二级分类
List<CategoryEntity> level2List = getByParentCid(selectList,v.getCatId());
List<Level2CategoryVo> level2CategoryVos = null;
if (level2List != null){
level2CategoryVos = level2List.stream().map(level2 -> {
//查询 二级分类 下的 三级分类
List<CategoryEntity> level3List = getByParentCid(selectList,level2.getCatId());
List<Level2CategoryVo.Level3Category> collect = null;
if (level3List != null) {
collect = level3List.stream().map(level3 -> {
Level2CategoryVo.Level3Category level3Category = new Level2CategoryVo.Level3Category(level2.getCatId(), level3.getCatId(), level3.getName());
return level3Category;
}).collect(Collectors.toList());
}
Level2CategoryVo level2CategoryVo = new Level2CategoryVo(v.getCatId(), collect, level2.getCatId(), level2.getName());
return level2CategoryVo;
}).collect(Collectors.toList());
}
return level2CategoryVos;
}));
//查询后 加入缓存
valueOperations.set("catalogJson", JSON.toJSONString(categoryMap),1, TimeUnit.DAYS);
return categoryMap;
}