Redis学习笔记
一 .Redis简单介绍:
1.Redis是什么?
Redis是一个用C语言编写,支持网络,持久化,数据以键值对Key-Value的形式存储的开源数据库。
为了满足不同的业务场景,Redis 内置了多种数据类型实现比如
5 种基础数据结构 :String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)。
3 种特殊数据结构 :HyperLogLogs(基数统计)、Bitmap (位存储)、Geospatial (地理位置)。
Redis 速度快较重要的主要有下面 3 点:
- Redis 基于内存,内存的访问速度是磁盘的上千倍;
- Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型,主要是单线程事件循环和 IO 多路复用
- Redis 内置了多种优化过后的数据结构实现,性能非常高。
2.Redis可以做什么?
(1)缓存,毫无疑问这是Redis当今最为人熟知的使用场景。再提升服务器性能方面非常有效;
(2)排行榜,如果使用传统的关系型数据库来做这个事儿,非常的麻烦,而利用Redis的SortSet数据结构能够非常方便搞定;
(3)计算器/限速器,利用Redis中原子性的自增操作,我们可以统计类似用户点赞数、用户访问数等,这类操作如果用MySQL,频繁的读写会带来相当大的压力;限速器比较典型的使用场景是限制某个用户访问某个API的频率,常用的有抢购时,防止用户疯狂点击带来不必要的压力;
(4)好友关系,利用集合的一些命令,比如求交集、并集、差集等。可以方便搞定一些共同好友、共同爱好之类的功能;
(5 )简单消息队列,除了Redis自身的发布/订阅模式,我们也可以利用List来实现一个队列机制,比如:到货通知、邮件发送之类的需求,不需要高可靠,但是会带来非常大的DB压力,完全可以用List来完成异步解耦;
(6)Session共享,默认Session是保存在服务器的文件中,即当前服务器,如果是集群服务,同一个用户过来可能落在不同机器上,这就会导致用户频繁登陆;采用Redis保存Session后,无论用户落在那台机器上都能够获取到对应的Session信息。
(7)使用Redis中Bitmap数据结构可以做用户签到功能,32位对应一个月30或31天(1表示签到成功,0失败)
(8) 使用Redis中Geospatial 数据结构可以做用户地理位置相关操作例如附近商店,附近的人等
(9) 使用Redis中HyperLogLogs数据结构可以做统计相关才做,比如今日网页用户活跃用户量等
二.在Java开发中使用Redis
1.在pom文件中导入相关坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.1.7.RELEASE</version>
</dependency>
<!--redisson-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
2.配置连接的application.yml文件
redis:
host: xxx.xxx.xxx.xxx
port: 639
3.Redisson配置类
@Configurationpublic
class RedissonConfig {
@Bean
public RedissonClient redissonClient(){
// 配置
Config config = new Config();
config.useSingleServer().setAddress("redis://xxx.xxx.xxx.xxx:6379"); //开启redis的主机ip:端口号
// 创建RedissonClient对象
return Redisson.create(config);
}
4.测试
@SpringBootTest
public class testRedis {
@Resource
private RedisTemplate redisTemplate; //k- v 都是对象
@Resource
private StringRedisTemplate stringRedisTemplate; //操作 k-v 字符串
@Test
void test(){
redisTemplate.opsForValue();//操作字符串
redisTemplate.opsForHash();//操作hash
redisTemplate.opsForList();//操作list
redisTemplate.opsForSet();//操作set
redisTemplate.opsForZSet();//操作有序set
}
}
三.Redis个别业务场景代码实现
1.基于Redis生成Token实现用户登录功能
登录接口实现代码(之前需要使用验证码发送接口将用户验证码缓存到Redis中)
@Override
public Result login(LoginDTO loginDto, HttpSession session) {
// 1.校验手机号
String phone = loginDto.getPhone();
//正则表达式方法校验手机号码格式
if (RegexUtils.isPhoneInvalid(phone)) {
// 2.如果不符合,返回错误信息
return Result.fail("手机号格式错误!");
}
// 3.从redis获取验证码并校验 此前在发送验证码的时候将("login_code_key:" + phone)作为key放入redis中
String cacheCode = stringRedisTemplate.opsForValue().get("login_code_key:" + phone);
String code = loginDto.getCode();
if (cacheCode == null || !cacheCode.equals(code)) {
// 不一致,报错
return Result.fail("验证码错误");
}
// 4.一致,根据手机号查询用户 select * from tb_user where phone = ?
User user = query().eq("phone", phone).one();
// 5.判断用户是否存在
if (user == null) {
// 6.不存在,创建新用户并保存
user = createUserWithPhone(phone);
}
// 7.保存用户信息到 redis中
// 7.1.随机生成token,作为登录令牌
String token = UUID.randomUUID().toString(true);
// 7.2.将User对象转为HashMap存储
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
CopyOptions.create()
.setIgnoreNullValue(true)
.setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
// 7.3.存储
String tokenKey = "login_code_key:" + token;
stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
// 7.4.设置token有效期 //自定义常量过期时间,时间单位
stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
// 8.返回token
return Result.ok(token);
}
配置拦截器在用户每次访问其他页面时候刷新Token
public class RefreshTokenInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1.获取请求头中的token
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)) {
//如果为空说明用户还没有登录,返回true进入登录页面
return true;
}
// 2.基于TOKEN获取redis中的用户
String key = "login_code_key:" + token;
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
// 3.判断用户是否存在
if (userMap.isEmpty()) {
return true;
}
// 4.将查询到的hash数据转为UserDTO
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
// 5.存在,保存用户信息到 ThreadLocal
UserHolder.saveUser(userDTO);
// 6.刷新token有效期
stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
// 7.放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 移除用户
UserHolder.removeUser();
}
}
配置拦截器
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 登录拦截器
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login"
).order(1); // 后面的order是拦截顺序,数字越小优先级越高
// token刷新的拦截器
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate))
.addPathPatterns("/**").order(0);
}
}
2.Redis缓存存在的常见问题
缓存雪崩:
大量缓存数据同时间失效,导致用户直接发起大量请求到数据库,产生瓶颈。
1、生成随机失效的缓存时间数据;
2、让缓存节点分布在不同的物理节点上;
3、生成不失效的缓存数据;
4、定时任务更新缓存数据;
解决方案:
- 给不同的Key的TTL添加随机值
- 利用Redis集群提高服务的可用性
- 给缓存业务添加降级限流策略
- 给业务添加多级缓存
缓存穿透:
用户请求数据,例如ID为负数,不存在缓存里,也不存在数据库里,会造成缓存穿透。
1、无意义数据放入缓存,下一次相同请求就会命中缓存;
2、IP过滤;
3、参数校验;
4、布隆过滤器;
解决方案:
核心思路如下:
在原来的逻辑中,我们如果发现这个数据在mysql中不存在,直接就返回404了,这样是会存在缓存穿透问题的。
现在的逻辑中:如果这个数据不存在,我们不会返回404 ,还是会把这个数据写入到Redis中,并且将value设置为空,欧当再次发起查询时,我们如果发现命中之后,判断这个value是否是null,如果是null,则是之前写入的数据,证明是缓存穿透数据,如果不是,则直接返回数据。
实现代码:
// 解决缓存穿透
//大量用户请求数据,例如ID为负数,不存在缓存里,也不存在数据库里,造成数据库压力过大导致缓存穿透。
//调用接口
public Result queryById(Long id) {
Shop shop = cacheClient.queryWithPassThrough(
CACHE_SHOP_KEY,
id,
Shop.class,
this::getById,
CACHE_SHOP_TTL,
TimeUnit.MINUTES);
if (shop == null) {
return Result.fail("店铺不存在!");
}
// 7.返回
return Result.ok(shop);
}
//封装后的方法
public <R,ID> R queryWithPassThrough(
String keyPrefix, //缓冲Redis中的key前缀可以不传
ID id, //id查询对象id
Class<R> type, //对象被封装的实体类
Function<ID, R> dbFallback, //流调函数 用来做数据库操作
Long time, //缓存过期时间
TimeUnit unit //时间单位
){
String key = keyPrefix + id;
// 1.从redis查询商铺缓存
String json = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isNotBlank(json)) {
// 3.存在,直接返回
return JSONUtil.toBean(json, type); //json转成实体类
}
// 判断命中的是否是空值
if (json != null) {
// 返回一个错误信息
return null;
}
// 4.不存在,根据id查询数据库
R r = dbFallback.apply(id);
// 5.不存在,返回错误
if (r == null) {
// 将空值写入redis
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
// 返回错误信息
return null;
}
// 6.存在,写入redis
this.set(key, r, time, unit);
return r;
}
缓存击穿:
由于缓存热点键到时失效导致用户请求直接访问数据库
1、用久缓存;
2、分布式锁
a.单体应用—>互斥锁—>zookeeper ,redis实现。
解决方案一、使用锁来解决:
因为锁能实现互斥性。假设线程过来,只能一个人一个人的来访问数据库,从而避免对于数据库访问压力过大,但这也会影响查询的性能,因为此时会让查询的性能从并行变成了串行,我们可以采用tryLock方法 + double check来解决这样的问题。
假设现在线程1过来访问,他查询缓存没有命中,但是此时他获得到了锁的资源,那么线程1就会一个人去执行逻辑,假设现在线程2过来,线程2在执行过程中,并没有获得到锁,那么线程2就可以进行到休眠,直到线程1把锁释放后,线程2获得到锁,然后再来执行逻辑,此时就能够从缓存中拿到数据了。
代码实现:
// 互斥锁解决缓存击穿
//调用接口
public Result queryWithMutex(Long id) {
Shop shop = cacheClient.queryWithPassThrough(
CACHE_SHOP_KEY,
id,
Shop.class,
this::getById,
CACHE_SHOP_TTL,
TimeUnit.MINUTES);
if (shop == null) {
return Result.fail("店铺不存在!");
}
// 7.返回
return Result.ok(shop);
}
//实现方法
public <R, ID> R queryWithMutex(
String keyPrefix,
ID id,
Class<R> type,
Function<ID, R> dbFallback,
Long time,
TimeUnit unit
) {
String key = keyPrefix + id;
// 1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
// 3.存在,直接返回
return JSONUtil.toBean(shopJson, type);
}
// 判断命中的是否是空值
if (shopJson != null) {
// 返回一个错误信息
return null;
}
// 4.实现缓存重建
// 4.1.获取互斥锁
String lockKey = LOCK_SHOP_KEY + id;
R r = null;
try {
boolean isLock = tryLock(lockKey);
// 4.2.判断是否获取成功
if (!isLock) {
// 4.3.获取锁失败,休眠并重试
Thread.sleep(50);
//重新实行该方法直到成功
return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);
}
// 4.4.获取锁成功,根据id查询数据库
r = dbFallback.apply(id);
// 5.不存在,返回错误
if (r == null) {
// 将空值写入redis
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
// 返回错误信息
return null;
}
// 6.存在,写入redis set方法
this.set(key, r, time, unit);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
// 7.释放锁
unlock(lockKey);
}
// 8.返回
return r;
}
//上锁方法
private boolean tryLock(String key) { //setIfAbsent实现上锁功能,如果key存在则上锁失败
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
//解锁方法
private void unlock(String key) {
stringRedisTemplate.delete(key);
}
//set方法
public void set(String key, Object value, Long time, TimeUnit unit) {
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
}
解决方案二、使用逻辑过期来解决:
方案分析:我们之所以会出现这个缓存击穿问题,主要原因是在于我们对key设置了过期时间,假设我们不设置过期时间,其实就不会有缓存击穿的问题,但是不设置过期时间,这样数据不就一直占用我们内存了吗,我们可以采用逻辑过期方案。
我们把过期时间设置在 redis的value中,注意:这个过期时间并不会直接作用于redis,而是我们后续通过逻辑去处理。假设线程1去查询缓存,然后从value中判断出来当前的数据已经过期了,此时线程1去获得互斥锁,那么其他线程会进行阻塞,获得了锁的线程他会开启一个 线程去进行 以前的重构数据的逻辑,直到新开的线程完成这个逻辑后,才释放锁, 而线程1直接进行返回,假设现在线程3过来访问,由于线程线程2持有着锁,所以线程3无法获得锁,线程3也直接返回数据,只有等到新开的线程2把重建数据构建完后,其他线程才能走返回正确的数据。
这种方案巧妙在于,异步的构建缓存,缺点在于在构建完缓存之前,返回的都是脏数据。
//调用接口
public Result queryWithLogicalExpire(Long id) {
Shop shop = cacheClient.queryWithPassThrough(
CACHE_SHOP_KEY,
id,
Shop.class,
this::getById,
CACHE_SHOP_TTL,
TimeUnit.MINUTES);
if (shop == null) {
return Result.fail("店铺不存在!");
}
// 7.返回
return Result.ok(shop);
}
//实现方法
// 逻辑过期解决缓存击穿
public <R, ID> R queryWithLogicalExpire(
String keyPrefix,
ID id,
Class<R> type,
Function<ID, R> dbFallback,
Long time,
TimeUnit unit
) {
String key = keyPrefix + id;
// 1.从redis查询商铺缓存
String json = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (!StrUtil.isBlank(json)) {
// 3.存在,直接返回
return JSONUtil.toBean(json,type);
}
// 4.命中,需要先把json反序列化为对象
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
LocalDateTime expireTime = redisData.getExpireTime();
// 5.判断是否过期
if(expireTime.isAfter(LocalDateTime.now())) {
// 5.1.未过期,直接返回店铺信息
return r;
}
// 5.2.已过期,需要缓存重建
// 6.缓存重建
// 6.1.获取互斥锁
String lockKey = LOCK_SHOP_KEY + id;
boolean isLock = tryLock(lockKey);
// 6.2.判断是否获取锁成功
if (isLock){
// 6.3.成功,开启独立线程,实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
// 查询数据库
R newR = dbFallback.apply(id);
// 重建缓存
this.setWithLogicalExpire(key, newR, time, unit);
} catch (Exception e) {
throw new RuntimeException(e);
}finally {
// 释放锁
unlock(lockKey);
}
});
}
// 6.4.返回过期的商铺信息
return r;
}
//上锁方法同上
//逻辑过期set方法
public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {
// 设置逻辑过期
RedisData redisData = new RedisData();
redisData.setData(value);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
// 写入Redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
}
四.linux系统操作Redis相关笔记
Redis启动方式
cd /usr/local/redis/bin # 启动目录
./redis-server /usr/local/redis/bin/redis.conf # 启动命令
Redis客户端启动方式:
cd /usr/local/redis/bin #启动目录
./redis-cli #启动命令
Redis关闭
服务端:
1.查看redis进程
ps -ef | grep redis
2.杀死redis进程
kill -9 Redis_PID(redis的pid)
客户端
redis-cli shutdown
Redis开放端口号
(1)查看对外开放的端口状态
查询已开放的端口
netstat -anp
查询指定端口是否已开 提示 yes,表示开启;no表示未开启。
firewall-cmd --query-port=6379/tcp
(2)查看防火墙状态
#查看防火墙状态
systemctl status firewalld
#开启防火墙
systemctl start firewalld
#关闭防火墙
systemctl stop firewalld
#开启防火墙
service firewalld start
#若遇到无法开启先用:
systemctl unmask firewalld.service
#然后:
systemctl start firewalld.service
(3)对外开发端口
#查询对外开放的指定端口
firewall-cmd --query-port=6379/tcp
#添加指定需要开放的端口:
firewall-cmd --add-port=6379/tcp --permanent
#重载入添加的端口:
firewall-cmd --reload
#查询指定端口是否开启成功:
firewall-cmd --query-port=6379/tcp
#除指定端口:
firewall-cmd --permanent --remove-port=6379/tcp