一、Redis的应用
1 基于Redis实现共享session登录
2 缓存更新策略
主动更新,先写数据库再删缓存
3 缓冲穿透实战
缓存穿透:缓存和数据库都没有数据
采用缓冲空对象方法
4 缓冲雪崩实战
缓存雪崩:大量key同时过期
5 缓冲击穿实战
缓存击穿:缓存没有而数据库有该数据
实践:基于互斥锁方式解决缓存击穿问题
获取锁:商铺id作键,来存储到内存中;setIfAbsent相当于redis中的SETNX key value,当且仅当 key 不存在,将 key 的值设为 value ,并返回1;若给定的 key 已经存在,则 SETNX 不做任何动作,并返回0。来实现锁
String key = CACHE_SHOP_KEY + id;
stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
释放锁:删除key值就可以了
stringRedisTemplate.delete(key);
实践:基于逻辑过期方式解决缓存击穿问题
:逻辑时间指自己在value上再加多一个时间属性,而不是为key设定过期时间,每当命中该key时,用其value附带的时间属性来判断是否过期。
6 优惠券秒杀功能
Redis实现全局唯一id
全局唯一id特性:唯一性、高可用、高性能、递增性、安全性
实现方法:使用redis自增特性, 满足上面的高可用、高性能、递增性
id组成:该id满足唯一性、安全性
具体实现方法:生成该类型的id,将其存到缓存中。
其他策略:
UUID
雪花算法
数据库自增
实践:秒杀下单功能
redis实现订单id全局唯一
超卖问题
高并发引发的问题
解决方法:乐观锁
具体实现方式:
版本号控制:数据表中加上一个
version
字段来实现的,表示数据被修改的次数,当执行写操作并且写入成功后,version = version + 1,当线程A要更新数据时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。CAS算法:在更新时,加上where判断值是否等于已经查询的值,非阻塞,无锁
产生问题:成功率降低
解决方法:where判断改为是否大于0
一人一单
在原有的业务上再加上查询,是否存在该用户id和优惠卷id的订单数据
对扣减库存、创建订单业务进行加锁,根据用户id来作条件
7 redis实现分布式锁
分布式锁
满足分布式系统或集群下的多进程可见并互斥的锁
特性:多进程可见、互斥、高可用、高性能、安全性
常见三种实现方法:
redis实现分布式锁
基于setnx实现
基本思路:
基本方法:基于setnx,并设定过期时间
具体流程:将用户id作key,当前运行的线程的id作value
问题1:锁过时释放,另一线程获取锁,原线程将其释放调,产生问题
解决方法:在释放锁时判断是否自己的锁,即根据其value值(线程id)判断
问题2:判断锁一致和释放锁两个动作之间产生阻塞,发生错误
解决:使判断和释放操作具有原子性,使用Lua脚本实现多条Redis命令的原子性
基于setnx方法存在以下问题:
基于Redisson框架
redis优化秒杀
多线程,将业务划分,一部分给redis,一部分给另一线程,实现异步优化
异步优化后的业务
阻塞队列,线程池,java多线程等知识
具体步骤:
接到请求,先执行Lua脚本,判断是否有资格下单,没有则返回
有则创建订单信息,并将订单信息放到阻塞队列
另一线程不断获取阻塞队列的数据,并将数据写入数据库
存在问题:
阻塞队列基于jvm的内存,高并发下大量订单对象放到队列,造成内存溢出,
基于内存保存订单对象,服务宕机,导致内存数据丢失,jvm内存没有持久化
Redis消息队列实现异步秒杀
业务划分:跟阻塞队列方法类似,只不过是用消息队列解决业务
Redis实现消息队列:
list结构:模拟消息队列
优点:利用redis存储,不受限jvm内存上限,基于redis持久化机制,保障数据安全性,满足消息有序
缺点:无法避免消息丢失,只支持单消费者
PubSub:基本的点对点模型,redis的发布订阅机制
优点:支持多生产多消费
缺点:不支持数据持久化,无法避免消息丢失,消息存储有限
Stream:比较完善的消息队列模型,是redis的新数据类型
优点:消息可回溯、支持多消费者获取、可阻塞读取
缺点:阻塞读取有消息漏读的风险
基于Stream的消息队列-消费者组
消费者监听消息的基本思路:
实现异步秒杀主要业务
Redis实战
redis完善点赞功能
业务:避免重复点赞
思路:
用redis中的set存储("blog:liked:" + blog.id,userId)键值对,blog.id映射所有点赞的用户id
点赞时,判断是否点赞过,根据set存储的结果+-点赞数,避免重复点赞
点赞排行榜
业务:打开blog时,发送请求,查询5名点赞用户,并返回
方法:根据点赞时间排序
将上面的Set存储(blogId,userId)改为用ZSet存储,时间戳作排序规则,在查询
时,查询该ZSet集合的前5名用户id,得到5名用户信息
共同关注功能
业务需求:点击查看某用户信息的共同关注时,显示该用户与本机用户的共同关注的用户
方法:用redis的set存储(用户id,该用户关注的用户id),请求查询时,查询set中本机用户与要查看的用户的并集结果,得到共同关注用户信息
附近商户搜索
思路:
使用redis中的GEO数据结构存储经纬度数据,当查询附近商户请求时,查询redis,按距离排序、分页,得到商户数据
用户签到
签到功能
需求:实现签到请求,将当前用户当天签到信息保存到Redis中
方法:使用BitMap数据结构
获取当前用户id和日期,拼接作key
获取今天是这月的第几天,index
存储到redis中BitMap,(key, index, true),index指定
统计连续签到天数
方法:
获取当前用户id和日期,拼接作key
根据key从上面的BitMap数据中取出对应签到数据long
根据数据和1做与运算来统计
UV统计
UV:独立访客量,用户数
PV:页面访问量
redis中的HyperLogLog用于确认非常大的集合的基数,而不需要存储其所值
技巧
将user数据实现线程共享
public class UserHolder {
private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();
public static void saveUser(UserDTO userId){
tl.set(userId);
}
public static UserDTO getUser(){
return tl.get();
}
public static void removeUser(){
tl.remove();
}
}
二级拦截
用于不同拦截的需求
这样无论访问什么路径都刷新token时间
一些常用的值尽量用常量来代替
public class SystemConstants {
public static final String IMAGE_UPLOAD_DIR = "D:\\lesson\\nginx-1.18.0\\html\\hmdp\\imgs\\";
public static final String USER_NICK_NAME_PREFIX = "user_";
public static final int DEFAULT_PAGE_SIZE = 5;
public static final int MAX_PAGE_SIZE = 10;
}
操作Redis中数据
使用StringRedisTemplate工具类,该类指定key,value的类型为string
//从Hash数据中找key对应的数据,返回Map类型
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
//userMap存储user属性对,将其存储到Redis的Hash结构中
stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
//存储简单key,value值
stringRedisTemplate.opsForValue().set(phone, code, 30, TimeUnit.MINUTES);
注解
@TableField(exist = false)
注明非数据库字段属性,不需要数据库存储的属性(暂时属性)
hutool工具类
//将 userDTO类转换为Map类型存储
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO);
//验证手机号格式
RegexUtils.isPhoneInvalid(phone)
//生成随机数,比原生的效果更好
RandomUtil.randomNumbers(6)
//时间常量类,多用在指定redis时间的单位
TimeUnit
//随机生成UUID
UUID.randomUUID()
//复制Bean对象
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
//判断对象是字符串是否为空白
StrUtil.isBlank(token)
//json反序列化为对象
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);