缓存
1、分布式本地缓存
分布式缓存-本地模式在分布式下的问题
这种情况下,每个服务维持一个缓存,所带来的问题:
(1)缓存不共享
在这种情况下,每个服务都有一个缓存,但是这个缓存并不共享,水平上当调度到另外一个台设备上的时候,可能它的服务中并不存在这个缓存,因此需要重新查询。
(2)缓存一致性问题
在一台设备上的缓存更新后,其他设备上的缓存可能还未更新,这样当从其他设备上获取数据的时候,得到的可能就是未给更新的数据。
2、分布式缓存
在这种下,一个服务的不同副本共享同一个缓存空间,缓存放置到缓存中间件中,这个缓存中间件可以是redis等,而且缓存中间件也是可以水平或纵向扩展的,如Redis可以使用redis集群。它打破了缓存容量的限制,能够做到高可用,高性能。
3、整合redis测试
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 源码提供的两种操作方法
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
//将保存进入Redis的键值都是Object
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean
//保存进Redis的数据,键值是(String,String)
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
- StringRedisTemplate
public class StringRedisTemplate extends RedisTemplate<String, String> {
/**
* Constructs a new <code>StringRedisTemplate</code> instance. {@link #setConnectionFactory(RedisConnectionFactory)}
* and {@link #afterPropertiesSet()} still need to be called.
*/
public StringRedisTemplate() {
setKeySerializer(RedisSerializer.string());//键序列化为String
setValueSerializer(RedisSerializer.string());//key序列化为String
setHashKeySerializer(RedisSerializer.string());
setHashValueSerializer(RedisSerializer.string());
}
redis:
host: 服务器地址
port: 6379
- 使用SpringBoot自动配置好的"StringRedisTemplate"来操作redis
@Autowired
StringRedisTemplate stringRedisTemplate;
@Test
public void testStringRedisTemplate(){
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
ops.set("hello","word_"+UUID.randomUUID().toString());
String hello = ops.get("hello");
System.out.println(hello);
}
@Test
public void testFindPath(){
Long[] catelogPath = categoryService.findCatelogPath(225L);
log.info("完整路径:{}", Arrays.asList(catelogPath));
}
4、三级分类业务改造(加入缓存)
@Override
public Map<String, List<Catelog2Vo>> getCatelogJson() {
/**
* 1、空结果缓存:解决缓存雪崩
* 2、设置过期时间(加随机值),解决缓存雪崩
* 3、加锁,解决缓存击穿
*/
//1、加入缓存逻辑
String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
if (StringUtils.isEmpty(catalogJSON)) {
//2、缓存中没有数据,查数据库
Map<String, List<Catelog2Vo>> catelogJsonFromDb = getCatelogJsonFromDbWithLocalLock();
return catelogJsonFromDb;
}
//转为我们指定的对象。
Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
});
return result;
}
public Map<String, List<Catelog2Vo>> getDataFromDb(){
//得到锁之后,应该再去缓存中确定一次,如果没有才需要继续查询
String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
if (!StringUtils.isEmpty(catalogJSON)) {
//缓存不为null直接返回
Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
});
return result;
}
List<CategoryEntity> entityList = baseMapper.selectList(null);
// 查询所有一级分类
List<CategoryEntity> level1 = getCategoryEntities(entityList, 0L);
Map<String, List<Catelog2Vo>> parent_cid = level1.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
// 拿到每一个一级分类 然后查询他们的二级分类
List<CategoryEntity> entities = getCategoryEntities(entityList, v.getCatId());
List<Catelog2Vo> catelog2Vos = null;
if (entities != null) {
catelog2Vos = entities.stream().map(l2 -> {
Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), l2.getName(), l2.getCatId().toString(), null);
// 找当前二级分类的三级分类
List<CategoryEntity> level3 = getCategoryEntities(entityList, l2.getCatId());
// 三级分类有数据的情况下
if (level3 != null) {
List<Catalog3Vo> catalog3Vos = level3.stream().map(l3 -> new Catalog3Vo(l3.getCatId().toString(), l3.getName(), l2.getCatId().toString())).collect(Collectors.toList());
catelog2Vo.setCatalog3List(catalog3Vos);
}
return catelog2Vo;
}).collect(Collectors.toList());
}
return catelog2Vos;
}));
//3、查询到的数据放入缓存,转json存
String s = JSON.toJSONString(parent_cid);
redisTemplate.opsForValue().set("catalogJSON", s, 1, TimeUnit.DAYS);
return parent_cid;
}
/**
* 从数据库查询封装并分类数据,本地锁
*/
public Map<String, List<Catelog2Vo>> getCatelogJsonFromDbWithLocalLock() {
/**
* 1、只要是一把锁,就能锁住需要这个锁的所有线程
* synchronize(this)springboot所有的组件在容器中都是单例的
*/
//TODO 本地锁:synchronize在分布式的情况下,想要锁住所有,必须使用分布式锁
synchronized (this) {
return getDataFromDb();
}
}
/**
* 分布式锁
*
* @return
*/
public Map<String, List<Catelog2Vo>> getCatelogJsonFromDbWithRedisLock() {
//1、占用分布式锁,去redis占坑;setNx
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
if (lock){
//占座成功
Map<String, List<Catelog2Vo>> dataFromDb;
try{
dataFromDb = getDataFromDb();
}finally {
//删除锁,获取值对比+对比成功删除=原子操作 lua脚本解锁
// String lockValue = redisTemplate.opsForValue().get("lock");
// if (uuid.equals(lockValue))
// redisTemplate.delete("lock");
String script = "if redis call('get',KEYS[1]) == ARGV[1] then return redis call('del',KEYS[1]) else return 0 end";
Long lock1 = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class)
, Arrays.asList("lock"), uuid);
}
return dataFromDb;
}else {
//占锁失败,重试,自旋
try {
Thread.sleep(200);
}catch (Exception e){
}
return getCatelogJsonFromDbWithRedisLock();
}
}
/**
* 第一次查询的所有 CategoryEntity 然后根据 parent_cid去这里找
*/
private List<CategoryEntity> getCategoryEntities(List<CategoryEntity> entityList, Long parent_cid) {
return entityList.stream().filter(item -> item.getParentCid().equals(parent_cid)).collect(Collectors.toList());
}
5、本地锁和分布式锁
本地锁的问题
- 多个服务还是会查询数据库
@Override
public Map<String, List<Catelog2Vo>> getCatelogJson() {
//给缓存中放json字符串,拿出json字符串,还要逆转为能用的对象类型【序列化与反序列化】
/**
* 1、空结果缓存,解决缓存穿透
* 2、设置过期时间(随机加值);解决缓存雪崩
* 3、加锁,解决缓存击穿
*/
//1、加入缓存逻辑
//JSON好处是跨语言,跨平台兼容。
String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
if (StringUtils.isEmpty(catalogJSON)){
//2、缓存中没有,查询数据库
System.out.println("缓存不命中.....将要查询数据库...");
Map<String, List<Catelog2Vo>> catelogJsonFromDB = getCatelogJsonFromDB();
}
System.out.println("缓存命中...直接返回...");
//转为我们指定的对象。
Map<String,List<Catelog2Vo>> result = JSON.parseObject(catalogJSON,new TypeReference<Map<String,List<Catelog2Vo>>>(){});
return result;
}
//从数据库查询并封装分类数据
public Map<String, List<Catelog2Vo>> getCatelogJsonFromDB() {
//只要同一把锁就能锁住需要这个锁的所有线程
//1、synchronized(this):SpringBoot所有的组件在容器中都是单例的
// TODO 本地锁:synchronized,JUC(lock)。在分布式情况下想要锁住所有,必须使用分布式锁
//使用DCL(双端检锁机制)来完成对于数据库的访问
synchronized (this){
//得到锁以后,我们应该再去缓存中确定一次,如果没有才需要继续查询
String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
if (!StringUtils.isEmpty(catalogJSON)){
//如果缓存不为null直接缓存
Map<String,List<Catelog2Vo>> result = JSON.parseObject(catalogJSON,new TypeReference<Map<String,List<Catelog2Vo>>>(){});
return result;
}
System.out.println("查询了数据库。。。。。");
/**
* 1、将数据库的多次查询变为1次
*/
List<CategoryEntity> selectList = baseMapper.selectList(null);
//查出所有一级分类
List<CategoryEntity> level1Category = getParent_cid(selectList,0L);
//2、封装数据
Map<String, List<Catelog2Vo>> parent_cid = level1Category.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
//1、每一个的一级分类,查到这个一级分类的二级分类
List<CategoryEntity> categoryEntities = getParent_cid(selectList,v.getCatId());
//2、封装上面的结果
List<Catelog2Vo> catelog2Vos = null;
if (categoryEntities != null) {
catelog2Vos = categoryEntities.stream().map(l2 -> {
Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
//1、找当前二级分类的三级分类封装vo
List<CategoryEntity> level3Catelog = getParent_cid(selectList,l2.getCatId());
if (level3Catelog != null){
List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> {
//2、封装成指定格式
Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
return catelog3Vo;
}).collect(Collectors.toList());
catelog2Vo.setCatelog3List(Collections.singletonList(collect));
}
return catelog2Vo;
}).collect(Collectors.toList());
}
return catelog2Vos;
}));
//3、将查到的数据再放入缓存,将对象转为JSON在缓存中
String jsonString = JSON.toJSONString(parent_cid);
redisTemplate.opsForValue().set("catalogJSON",jsonString,1, TimeUnit.DAYS);
return parent_cid;
}
}
通过观察日志,能够发现只有一个线程查询了数据库,其他线程都是直接从缓存中获取到数据的。所以在单体应用上实现了多线程的并发访问。
由于这里我们的“gulimall-product”就部署了一台,所以看上去一切祥和,但是在如果部署了多台,问题就出现了,主要问题就集中在我们所使用的锁上。我们锁使用的是“synchronized ”,这是一种本地锁,它只是在一台设备上有效,无法实现分布式情况下,锁住其他设备的相同操作。
分布式锁
阶段一
删除锁,获取值对比+对比成功删除=原子操作,如果出错就会出现死锁
public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisLock() {
//阶段一
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "111");
//获取到锁,执行业务
if (lock) {
//加锁成功。。。执行业务
//2、设置过期时间,必须和加锁是同步的,原子的
Map<String, List<Catelog2Vo>> dataFromDB = getDataFromDB();
redisTemplate.delete("lock");//删除锁
return dataFromDB;
}else {
//没获取到锁,等待100ms重试
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getCatalogJsonDBWithRedisLock();
}
}
下面使用redis来实现分布式锁,使用的是SET key value [EX seconds] [PX milliseconds] [NX|XX],
stringRedisTemplate.opsForValue(“lock”, “111”).setIfAbsent就是setNX操作,set之前会检查是否已经存在key"lock",已存在则返回null
阶段二
- 问题: 1、setnx占好了位,业务代码异常或者程序在页面过程中宕机。没有执行删除锁逻辑,这就造成了死锁
- 问题: 1、setnx设置好,正要去设置过期时间,宕机。又死锁了。 解决: 设置过期时间和占位必须是原子的。redis支持使用setnx ex命令
- 问题: 1、删除锁直接删除??? 如果由于业务时间很长,锁自己过期了,我们直接删除,有可能把别人正在持有的锁删除了。 解决: 占锁的时候,值指定为uuid,每个人匹配是自己的锁才删除。
- 问题: 1、如果正好判断是当前值,正要删除锁的时候,锁已经过期,别人已经设置到了新的值。那么我们删除的是别人的锁 解决: 删除锁必须保证原子性。使用redis+Lua脚本完成
- 防止解错锁:加锁添加uuid
- 防止程序出错导致死锁:添加过期时间
- 解锁保证原子性:lua脚本解锁:
if redis call('get',KEYS[1]) == ARGV[1] then return redis call('del',KEYS[1]) else return 0 end"
保证加锁【占位+过期时间】和删除锁【判断+删除】的原子性。更难的事情,锁的自动续期
/**
* 分布式锁
*
* @return
*/
public Map<String, List<Catelog2Vo>> getCatelogJsonFromDbWithRedisLock() {
//1、占用分布式锁,去redis占坑;setNx
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
if (lock){
//占座成功
Map<String, List<Catelog2Vo>> dataFromDb;
try{
dataFromDb = getDataFromDb();
}finally {
//删除锁,获取值对比+对比成功删除=原子操作 lua脚本解锁
// String lockValue = redisTemplate.opsForValue().get("lock");
// if (uuid.equals(lockValue))
// redisTemplate.delete("lock");
String script = "if redis call('get',KEYS[1]) == ARGV[1] then return redis call('del',KEYS[1]) else return 0 end";
Long lock1 = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class)
, Arrays.asList("lock"), uuid);
}
return dataFromDb;
}else {
//占锁失败,重试,自旋
try {
Thread.sleep(200);
}catch (Exception e){
}
return getCatelogJsonFromDbWithRedisLock();
}
}
官网说法:
6、Redisson
Redison使用手册:https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95