1 基本原理
2 实现方法
2.1 实现代码
修改后的三个方法:
getCatalogJson
@Override
public Map<String, List<Catelog2Vo>> getCatalogJson() {
// 给缓存中放JSON字符串,拿出的JSON字符串,还需要逆转为能用的对象类型
String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");
if (!StringUtils.hasText(catalogJSON)) {
// 缓存中没有,查询数据库
System.out.println("缓存不命中,查询数据库...");
return getCatalogJsonFromDBWithRedisLock();
}
System.out.println("缓存命中,直接返回");
// 将缓存中查询出的json转换为指定的对象
Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
});
return result;
}
getCatalogJsonFromDBWithRedisLock
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDBWithRedisLock() {
// 1. 占分布式锁
String uuid = UUID.randomUUID().toString();
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
if (lock) {
// 加锁成功,执行业务
System.out.println("获取分布式锁成功!!!");
Map<String, List<Catelog2Vo>> catalogJsonFromDB;
try {
catalogJsonFromDB = getCatalogJsonFromDB();
} finally {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
// 删除锁
Long lock1 = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
Arrays.asList("lock"), uuid);
}
return catalogJsonFromDB;
}else {
// 加锁失败,重试
// TODO 休眠100s重试
System.out.println("获取分布式锁失败,等待重试...");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getCatalogJsonFromDBWithRedisLock(); // 自旋获取锁
}
}
getCatalogJsonFromDB
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDB() {
// 得到锁以后,在去缓存中确定一次
String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");
if (!StringUtils.isEmpty(catalogJSON)) {
Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
});
return result;
}
System.out.println("查询了数据库");
// 查询出表pms_category所有的记录实体
List<CategoryEntity> categoryEntityList = baseMapper.selectList(null);
// 查出所有的一次分类
List<CategoryEntity> level_1_categorys = getParent_cid(categoryEntityList, 0L);
// 封装数据,构造一个以1级id为键,2级分类列表为值的map
Map<String, List<Catelog2Vo>> collect = level_1_categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), l1 -> {
// 根据一级分类id查找二级分类
List<CategoryEntity> level_2_categorys = getParent_cid(categoryEntityList, l1.getCatId());
// 封装结果为Catelog2Vo的集合
List<Catelog2Vo> catelog2Vos = null;
if (level_2_categorys != null) {
// 把 level_2_categorys 封装为 catelog2Vos
catelog2Vos = level_2_categorys.stream().map(l2 -> {
Catelog2Vo catelog2Vo = new Catelog2Vo(l1.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
// 根据二级分类id查找三级分类
List<CategoryEntity> level_3_categorys = getParent_cid(categoryEntityList, l2.getCatId());
// 将 level_3_categorys 封装为 catelog3Vos
if (level_3_categorys != null) {
List<Catelog2Vo.Catelog3Vo> catelog3Vos = level_3_categorys.stream().map(l3 -> {
Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
return catelog3Vo;
}).collect(Collectors.toList());
catelog2Vo.setCatalog3List(catelog3Vos);
}
return catelog2Vo;
}).collect(Collectors.toList());
}
return catelog2Vos;
}));
// 将查到的数据放入缓存
String s = JSON.toJSONString(collect);
stringRedisTemplate.opsForValue().set("catalogJSON", s);
return collect;
}
3 测试分布式锁
1、复制几个product服务配置,设置不同的端口。记得启动gateway服务。
2、配置测试http
3、JMeter测试,查看创建的几个product服务的打印信息中是否只有一次==“查询了数据库”==
4 Redisson
**文档地址:**https://github.com/redisson/redisson/wiki/Table-of-Content
4.1 实践
4.1.1 导入依赖
我的SpringBoot版本是2.6.6,导入3.12.0版本的redisson有冲突,导入依赖失败,于是选择了3.16.4版本的
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.4</version>
</dependency>
4.1.2 配置
**配置方法文档:**https://github.com/redisson/redisson/wiki/2.-%E9%85%8D%E7%BD%AE%E6%96%B9%E6%B3%95
**第三方框架整合文档:**https://github.com/redisson/redisson/wiki/14.-%E7%AC%AC%E4%B8%89%E6%96%B9%E6%A1%86%E6%9E%B6%E6%95%B4%E5%90%88
创建MyRedissonConfig配置类,路径:com/atguigu/gulimall/product/config/MyRedissonConfig.java
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyRedissonConfig {
@Bean(destroyMethod = "shutdown")
public RedissonClient redisson() {
// 1. 创建配置
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.56.10:6379");
// 2. 根据Config创建出RedissonClient
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
4.1.3 测试
@Autowired
RedissonClient redissonClient;
@Test
void redisson() {
System.out.println(redissonClient);
}
4.2 lock锁测试
**分布式锁文档:**https://github.com/redisson/redisson/wiki/8.-%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E5%92%8C%E5%90%8C%E6%AD%A5%E5%99%A8
1、修改hello方法,路径:com/atguigu/gulimall/product/web/IndexController.java
@ResponseBody
@GetMapping("/hello")
public String hello() {
// 1. 获取一把锁,只要锁的名字一样,就是同一把锁
RLock lock = redissonClient.getLock("myLock");
// 2. 上锁
lock.lock(); // 会自动续期
// lock.lock(10, TimeUnit.SECONDS); // 10秒后解锁,不自动续期
try {
System.out.println("上锁成功,执行业务....当前线程号:" + Thread.currentThread().getId());
Thread.sleep(30000);
} catch (Exception e) {
} finally {
// 3. 解锁
System.out.println("解锁...当前线程号:" + Thread.currentThread().getId());
lock.unlock();
}
return "hello";
}
lock.lock();
是阻塞式等待,默认锁的过期时间(TTL)为30秒。锁可以自动续租,原理就是维持了一个定时任务,给隔10 秒把锁的过期时间设置为30 秒。如果这时候 redison 客户端退出,这个续期的定时任务被释放,锁就会过期。
一般选择
lock.lock(10, TimeUnit.SECONDS);
设置指定过期时间
4.3 读写锁测试
创建writeValue
和readValue
方法,路径:com/atguigu/gulimall/product/web/IndexController.java
读读共享、读写互斥、写写互斥
@Autowired
StringRedisTemplate stringRedisTemplate;
@GetMapping("/write")
@ResponseBody
public String writeValue() {
RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("rw-lock");
String s = "";
RLock wLock = readWriteLock.writeLock();
wLock.lock();
try {
s = UUID.randomUUID().toString();
Thread.sleep(30000);
stringRedisTemplate.opsForValue().set("writeValue", s);
} catch (Exception e) {
e.printStackTrace();
} finally {
wLock.unlock();
}
return s;
}
@GetMapping("/read")
@ResponseBody
public String readValue() {
RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("rw-lock");
String s = "";
RLock rLock = readWriteLock.readLock();
rLock.lock();
try {
s = stringRedisTemplate.opsForValue().get("writeValue");
} catch (Exception e) {
e.printStackTrace();
} finally {
rLock.unlock();
}
return s;
}
4.4 闭锁测试
创建lockDoor
和gogogo
方法,路径:com/atguigu/gulimall/product/web/IndexController.java
@GetMapping("/lockDoor")
@ResponseBody
public String lockDoor() throws InterruptedException {
RCountDownLatch door = redissonClient.getCountDownLatch("door");
door.trySetCount(5);
door.await(); // 等待闭锁完成
return "放假了...";
}
@GetMapping("/gogogo/{id}")
@ResponseBody
public String gogogo(@PathVariable("id") Long id) {
RCountDownLatch door = redissonClient.getCountDownLatch("door");
door.countDown();
return id + "班的人都走了...";
}
4.5 信号量测试
1、创建park
和go
方法,路径:com/atguigu/gulimall/product/web/IndexController.java
@GetMapping("/park")
@ResponseBody
public String park() throws InterruptedException {
RSemaphore park = redissonClient.getSemaphore("park");
// park.acquire(); // 获取一个信号
boolean b = park.tryAcquire();
return "ok -> " + b;
}
@GetMapping("/go")
@ResponseBody
public String go() throws InterruptedException {
RSemaphore park = redissonClient.getSemaphore("park");
park.release(); // 释放一个车位
return "ok";
}
2、在redis里添加一个缓存
3、访问park和go进行测试
5 缓存一致性解决
5.1 双写模式
即修改数据库后在修改缓存。
存在的问题:
假如一个修改数据库的请求 P1 进来,修改了数据库里的记录 A -> B,但由于某些原因,cpu开始执行另一个修改数据库的请求,把记录 B 又改回了 A,然后先执行了 P2 的写缓存。最后在执行 P1 的写缓存。最终导致数据库的数据和缓存的数据不一致,数据库里的记录是 A,而缓存里的记录是 B
5.2 失效模式
即修改完数据库某个数据后,删除缓存里的对应的数据内容
存在的问题:
读写如果并发执行,可能读到的是脏数据
5.3 解决方案分析
无论是双写模式还是失效模式,都会导致缓存的不一致问题。即多个实例同时更新会出事。怎么办?
- 如果是用户纬度数据(订单数据、用户数据),这种并发几率非常小,不用考虑这个问题,缓存数据加上过期时间,每隔一段时间触发读的主动更新即可
- 如果是菜单,商品介绍等基础数据,也可以去使用canal订阅binlog的方式。
- 缓存数据+过期时间也足够解决大部分业务对于缓存的要求。
- 通过加锁保证并发读写,写写的时候按顺序排好队。读读无所谓。所以适合使用读写锁。(业务不关心脏数据,允许临时脏数据可忽略);
总结:
- 我们能放入缓存的数据本就不应该是实时性、一致性要求超高的。所以缓存数据的时候加上过期时间,保证每天拿到当前最新数据即可。
- 我们不应该过度设计,增加系统的复杂性
- 遇到实时性、一致性要求高的数据,就应该查数据库,即使慢点。
我们最终选择失效模式 + 读写锁的方式