一、Springboot整合Redis
1.引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2.在yaml配置基本信息
spring:
redis:
port: 6379
host: 192.168.220.131
3.调用StringRedisTemplate来操作
简单实例1
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
public void test3(){
stringRedisTemplate.opsForValue().set("username","Alan");
String alan = stringRedisTemplate.opsForValue().get("username");
System.out.println(alan);
}
简单实例2
@Autowired
private RedisTemplate redisTemplate;
@Test
public void test2(){
AttrEntity attr = new AttrEntity();
attr.setAttrId(8L);
redisTemplate.opsForValue().set("attrEntity",attr);
AttrEntity attrEntity = (AttrEntity) redisTemplate.opsForValue().get("attrEntity");
System.out.println(attrEntity.getAttrId());
}
4.产生对外内存溢出
- springboot2.0以后默认lettuce作为操作redis的客户端,它使用netty进行网罗通信
- lettuce的bug导致netty堆外内存溢出, -Xmx300m;netty如果没有指定堆外内存,默认使用-Xmx300m
解决方案
- 升级lettuce客户端
- 切换使用jedis
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettue</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
二.高并发下缓存失效问题
缓存穿透
查询一个不存在的数据,由于缓存是不命中,将去查询数据库,但数据库无此记录,我们没有将这次查询的null写入缓存,这将导致这个不不存在数据每次请求都要到存储层查询,失去了缓存的意义
- 风险 利用不存在的数据进行攻击,数据库瞬时压力增大,最终导致崩溃
- 解决 null结果缓存,并加入短暂过期时间
缓存雪崩
我们在设置缓存时key采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重血崩
- 解决 原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存过期时间的重复率就会降低,就很难引发集体失效事件。
缓存击穿
对于一些设置了过期时间的key,如果这些key可能会在某一时间点被超高并发的访问,是一种非常“热点”的数据。如果这个key在大量请求同时进来前正好失效,那么所有,对这个key的数据查询都落在db,我们成为缓存击穿
- 解决 加锁,大量并发只让一个人去查,其他人等待,查到以后释放所,其他人获得锁,先查缓存,就会有数据,不用去db
1.加锁解决缓存击穿问题
-
本地锁
代码示例
public Map<String, List<Catalog2VO>> getCatalogJsonFromDb() { synchronized (this){ //TODO 得到锁后应该再去缓存确认一次 String catalogJSON = redisTemplate.opsForValue().get("catalogJson"); if(!StringUtils.isEmpty(catalogJSON)){ Map<String, List<Catalog2VO>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2VO>>>(){}); System.out.println("查询缓存" + "***********"); return result; }
System.out.println("查询数据库" + "**************");
//查询三级分类,并封装三级分类为map
List<CategoryEntity> category3Entities = this.list(new QueryWrapper<CategoryEntity>().eq("cat_level", 3));
Map<Long, List<Catalog2VO.Catalog3VO>> cat3Map = new HashMap<>();
for (CategoryEntity category3Entity : category3Entities) {
Catalog2VO.Catalog3VO catalog3VO = new Catalog2VO.Catalog3VO();
catalog3VO.setCatelog2Id(category3Entity.getParentCid().toString());
catalog3VO.setId(category3Entity.getCatId().toString());
catalog3VO.setName(category3Entity.getName());
List<Catalog2VO.Catalog3VO> entites = cat3Map.get(category3Entity.getParentCid());
if(CollectionUtils.isEmpty(entites)){
List<Catalog2VO.Catalog3VO> list = new ArrayList<>();
list.add(catalog3VO);
cat3Map.put(category3Entity.getParentCid(), list);
}else {
entites.add(catalog3VO);
}
}
//查询二级分类
List<CategoryEntity> category2Entities = this.list(new QueryWrapper<CategoryEntity>().eq("cat_level", 2));
//封装cat2Map
Map<String, List<Catalog2VO>> cat2Map = new HashMap<>();
for (CategoryEntity category2Entity : category2Entities) {
Catalog2VO catalog2VO = new Catalog2VO();
catalog2VO.setCatelog1Id(category2Entity.getParentCid().toString());
catalog2VO.setId(category2Entity.getCatId().toString());
catalog2VO.setName(category2Entity.getName());
List<Catalog2VO.Catalog3VO> catalog3VOList = cat3Map.get(category2Entity.getCatId());
if(!CollectionUtils.isEmpty(catalog3VOList))
catalog2VO.setCatalog3List(catalog3VOList);
List<Catalog2VO> entites = cat2Map.get(category2Entity.getParentCid().toString());
if(CollectionUtils.isEmpty(entites)){
List<Catalog2VO> list = new ArrayList<>();
list.add(catalog2VO);
cat2Map.put(category2Entity.getParentCid().toString(), list);
}else{
entites.add(catalog2VO);
}
}
String jsonString = JSON.toJSONString(cat2Map);
redisTemplate.opsForValue().set("catalogJson",jsonString);
return cat2Map;
}
}
缺点:在分布式中同步锁不共享,每个微服务都会在开始时查询一次数据库
2.分布式锁
通过redis实现
简单示例
public Map<String, List<Catalog2VO>> getCatalogJsonFromDbWithReidsLock() {
//1、占分布式锁 去redis占坑
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111");
if(lock){
//加锁成功。。。 执行业务
Map<String, List<Catalog2VO>> catalogJsonFromDb = this.getCatalogJsonFromDb();
redisTemplate.delete("lock");
return catalogJsonFromDb;
}else{
//加锁失败重试
return getCatalogJsonFromDbWithReidsLock();//自旋的方式
}
}
缺点: setnx占好了坑位,业务代码异常或者程序在页面过程中宕机。没有执行删除锁逻辑,这就造成了死锁
解决: 设置锁的自动过期,即使没有删除,会自动删除
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111", 300, TimeUnit.SECONDS);
但这还是不行
缺点: 由于业务时间很长,锁自己已经过期了,我们直接删除,有可能把别人正在持有的锁删除
解决: 占锁的时候,值指定为uuid,每个人匹配是自己的锁才删除
public Map<String, List<Catalog2VO>> getCatalogJsonFromDbWithReidsLock() {
//1、占分布式锁 去redis占坑
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
if(lock){
//加锁成功。。。 执行业务
Map<String, List<Catalog2VO>> catalogJsonFromDb = this.getCatalogJsonFromDb();
String lockValue = redisTemplate.opsForValue().get("lock");
if(uuid.equals(lockValue)){
redisTemplate.delete("lock");
}
return catalogJsonFromDb;
}else{
//加锁失败重试
return getCatalogJsonFromDbWithReidsLock();//自旋的方式
}
}
这还有问题
如果正好判断是当前值,正当要删除锁的时候,锁已经过期,别人已经设置到新的值、那我们删除的是别人的锁
解决:删除锁必须保证原子性。使用redis+Lua脚本完成
执行脚本
if redis.call("get",KEYS[1]) == ARGV[1]
then
return redis.call("del",KEYS[1])
else
return 0
end
保证加锁[占位+过期时间]和删除锁[判断+删除]的原子性。更难的事情,锁的自动续期。
代码如下
public Map<String, List<Catalog2VO>> getCatalogJsonFromDbWithReidsLock() {
//1、占分布式锁 去redis占坑
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
if(lock){
//加锁成功。。。 执行业务
Map<String, List<Catalog2VO>> catalogJsonFromDb;
try{
catalogJsonFromDb = this.getCatalogJsonFromDb();
}finally {
//lua脚本
String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
"then\n" +
" return redis.call(\"del\",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";
//删除锁, Integer为方法的返回值
Long execute = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
}
return catalogJsonFromDb;
}else{
System.out.println("获取分布式失败。。。等待重试");
try{
Thread.sleep(200);
}catch (Exception e){
}
//加锁失败重试
return getCatalogJsonFromDbWithReidsLock();//自旋的方式
}
}
三、Redisson
1.java整合redisson
1.导入依赖
<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.4</version>
</dependency>
2.配置
@Configuration
public class MyRedissonConfig {
@Bean(destroyMethod="shutdown")
RedissonClient redisson() throws IOException {
//1、创建配置
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.220.131:6379");
//2、根据Config床建出RedissonClient实例
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
3.简单实例
@Autowired
private RedissonClient redisson;
@ResponseBody
@GetMapping("/hello")
public String hello(){
//1.获取一把锁,只要锁的名字一样,就是同一把锁
RLock lock = redisson.getLock("my-lock");
//2.加锁
lock.lock();//阻塞式等待,默认是30s,超过自动续期
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(10,TimeUnit.SECONDS);//不会自动续期
2.读写锁
代码示例
@Autowired
private StringRedisTemplate redisTemplate;
//保证一定能读到最新数据,修改期间,写锁是一个排他锁(互斥锁)。读锁是一个共享锁
//写锁没释放读就必须等待
@GetMapping("/write")
@ResponseBody
public String writeValue(){
RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
String s ="";
RLock rLock = lock.writeLock();
rLock.lock();
try{
s = UUID.randomUUID().toString();
Thread.sleep(30000);
redisTemplate.opsForValue().set("writeValue",s);
}catch (Exception e){
e.printStackTrace();
} finally {
rLock.unlock();
}
return s;
}
@GetMapping("/read")
@ResponseBody
public String readValue(){
RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
String s = "";
RLock rLock = lock.readLock();
rLock.lock();
try{
s = redisTemplate.opsForValue().get("writeValue");
}catch (Exception e){
e.printStackTrace();
} finally {
rLock.unlock();
}
return s;
}
读 --> 读: 相当于无锁,并发读,只会在redis中记录好,所有当前的读锁。他们都会同时加载成功
写 --> 读: 等待写锁释放
写 --> 写: 阻塞方式
读 --> 写: 有读锁。写也需要等待
3.信号量
//可看作停车问题,可作分布式限流
//此为停车方法
@GetMapping("/park")
@ResponseBody
public String park() throws InterruptedException{
RSemaphore park = redisson.getSemaphore("park");
//查看是否有空余的信号(车位)
boolean b = park.tryAcquire();
if(b){
//获取一个信号()
park.acquire();
}else {
return "error";
}
return "ok";
}
//此为离开方法
@GetMapping("/go")
@ResponseBody
public String go() throws InterruptedException {
RSemaphore park = redisson.getSemaphore("park");
park.release();
return "ok";
}
4.闭锁测试
@GetMapping("/lockDoor")
@ResponseBody
public String lockDoor() throws InterruptedException{
RCountDownLatch door = redisson.getCountDownLatch("door");
door.trySetCount(5);
door.await();
return "放假了。。。。";
}
@GetMapping("/gogogo/{id}")
@ResponseBody
public String gogogo(@PathVariable("id") Long id){
RCountDownLatch door = redisson.getCountDownLatch("door");
door.countDown();//计数减一
return id + "班的人都走了";
}
5.缓存一致性(数据更新时)
双写模式
双写模式是指修改数据后写到数据库,接着写到缓存

由于卡顿等原因,导致写缓存2在最前,写缓存1在后面就出现数据不一致的问题
脏数据问题:
这是暂时性脏数据的问题,但是在数据稳定,缓存期以后,又能得到新的正确数据
失效模式
失效模式值数据更新写入数据库,删除缓存数据,下次查时再存入缓存;

解决方案
- 无论是双写模式,都会存在缓存不一致问题,即多个实例同时更新会出事
- 如果用户纬度数据(订单数据、用户数据),这种并发几率非常小,不用考虑这种问题,缓存数据加上过期时间,每隔一段时间主动触发读的主动更新即可
- 如果是菜单,商品介绍等基础数据,也可以去使用canal订阅binlog的方式
- 缓存数据+过期时间也足够解决大部分对于缓存的要求
- 通过加锁保证并发读写,写写的时候按照顺序排队。读读无所谓、所以适合用读写锁。(业务不关心脏数据,允许脏数据可以忽略)
- 总结
- 我们能放入缓存的数据本就不应该是实时性、一致性要求超高的。所以缓存数据的时候加上过期时间,保证每天拿到当前最新数据即可;
- 我们不应该过度设计,增加系统的复杂性;
- 遇到实时性、一致性要求高的数据,就应该查数据库,即使慢点。
我们系统一致性解决方案:
1.缓存的所有数据都有过期时间,数据过期下一次查询触发主动更新
2.读写数据的时候,加上分布式的读写锁 (偶尔写经常读)