rebis的使用场景
1. 作为缓存
- 缓存就是数据交换的缓冲区(称作:Cache),当某一硬件要读取数据时,会首先从缓存汇总查询数据,有则直接执行,不存在时从内存中获取。由于缓存的数据比内存快的多,所以缓存的作用就是帮助硬件更快的运行。
- 缓存往往使用的是RAM(断电既掉的非永久存储),所以在用完后还是会把文件送到硬盘等存储器中永久存储。电脑中最大缓存就是内存条,硬盘上也有16M或者32M的缓存。
- 高速缓存是用来协调CPU与主存之间存取速度的差异而设置的。一般CPU工作速度高,但内存的工作速度相对较低,为了解决这个问题,通常使用高速缓存,高速缓存的存取速度介于CPU与主存之间。系统将一些CPU在最近几个时间段经常访问的内容存在高速缓存,这样就在一定程度上缓解了由于主存速度低造成的CPU“停工待料”的情况。
- 缓存就是把一些外存上的数据保存在内存上而已,为什么保存在内存上,我们运行的所有程序里面的变量都是存放在内存中的,所以如果想将值放入内存上,可以通过变量的方式存储。在JAVA中一些缓存一般都是通过Map集合来实现的。
- 缓存在不同的场景下,作用是不一样的具体举例说明:
- 操作系统磁盘缓存 ——> 减少磁盘机械操作。
- 数据库缓存——>减少文件系统IO。
- 应用程序缓存——>减少对数据库的查询。
- Web服务器缓存——>减少应用服务器请求。
- 客户端浏览器缓存——>减少对网站的访问。
- 缓存在不同的场景下,作用是不一样的具体举例说明:
1.1 为什么使用缓存
(1)提高性能:缓存查询速度必数据库查询速度快(内存vs硬盘)
(2)提高并发能力:缓存分担了部分请求,支持更高的并发
1.2 什么样的数据适合放入到缓存中
热点数据
修改频率比较低
安全系数低的
缓存的原理:
怎么使用缓存:
(1)搭建一个springboot+mp的工程
(2)引入redis相关的依赖
(3)配置redis
(4)service代码
2. redis作为分布式锁
2.1代码实现
表示:
- 先从Redis中读取stock的值,表示商品的库存
- 判断商品库存是否大于0,如果大于0,则库存减1,然后再保存到Redis缓存中里面去,否则就提示“库存不足”
package com.hong.service.impl;
import com.hong.dao.StockDao;
import com.hong.entity.Stock;
import com.hong.service.StockService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* @Author hongCheng
* @Date 2021/4/16 15:39
* @Version 1.0
*/
@Service
public class StockServiceImpl implements StockService {
@Resource
private StockDao stockDao;
@Override
public String findById(Integer productId) {
//根据id查询stock信息
Stock stock = stockDao.selectById(productId);
if (stock.getNum() > 0) {
//修改数量,数量-1
stock.setNum(stock.getNum() - 1);
//根据获取到的stock对象,再根据id修改数量
stockDao.updateById(stock);
System.out.println("库存剩余数量:" + stock.getNum());
return "库存减少成功";
}
return "库存不足";
}
}
可见用压力测试软件在多个线程同时访问,测试结果是有重复的
2.2 改进①
2.1中从Redis读取、判断值再减1保存到Redis的操作,很容易在并发场景下出问题,可以使用加并发锁synchronized
来解决:
package com.hong.service.impl;
import com.hong.dao.StockDao;
import com.hong.entity.Stock;
import com.hong.service.StockService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* @Author hongCheng
* @Date 2021/4/16 15:39
* @Version 1.0
*/
@Service
public class StockServiceImpl implements StockService {
@Resource
private StockDao stockDao;
@Override
public String findById(Integer productId) {//加入synchronized同步锁
synchronized (this) {
//根据产品id查询信息
Stock stock = stockDao.selectById(productId);
if (stock.getNum() > 0) {
//根据id修改库存
stock.setNum(stock.getNum() - 1);
stockDao.updateById(stock);
System.out.println("库存剩余数量:" + stock.getNum());
return "库存减少成功";
}
}
return "库存不足";
}
}
可以看出测试结果没有重复的了
注意:在Java中关键字synchronized可以保证在同一时刻,只有一个线程可以执行某个方法或某个代码块
2.3 改进②
2.2的代码在单体模式下并没太大问题,但是在分布式或集群架构环境下存在问题
在分布式或集群架构下,synchronized只能保证当前的主机在同一时刻只能有一个线程执行减库存操作,但如图同时有多个请求过来访问的时候,不同主机在同一时刻依然是可以访问减库存接口的,这就导致问题在集群架构下依然存在。
注意:可以使用JMeter来模拟出高并发场景下访问Nginx来测试触发上面的问题
package com.hong.service.impl;
import com.hong.dao.StockDao;
import com.hong.entity.Stock;
import com.hong.service.StockService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* @Author hongCheng
* @Date 2021/4/16 15:39
* @Version 1.0
*/
@Service
public class StockServiceImpl implements StockService {
@Resource
private StockDao stockDao;
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public String findById(Integer productId) {//加入synchronized同步锁 同步代码块
//如果redis中存在该key则返回false,不存在返回true---》键库存--》del然后删除key
Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent("product::" + productId, "dongdong");
if (aBoolean) {//获取锁了
//根据产品id查询信息
Stock stock = stockDao.selectById(productId);
if (stock.getNum() > 0) {
//根据id修改库存
stock.setNum(stock.getNum() - 1);
stockDao.updateById(stock);
System.out.println("库存剩余数量:" + stock.getNum());
redisTemplate.delete("product::" + productId);//释放锁资源
return "库存减少成功";
} else {
redisTemplate.delete("product::" + productId);//释放所资源
return "库存不足";
}
} else {
System.out.println("服务器正忙,请稍后。。。。。。");
return "服务器正忙,请稍后。。。。。。";
}
}
}
2.4 使用第三方组件redission-----专门用于解决分布式问题
- 引入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.4</version>
</dependency>
- 引入Bean
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.31.126:6379").setPassword("654321");
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
- 代码实现
package com.hong.service.impl;
import com.hong.dao.StockDao;
import com.hong.entity.Stock;
import com.hong.service.StockService;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
/**
* @Author hongCheng
* @Date 2021/4/16 15:39
* @Version 1.0
*/
@Service
public class StockServiceImpl01 implements StockService {
@Resource
private StockDao stockDao;
@Autowired
private RedissonClient redisson;
@Override
public String findById(Integer productId) {//加入synchronized同步锁 同步代码块
RLock lock = redisson.getLock("product::" + productId);//获取锁对象
try {
lock.tryLock(60,20, TimeUnit.SECONDS); //自己别设置时间。
Stock stock = stockDao.selectById(productId);
if (stock.getNum() > 0) {
//根据id修改库存
stock.setNum(stock.getNum() - 1);
stockDao.updateById(stock); //异常发生
// int c=10/0;
// Thread.sleep(35000);
System.out.println("库存剩余:" + (stock.getNum()));
return "库存减少成功";
} else {
return "库存不足";
}
}catch (Exception e){
throw new RuntimeException(e.getMessage());
}
finally {
lock.unlock();
}
}
}
3 作为点赞量videaId,0 incr(videaId),排行榜,转发量。
什么是计数器,如电商网站商品的浏览量、视频网站视频的播放数等。为了保证数据实时效,每次浏览都得给+1,并发量高时如果每次都请求数据库操作无疑是种挑战和压力。Redis提供的incr命令来实现计数器功能,内存操作,性能非常好,非常适用于这些计数场景 。
关系型数据库在排行榜方面查询速度普遍偏慢,所以可以借助redis的SortedSet进行热点数据的排序。 在奶茶活动中,我们需要展示各个部门的点赞排行榜, 所以我针对每个部门做了一个SortedSet,然后以用户的openid作为上面的username,以用户的点赞数作为上面的score, 然后针对每个用户做一个hash, 通过zrangebyscore就可以按照点赞数获取排行榜,然后再根据username获取用户的hash信息,这个当时在实际运用中性能体验也蛮不错的。
4 限时业务的运用
redis中可以使用expire命令设置一个键的生存时间,到时间后redis会删除它。利用这一特性可以运用在限时的优惠活动信息、手机验证码等业务场景。