为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访问。而db承担数据落盘工作。
哪些数据适合放入缓存?
springBoot可以直接使用redis,但是可能会有异常
@Override
public Map<String, List<Catalog2Vo>> getCatalogJson() {
String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");
if(StringUtils.isBlank("catalogJSON") || catalogJSON == null){
Map<String, List<Catalog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();
String s = JSON.toJSONString(catalogJsonFromDb);
stringRedisTemplate.opsForValue().set("catalogJSON",s,1,TimeUnit.SECONDS);
return catalogJsonFromDb;
}
Map<String, List<Catalog2Vo>> map = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>() {
});
return map;
}
堆外内存溢出:
原因:
springboot在2.0后默认使用lettuce操作redis客户端,它使用netty进行网络通信,lettuce的bug导致堆外内存溢出,底层实现是只要有操作,就会统计内存使用量,操作完会decrement减内存,可能是lettuce客户端在减内存的过程出错
解决:
对这个问题调节-Xmx大小不起作用,而且如果netty(netty如没有指定,默认是-Xmx300m)没指定堆外内存,默认使用-Xmx,所以一旦出现问题,就算调大-Xmx也总会到堆外内存溢出的地步。所以我们不能用io.netty.maxDirectMemory只修改堆外内存大小。
还有2种方案:
1、升级lettuce客户端
2、排除lettuce依赖,使用jedis
jedis是什么
Jedis是Redis的Java实现的客户端,其API提供了比较全面的Redis命令的支持;
Jedis实例不是线程安全的,所以不可以多个线程共用一个Jedis实例,但是创建太多的实现也不好因为这意味着会建立很多sokcet连接。
JedisPool是一个线程安全的网络连接池。可以用JedisPool创建一些可靠Jedis实例,可以从池中获取Jedis实例,使用完后再把Jedis实例还回JedisPool。这种方式可以避免创建大量socket连接并且会实现高效的性能.
依赖
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
<version>2.9.3version>
dependency>
RedisUtil
public class RedisUtil {
private JedisPool jedisPool;
public void initPool(String host,int port ,int database){
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(200);
poolConfig.setMaxIdle(30);
poolConfig.setBlockWhenExhausted(true);
poolConfig.setMaxWaitMillis(10*1000);
poolConfig.setTestOnBorrow(true);
jedisPool=new JedisPool(poolConfig,host,port,20*1000);
}
public Jedis getJedis(){
Jedis jedis = jedisPool.getResource();
return jedis;
}
}
RedisConfig
@Configuration
public class RedisConfig {
//读取配置文件中的redis的ip地址
@Value("${spring.redis.host:disabled}")
private String host;
@Value("${spring.redis.port:0}")
private int port;
@Value("${spring.redis.database:0}")
private int database;
@Bean
public RedisUtil getRedisUtil(){
if(host.equals("disabled")){
return null;
}
RedisUtil redisUtil=new RedisUtil();
redisUtil.initPool(host,port,database);
return redisUtil;
}
}
application.properties
spring.redis.host=redis服务地址
spring.redis.port=6379
spring.redis.database=0
使用测试
@Autowired
RedisUtil redisUtil;
@Test
public void testRedis() {
Jedis jedis = redisUtil.getJedis();
//jedis.set("test","test");
String s = jedis.get("test");
System.out.println(s);
}
业务使用
/**
* 查出所有分类 返回首页json
*/
@Override
public Map<String, List<Catalog2Vo>> getCatalogJson() {
//加入缓存逻辑
Jedis jedis = redisUtil.getJedis();
String catalogJson = jedis.get("catalogJson");
Map<String, List<Catalog2Vo>> json = null;
//缓存存在 转换返回
if (!StringUtils.isEmpty(catalogJson)) {
json = JSONObject.parseObject(catalogJson, new TypeReference<Map<String, List<Catalog2Vo>>>() {
});
return json;
}
//缓存没有从数据查询
json = getCatalogJsonFromDB();
//转成str 加入缓存
String jsonString = JSON.toJSONString(json);
jedis.set("catalogJson", jsonString);
return json;
}
/**
* 从数据库获取数据并封装返回
*
* @return
*/
public Map<String, List<Catalog2Vo>> getCatalogJsonFromDB() {
//查询出所有分类
List<CategoryEntity> selectList = baseMapper.selectList(null);
//先查出所有一级分类
List<CategoryEntity> level1Categorys = getCategorys(selectList, 0L);
//封装数据 map k,v 结构
Map<String, List<Catalog2Vo>> map = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
//每一个的一级分类,查到这个一级分类的二级分类
List<CategoryEntity> category2Entities = getCategorys(selectList, v.getCatId());
List<Catalog2Vo> catelog2Vos = null;
if (category2Entities != null) {
catelog2Vos = category2Entities.stream().map(level2 -> {
//封装catalog2Vo
Catalog2Vo catalog2Vo = new Catalog2Vo(v.getCatId().toString(), null, level2.getCatId().toString(), level2.getName());
//每一个二级分类,查到三级分类
List<CategoryEntity> category3Entities = getCategorys(selectList, level2.getCatId());
if (category3Entities != null) {
List<Object> catalog3List = category3Entities.stream().map(level3 -> {
//封装catalog3Vo
Catalog2Vo.Catalog3Vo catalog3Vo = new Catalog2Vo.Catalog3Vo(level2.getCatId().toString(), level3.getCatId().toString(), level3.getName());
return catalog3Vo;
}).collect(Collectors.toList());
//封装catalog3Vo到catalog2Vo
catalog2Vo.setCatalog3List(catalog3List);
}
return catalog2Vo;
}).collect(Collectors.toList());
}
//返回v=catalog2Vo
return catelog2Vos;
}));
return map;
}
测试ok
但是这样在高并发下缓存是会存在很多问题的,详情参见分布式-Redis-缓存问题
lua脚本:
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
就是加锁设置过期时间保证加锁操作是原子性的,解锁也是同样保持原子性。(原子性简单理解就是1和2两个操作都是要同时ok这整个任务才能算ok)
加锁获取数据
public Map<String, List<Catalog2Vo>> getCatalogJsonFromDBWithRedisLock() {
Jedis jedis = redisUtil.getJedis();
//加锁
String token = UUID.randomUUID().toString();
String lock = jedis.set("lock", token, "NX", "EX", 20);
System.out.println(lock);
Map<String, List<Catalog2Vo>> map = null;
//加锁成功
if ("ok".equals(lock)) {
map = getCatalogJsonFromDB();
//删除锁 lua脚本
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
jedis.eval(script, Collections.singletonList("lock"), Collections.singletonList(token));
return map;
} else {
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
//自旋
return getCatalogJsonFromDBWithRedisLock();
}
}
Redis总结