Redis命令
切记。打开五个服务
shutdown -r now :重启虚拟机
1.首先安装Redis的依赖:
yum install -y gcc tcl2.进入到 cd /usr/local/src 目录下,
下载redis镜像文件 解压包在 /usr/local/src/redis-6.2.6中解压镜像文件 tar -zxvf redis-6.2.6
进入 redis cd redis-6.2.63.编译并且安装 make && make install
使用make安装命令成功后安装默认的安装路径是在 /usr/local/bin
在 /usr/local/src/redis-6.2.6中有 redis.conf文件,可以先将此文件拷贝一份
cp redis.conf redis.conf.bck
解决防火墙未安装:
安装防火墙:yum install firewalld firewall-config
set key value ###可以对value进行覆盖
setnx key value ###只有在key不存在的时候才能设置成功, 不可以对value进行覆盖
incry k2 2 ### 数值型k2 +2
decry k2 2 #减去2
mset k1 v1 k2 v2
mget k1 k2
msetnx k1 v1 k2 v2 ##原子性操作任何一个失败都失败
getrange name 0 3 : ###获取name中下标从0到3的字符串
setrange name 3 abc: ###从下标为3开始替换成abc
setex <key> <时间秒> <value> ##设置key value的生存周期
ttl <key> ##得出生存周期的时间 -2过期,-1永久
getset age <value> ##返回新值,实际上已经使用value替换了旧value
Set集合
相当于value为空
sadd <key> <value1> <value2>...
Hash
Linux下基本文件命令
: w 保存
命令模式下:
wq!保存退出 强制
q! 强制退出
安装后重点文件说明:
/usr/local/redis-4.0.0/src/redis-server:Redis服务启动脚本
/usr/local/redis-4.0.0/src/redis-cli:Redis客户端脚本
/usr/local/redis-4.0.0/redis.conf:Redis配置文件
如果需要远程连接linux,则需要将bind:127.0.0.1注释掉,并且将保护模式关闭:pretoected mode no
进入redis.conf配置文件 在命令模式下输入斜杠 / 输入bind进行搜索 按n键可以搜索下一个。
#开启redis服务器并且制定配置文件 src/redis-server ./redis.conf
#开启服务器并且输入密码 src/redis-cli -h localhost -p 6379
不指定密码:
[root@localhost redis-4.0.0]# ./src/redis-cli
127.0.0.1:6379> keys *
(error) NOAUTH Authentication required.
127.0.0.1:6379> auth 123456
OK
127.0.0.1:6379>
[root@localhost redis-4.0.0]# vim redis.conf
[root@localhost redis-4.0.0]# ps -ef |grep redis
root 86172 1 0 17:32 ? 00:00:01 src/redis-server 127.0.0.1:6379
root 110471 109294 0 17:50 pts/0 00:00:00 grep --color=auto redis
[root@localhost redis-4.0.0]# kill -9 86172
[root@localhost redis-4.0.0]# src/redis-server ./redis.conf
110999:C 02 May 17:51:21.517 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
110999:C 02 May 17:51:21.517 # Redis version=4.0.0, bits=64, commit=00000000, modified=0, pid=110999, just started
110999:C 02 May 17:51:21.517 # Configuration loaded
[root@localhost redis-4.0.0]# src/redis-cli -h localhost -p 6379
localhost:6379> keys *
(error) NOAUTH Authentication required.
localhost:6379> auth 123456
OK
事务和锁的机制
- Redis 事务的主要作用急速串联多个命令,防止别的命令插队。
- 原理,使用multi进行组队:
- 将命令放入队列中等待执行。
- 当执行Exec的时候才会执行
- 如果执行discard那么整个队列的命令都不执行。
- 组队中如果谁有错误,那么所有的命令都不执行,如果执行中谁有错误谁就不执行。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KQOiNvrH-1652805727556)(E:/TYpora%E7%AC%94%E8%AE%B0/image/image-20220514140626007.png)]
multi 进行批处理, 使用队列形式进行处理,组队中有失败的命令也有成功的命令。
exec:将组队的命令全部执行。
discard :将multi批处理的数据全部回滚删除。
使用multi进行命令组队的时候如果任何一个命令出错了,那么其余命令都会回滚,不会执行。
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set b1 v1
QUEUED
127.0.0.1:6379> set b2 v2
QUEUED
127.0.0.1:6379> set b3
(error) ERR wrong number of arguments for ‘set’ command
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> keys *
(empty list or set)
Redis超卖和超时问题
- 连接超时问题可以使用连接池进行解决
- 超卖(<0) 可以使用事务(乐观锁)进行解决 multi
String kcKey = "sk:"+prodid + ":qt"; //库存id
String userKey = "sk:" +prodid + ":user"; //秒杀成功用户
//监控库存
jedis.watch(kcKey); //监控库存可以使得多个multi只有一个exec能够根据版本执行 乐观锁
//获取库存
String kc = jedis.get(kcKey);
if(kc == null){
System.out.printfln("秒杀还没有开始!");
jedis.close();
return false;
}
//秒杀事务
//使用事务控制
Transaction multi = jedis.multi();
//组队操作
multi.decy(kcKey); //将库存减一
multi.sadd(userKey,uid); //将秒杀用户存入Set中
//执行
List <Object> results = multi.exec();
if(results == null ||results.size() ==0){
System.out.println("秒杀失败了!");
jedis.close();
return false;
}
Redis乐观锁出现库存遗留问题
场景:
当一千个人秒杀商品的时候,其中一个人秒杀成功,将乐观锁版本号进行更改,那么其余的人便不能继续做操作,使得出现库存遗留问题。
解决方案
使用LUA脚本。
持久化-RDB(RedisDataBase)
原理
通过一个Fork的子进程建立一个临时RDB文件,来替换持久化的文件 (写时复制技术)
可以设置多少秒内有多少个key进行改变就能替换持久化文件:比如20s内至少替换三个。
缺点:
最后一次持久化,服务器挂掉,RDB还没来得及写入持久化文件可能会造成数据丢失。
持久化-AOF
主从复制
一主多从:
- 首先将redis.conf文件复制一份到根目录
[root@localhost myredis]# cp /usr/local/redis-4.0.0/redis.conf /myredis/redis.conf
[root@localhost myredis]# ll
总用量 60
-rw-r--r--. 1 root root 57762 5月 14 16:59 redis.conf
- 复制三份不同端口号的redis.conf文件
vi redis6379.conf
include /myredis/redis.conf
pidfile /var/run/redis_6381.pid
port 6381
dbfilename dump6381.rdb
replicaof 127.0.0.1 6379
masterauth 123456 #主机的密码
replicaof 表明自己是从机,他的主机的ip地址是127.0.0.1(本机),端口号是6379。
masterauth是主机的密码。
vi redis6380.conf
vi redis6381.conf 都修改成这个内容
- 启动三个redis服务器
[root@localhost myredis]# redis-server redis6379.conf
[root@localhost myredis]# redis-server redis6380.conf
[root@localhost myredis]# redis-server redis6381.conf
[root@localhost myredis]# ps -ef |grep redis
root 37672 1 0 17:17 ? 00:00:00 redis-server *:6379
root 37972 1 0 17:18 ? 00:00:00 redis-server *:6380
root 38187 1 0 17:18 ? 00:00:00 redis-server *:6381
root 38445 111035 0 17:18 pts/2 00:00:00 grep --color=auto redis
- 开启三个Redis客户端连接
redis-cli -p 6381
redis-cli -p 6380
redis-cli -p 6379
- 输入info replication查看当前是主还是从
127.0.0.1:6381> info replication
# Replication
role:master #master主
connected_slaves:0
master_replid:84ae290048a52f161387e91e9c6c0f026a72df66
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
- 配置从机
slaveof ip地址 端口号 #设置为指定服务器的从机
slaveof no one ##将从机变为主机
127.0.0.1:6380> slaveof 127.0.0.1 6379 #设定为 127.0.0.1 端口号为6379的从机
OK
127.0.0.1:6380> info replication #查看信息
# Replication
role:slave #当前为从机
master_host:127.0.0.1 #主机ip
master_port:6379 #主机端口
master_link_status:down
master_last_io_seconds_ago:-1
master_sync_in_progress:0
slave_repl_offset:1
master_link_down_since_seconds:1652521252
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:c799cd475739fc5c61e790c0442cd1be8efb91f5
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
- 不能在从机上做写操作,只能做读操作
127.0.0.1:6381> keys *
(empty list or set)
127.0.0.1:6381> set a2 v2
(error) READONLY You can't write against a read only slave.
127.0.0.1:6381>
主从复制的原理
哨兵机制
当主机宕机后,往从机中选择一个作为主机。
- 在myredis目录下 新建一个 sentinel.conf文件
- 在文件中配置 sentinel monitor mymaster 127.0.0.1 3682 1 :其中 1 为同意迁移的数量 其中mymaster为取的名称
- 开启redis的哨兵监控
redis-sentinel /myredis/sentinel.conf
- 当为redis设置了密码,同样哨兵检测配置文件也需要设置,sentinel auth-pass <password
sentinel monitor mymaster 127.0.0.1 3682 1 ###设置哨兵监控主服务器
sentinel auth-pass mymaster 123456 #在sentinel.conf配置文件中设置服务器的密码
- 执行命令 ‘redis-sentinel /myredis/sentinel.conf’
可以看到,当 6379 主服务器宕机以后,哨兵会通过投票,将 6381 服务器选举为新的“master”服务器,并且另外一个 6380 的从服务器也自动归属在 6381 服务器下。
主服务器宕机后,哨兵选取从服务器作为主服务器的规则
偏移量指的是和主服务器同步量多的
搭建Redis集群
- 在六个redis.conf文件中添加如下配置:
cluster-enabled yes #开启集群
cluster-config-file nodes-6379.conf #设置结点的配置文件名称
cluster-node-timeout 15000 #设置结点失联时间 超过该时间 毫秒 集群自动进行主从切换
- 开启六个地址的服务器
-
进入到本地用户的bin目录下启动 (遇到的问题)
**注意:**redis版本 4.0 ;
问题:Unrecognized option or bad number of args for: ‘–cluster’
解决方案:不能在安装目录下启动,需要在bin目录下启动cd /usr/local/bin /redis的安装目录/src/redis-cli --cluster create --cluster-replicas 1 ip:端口 ip:端口号
- 集群中主机挂掉,从机变为主机,如果主机再启动,那么启动的这个主机就变为从机。如果该结点的主机从机全部宕机了,那么根据配置看该集群是否还可用。
- 集群的好处和不足
(173条消息) Redis及其Sentinel配置项详细说明_a1282379904的博客-CSDN博客_failover-timeout
(173条消息) 八、Sentinel.conf 配置文件详细介绍_胖太乙的博客-CSDN博客_sentinel 配置文件
缓存穿透
概念
当访问量过大,服务器压力变大,去查询Redis的时候命中率变低 (即查不到数据) 就会去查询数据库
出现很多不正常的url 即搜索不存在的数据,造成服务器瘫痪。
解决方案
缓存击穿
解决方案
缓存雪崩
概念
在极少时间段内,查询大量key集中过期的情况。
分布式锁
1. setnx users 10 #nx表示上锁
del users #释放锁
2. #为了保证上了锁一直没有释放锁 给所设置个过期时间
3. 为了保证原子操作,将上锁和设置过期时间一起执行
set users 10 nx ex 12 #ex表示设置过期时间为12s
实操
Redis配置文件
package com.example.springboot_redis.config;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory connectionFactory){
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
// 设置连接工厂
redisTemplate.setConnectionFactory(connectionFactory);
// 设置JSON的序列化工具 不必手动使用ObjectMapper进行映射
GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
// 设置Key的序列化
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
// 设置Value额序列化
redisTemplate.setValueSerializer(jsonRedisSerializer);
redisTemplate.setHashValueSerializer(jsonRedisSerializer);
return redisTemplate;
}
}
hutool工具类
StrUtil.isNotBlank(shopJson)
BooleanUtil.isTrue(flag); //使用hutool工具类来判断返回能够拆封装防止空指针
Shop shop = JSONUtil.toBean(shopJson,Shop.class);
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop));//将实体类转化为JSON
1.将用户信息以Map格式存储到Redis中
// 7.1 将User对象转成Hash存储
UserDTO userDTO = BeanUtil.copyProperties(user,UserDTO.class); //将user的信息复制到UserDTO中
// 7.2将userDTO转化为Map结构方便一次性存入Redis中
Map<String, Object> map = BeanUtil.beanToMap(userDTO,new HashMap<>(),
CopyOptions.create().setIgnoreNullValue(true) //允许有空值
.setFieldValueEditor((fieldName,fieldValue)->fieldValue.toString())//将value转为String
);
// 7.3存储Redis中
String tokenKey = LOGIN_USER_KEY + token;
stringRedisTemplate.opsForHash().putAll(tokenKey,map); //将map结构存入Redis中
2.将查询到的用户信息以Map格式填充为UserDTO类
ap<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY + token);
// 判断用户是否存在
if (userMap.isEmpty()){
return true;
}
// 5.将查询到的Hash数据转换为usrDTO对象 将userMap填充为UserDTO 这种类型,并且报异常
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap,new UserDTO(),false);
Session在Tomcat中不共享的问题
保存用户数据方案:
Token令牌
前端token用例
- 当登录请求响应回来会带上登录凭证Token,将登录凭证Token存储到浏览器的sessionStorage中
注册、登录 带Token
UserServiceImpl类
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Resource
private StringRedisTemplate stringRedisTemplate;
//发送验证码
@Override
public Result sendCode(String phone, HttpSession session) {
// 1.校验手机号
if (RegexUtils.isPhoneInvalid(phone)) {
// 如果不符合,返回错误信息
return Result.fail("手机格式错误!");
}
// 3.符合,生成验证码
// 3.1调用hutool工具类
String code = RandomUtil.randomNumbers(6);
// 4.保存验证码到Redis 并且设置过期时间为2分钟
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);
// 5.发送验证码
log.debug("发送短信验证码成功,验证码:{}"+code);
return Result.ok();
}
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)){
// 校验手机号码格式,不符合格式直接返回
return Result.fail("手机格式有误!");
}
// 校验验证码 从Redis中获得验证码
String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY+phone);
String code = loginForm.getCode();
if (cacheCode == null || !cacheCode.toString().equals(code)){
// 验证码不一致 报错
return Result.fail("验证码错误");
}
// 根据手机号查询用户
User user = query().eq("phone",phone).one(); //根据手机号查询一个用户
if (user == null){
user = createUserWithPhone(phone);
}
// 7 保存用户信息到Redis
String token = UUID.randomUUID().toString(true); //使用hutool中的UUID随机生成一个不带-的token
// 7.1 将User对象转成Hash存储
UserDTO userDTO = BeanUtil.copyProperties(user,UserDTO.class);
// 7.2将userDTO转化为Map结构方便一次性存入Redis中
Map<String, Object> map = BeanUtil.beanToMap(userDTO,new HashMap<>(),
CopyOptions.create().setIgnoreNullValue(true) //允许有空值
.setFieldValueEditor((fieldName,fieldValue)->fieldValue.toString())//将value转为String
);
// 7.3存储Redis中
String tokenKey = LOGIN_USER_KEY + token;
stringRedisTemplate.opsForHash().putAll(tokenKey,map); //将map结构存入Redis中
// 7.4设置token的有效期
stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);
// 7.5 登录续期 在拦截器中做登录续期, 如果能够通过拦截器表示该用户登录,进行续期
session.setAttribute("user", BeanUtil.copyProperties(user,UserDTO.class)); //将user拷贝到UserDto中
return Result.ok();
}
private User createUserWithPhone(String phone){
// 创建用户
User user = new User();
user.setPhone(phone);
user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(10));
save(user);
return user;
}
}
- MvcConfig 用来注册拦截器
package com.hmdp.config;
import com.hmdp.utils.LoginInterceptor;
import com.hmdp.utils.ReflushTokenInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 注册拦截器 添加配置的拦截器
*/
@Configuration
public class MvcConfig implements WebMvcConfigurer {
//这里可以自动注入,因为Configuration交给容器
@Autowired
StringRedisTemplate redisTemplate;
//第二个拦截器,用来做请求是否放行 order是拦截器的级别默认为0,越低越高
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns( //添加放行请求路径
"/shop/**",
"voucher/**",
"/shop-type/**",
"upload/**",
"blog/hot",
"/user/code",
"/user/login"
).order(1);
//第一个拦截器,用来做Redis续期
registry.addInterceptor(new ReflushTokenInterceptor(redisTemplate))
.addPathPatterns("/**").order(0);
}
}
- 线程包装类
package com.hmdp.utils;
import com.hmdp.dto.UserDTO;
/**
* 线程包装类
*/
public class UserHolder {
private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();
public static void saveUser(UserDTO user){
tl.set(user);
}
public static UserDTO getUser(){
return tl.get();
}
public static void removeUser(){
tl.remove();
}
}
拦截器就采用下面那种方案,使用两个拦截器。
拦截器采用责任链模式,当某个拦截器返回false,那么所有的post都不会被执行,直接返回前一个拦截器的ater方法,因为post是目标请求执行之前处理,而目标请求被拦截了,而且又是责任链模式(即先执行所有拦截器的pre方法,再执行post) 那么post也不会被执行
问题:当用户一直访问详情页、主页等不需要登录就能看的页面无法刷新生命周期,那么30分钟之后就会过期。
解决方案:
设置两个拦截器,第一个用来刷新Redis声明周期,第二个拦截器用来控制拦截状态
- 第一个拦截器用来刷新用户的登录时间
package com.hmdp.utils;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* 统一放行,到第二个拦截器再判断是否拦截
*/
public class ReflushTokenInterceptor implements HandlerInterceptor {
//这里不能@Autowired 进行注入,因为拦截器配置器那里的对象是new出来的,只能使用构造器
private StringRedisTemplate stringRedisTemplate;
public ReflushTokenInterceptor(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)) {
return true;
}
// 基于TOKEN获取Redis中的用户
String key = RedisConstants.LOGIN_USER_KEY+token;
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY + token);
// 判断用户是否存在
if (userMap.isEmpty()){
return true;
}
// 5.将查询到的Hash数据转换为usrDTO对象 将userMap填充为UserDTO 这种类型,并且报异常
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap,new UserDTO(),false);
// 存在,保存用户信息到 ThreadLocal
UserHolder.saveUser(userDTO);
// 7.对Redis用户信息进行续期
stringRedisTemplate.expire(key,RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
return true;
}
/**
* 第一个拦截器为结尾,移除当前用户 视图渲染结束之后执行
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 移除用户
UserHolder.removeUser();
}
}
- 第二个拦截器用来校验是否放行
package com.hmdp.utils;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 判断ThreadLocal中是否有用户 来进行拦截
if (UserHolder.getUser() == null){
// 没有用户就进行拦截
response.setStatus(401);
return false;
}
// 有用户,放行
return true;
}
}
Redis缓存商户应用场景
使用场景:
根据请求的商户Id去查询Redis,判断缓存是否命中,命中的话就直接返回给客户端,如果未命中就查询mysql数据库,并且将查询的数据缓存进Redis。
对Redis做修改操作应用场景
缓存和数据库的双写一致
需要先对Redis做更新,再去操作数据库
public Result queryById(Long id) {
String key = CACHE_SHOP_KEY + id;
// 1.从Redis中查询商铺的缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在该商铺
if (StrUtil.isNotBlank(shopJson)){
// 存在就转成Bean并且返回
Shop shop = JSONUtil.toBean(shopJson,Shop.class);
return Result.ok(shop);
}
// 3.不存在就从sql中查询
Shop shop = getById(id);
if (shop == null){
return Result.fail("该店铺不存在!");
}
// 存在,写入Redis, 但要将实体类转化为JSON字符串
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
return Result.ok(shop);
}
/**
* 更新店铺, 需要先查询更新缓存,再操作数据库
* @param shop
* @return
*/
@Override
@Transactional
public Result update(Shop shop) {
Long id =shop.getId();
if(id == null){
return Result.fail("该店铺不存在");
}
// 1. 更新数据库
this.updateById(shop);
// 2. 删除缓存
stringRedisTemplate.delete(CACHE_SHOP_KEY+id);
return Result.ok();
}
解决缓存穿透的使用场景
-
缓存null值
-
布隆过滤
思路:
在sql中查询,如果查询不到该商铺,就将空字符串""写入redis中
在请求redis的时候:
- 如果查询的shopJson是一个空字符串,那么表示该商铺不存在
- 如果查询到null,表示redis中没有该商铺,那么就继续向mysql中进行查询
- 如果查询到数据了,那么就直接返回数据。
查询redis,如果命中了数据直接返回数据,如果命中空字符串返回null,如果数据和空字符串都没命中,那么就查数据库
实现代码:
//封装缓存穿透
public Shop queryPassThrough(Long id){
String key = CACHE_SHOP_KEY + id;
// 1.从Redis中查询商铺的缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在该商铺
if (StrUtil.isNotBlank(shopJson)){ //只有字符串才是true
// 存在就转成Bean并且返回
return JSONUtil.toBean(shopJson,Shop.class);
}
// 判断命中的是否是空值
if(shopJson != null){
// 如果当前不是null,是空字符串,就是之前查数据库查不到的店铺,就返回错误信息
return null;
}
// 3.不存在就从sql中查询
Shop shop = getById(id);
if (shop == null){
// 从数据库中查询不到该商铺就写入Redis一个空字符串
stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
// 存在,写入Redis, 但要将实体类转化为JSON字符串
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
return shop;
}
缓存击穿问题
使用互斥锁来解决缓存击穿
互斥锁,使用setnx lock 1 只有不存在的时候才能设置成功,当释放锁后才能继续被赋值
127.0.0.1:6379> setnx lock 2
(integer) 1
127.0.0.1:6379> setnx lock 2 ####锁没被释放,所以不能赋值
(integer) 0
缓存击穿代码:
// 缓存穿透
// Shop shop = queryPassThrough(id);
//互斥锁解决缓存击穿
Shop shop = queryWithMutex(id);
if(shop == null) {
return Result.fail("店铺不存在!");
}
return Result.ok(shop);
}
public Shop queryWithMutex(Long id ){
String key = CACHE_SHOP_KEY + id;
Shop shop = null;
String lockKey = "lock:shop:" + id;
try {
// 1.从Redis中查询商铺的缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在该商铺
if (StrUtil.isNotBlank(shopJson)){ //只有字符串才是true
// 存在就转成Bean并且返回
return JSONUtil.toBean(shopJson,Shop.class);
}
// 判断命中的是否是空值
if(shopJson != null){
// 缓存穿透
return null;
}
// 4.实现缓存重建
// 4.获取互斥锁
boolean isLock = tryLock(lockKey); //尝试获取锁
if (!isLock){ //获取锁失败
Thread.sleep(50); //失败就睡眠,等待下次获取
return queryWithMutex(id);
}
// 3.不存在就从sql中查询
shop = getById(id);
if (shop == null){
// 从数据库中查询不到该商铺就写入Redis一个空字符串
stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
// 存在,写入Redis, 但要将实体类转化为JSON字符串
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 释放锁
unlock(lockKey);
}
return shop;
}
封装Redis工具类
- 封装的Redis工具类
@Slf4j
@Component
public class CacheClient {
private final StringRedisTemplate stringRedisTemplate;
public CacheClient(StringRedisTemplate stringRedisTemplate){
this.stringRedisTemplate = stringRedisTemplate;
}
public void set(String key, Object value, Long time, TimeUnit timeUnit){
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,timeUnit);
}
public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit timeUnit){
// 设置逻辑过期时间
RedisData redisData = new RedisData();
redisData.setData(value);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(time)));
// 写入redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value));
}
//函数式编程 ID为参数的泛型,R为返回值的泛型
//缓存穿透
public <R,ID>R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID,R>dbFallback
,Long time,TimeUnit timeUnit){
String key =keyPrefix + id;
// 从redis中查询商铺缓存
String json = stringRedisTemplate.opsForValue().get(key);
// 判断是否存在
if(StrUtil.isNotBlank(json)){
return JSONUtil.toBean(json,type);
}
// 判断是否为空字符串
if(json != null){
// 不为null 表示为空字符串
return null; //返回null表示错误数据,不存在该店铺
}
// 接下来查询mysql数据库
R r = dbFallback.apply(id); //调用函数式编程中的方法
if (r == null){ //不存在这个数据存入空字符串
stringRedisTemplate.opsForValue().set(key,"",time,timeUnit); //存入空字符串
return null; //返回null表示数据错误
}
// 存在就写入redis
this.set(key,r,time,timeUnit);
return r;
}
}