实战篇-10.短信登录-基于Redis:实现短信登录
1.自己构建的构造函数使用StringRedisTemplate,不能使用依赖注入
需要在原来的构造函数里注入StringRedisTemplate
2.将Long数据类型转化为String存储到Map中
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
CopyOptions.create()
.setIgnoreNullValue(true)
.setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
实战篇-11.短信登录-解决状态登录刷新的问题
使用两个拦截器,一个登录拦截器,一个token刷新的拦截器,使用order排序执行顺序
// 登录拦截器
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login"
).order(1);
// token刷新的拦截器
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
实战篇-商户查询缓存-02.添加商户缓存
实战篇-商户查询缓存-03.缓存练习题分析
@Override
public Result queryTypeList() {
String key="shop-type";
Long size=stringRedisTemplate.opsForList().size(key);
// 1. 从redis中查询商铺类型列表
List<String> categoryList=stringRedisTemplate.opsForList().range(key,0,size);
// 2. 查询成功,直接将结构放到List中返回
if(categoryList.size()!=0){
List<ShopType> arr=new ArrayList<>();
for (int i = 0; i <size ; i++) {
String cacheCatogery=categoryList.get(i);
arr.add(JSONUtil.toBean(cacheCatogery,ShopType.class));
}
return Result.ok(arr);
}
// 3. 不存在,查询数据库
List<ShopType> typeList = query().orderByAsc("sort").list();
// 4. 不存在返回错误
if(typeList.size()==0){
return Result.fail("没有该数据");
}
//5.存在写入Redis
for(ShopType shopType:typeList){
stringRedisTemplate.opsForList().rightPush(key,JSONUtil.toJsonStr(shopType));
}
return Result.ok(typeList);
}
实战篇-商户查询缓存-05.实现商铺缓存与数据库的双写一致性
修改ShopController中的业务逻辑,满足下面的需求:
①根据id查询店铺时,如果缓存未命中,则查询数据库,将数据库结果写入缓存,并设置超时时间
②根据id修改店铺时,先修改数据库,再删除缓存
@Override
@Transactional
public Result update(Shop shop) {
Long id = shop.getId();
if (id == null) {
return Result.fail("店铺id不能为空");
}
// 1.更新数据库
updateById(shop);
// 2.删除缓存
stringRedisTemplate.delete(CACHE_SHOP_KEY + id);
return Result.ok();
}
缓存穿透
存取null值的解决方法
@override
public Result queryById(Long id){
String key CACHE_SHOP_KEY+id;
//1.从redis查询商铺缓存
String shopJson stringRedisTemplate.opsForValue().get(key);
//2,判断是否存在
if (Strutil.isNotBlank(shopJson)){
//3.存在,直接返回
Shopshop JSONUtil.toBean(shopJson,Shop.class);
return Result.ok(shop);
}
//判断命中的是否是空值
if (shopJson!=null){
//返回一个错误信息
return Result,fail("店铺信息不存在!");
}
//4.不存在,根据id查询数据库
Shop shop getById(id);
//5.不存在,返回错误
if (shop =null)
//将空值写入redis stringRedisTemplate.opsForValue().set(key,value:""CACHE_NULL_TTL,TimeUnit.MINUTES);
//返回错误信息
return Result.fail("店铺不存在!");
}
//6,存在,写入redis stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonstr(shop),CACHE_SHOP_TTL,TimeUnit.MINUTES);
//7.返回
return Result.ok(shop);
}
缓存穿透产生的原因是什么?
用户请求的数据在缓存中和数据库中都不存在,不断发起这样的请求,给数据库带来巨大压力
缓存穿透的解决方案有哪些?
- 缓存null值
- 布隆过滤
- 增强id的复杂度,避免被猜测id规律
- 做好数据的基础格式校验
- 加强用户权限校验
- 做好热点参数的限流
缓存雪崩
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
缓存击穿
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
互斥锁方法解决缓存击穿
@Override
public Result queryById(Long id) {
// 互斥锁解决缓存击穿
Shop shop = cacheClient.queryWithMutex(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 queryWithMutex(Long id){
string key CACHE_SHOP_KEY +id;
//1,从redis查询商铺缓存
String shopJson stringRedisTemplate.opsForvalue().get(key);
//2,判断是否存在
if (Strutil.isNotBLank(shopJson)){
//3.存在,直接返回
return JSONUtil.toBean(shopJson,Shop.class);
}
//判断命中的是否是空值
if (shopJson !null){
//返回一个销误信息
return null;
}
//4,实现缓存重建
//4.1.获取互斥锁
String lockKey ="lock:shop":id;
Shop shop null;
try{
boolean isLock tryLock(lockKey);
/4,2,判断是否获取成功
if(!isLock){
//4.3.失败,则休眠并重试
Thread.sleep(50);
return queryWithMutex(id);
}
//4.4,成功,根据1d查询数掘库
shop getById(id);
//5.不存在,返回销误
if (shop =null){
//将空值写入redis
stringRedisTemplate.opsForValue().set(key,value:""CACHE_NULL_TTL,TimeUnit.MINUTES);
//返回错误信,息
return null;
}
//6,存在,写入redis
stringRedisTemplate.opsForValue().set key,JSONUtil.toJsonstr(shop),CACHE_SHOP_TTL,TimeUnit.MINUTES);
catch (InterruptedException e){
throw new RuntimeException(e);
}finally
//7.释放互斥锁
unlock(lockKey);
}
//加互斥锁
private boolean tryLock(String key){
Boolean flag=stringRedisTemplate.opsForValue()
.setIfAbsent(key,value:"1,timeout:10,TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
//释放互斥锁
private void unlock(String key){
stringRedisTemplate.delete(key);
}
逻辑过期方式解决缓存击穿
```
![](https://zscblog.oss-cn-hangzhou.aliyuncs.com/img/image-20230805001321944.png)
### P46实战篇-商户查询缓存-12.封装Redis工具类
@Data
public class RedisData {
private LocalDateTime expireTime;
private Object data;
}
逻辑过期
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));
}
// 解决缓存穿透
public <R,ID> R queryWithPassThrough(
String keyPrefix, ID id, Class
String key = keyPrefix + id;
// 1.从redis查询商铺缓存
String json = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isNotBlank(json)) {
// 3.存在,直接返回
return JSONUtil.toBean(json, type);
}
// 判断命中的是否是空值
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;
}
// 逻辑过期解决缓存击穿
public <R, ID> R queryWithLogicalExpire(
String keyPrefix, ID id, Class
String key = keyPrefix + id;
// 1.从redis查询商铺缓存
String json = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isBlank(json)) {
// 3.存在,直接返回
return null;
}
// 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;
}
// 互斥锁解决缓存击穿
public <R, ID> R queryWithMutex(
String keyPrefix, ID id, Class
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
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) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
private void unlock(String key) {
stringRedisTemplate.delete(key);
}
### 全局ID生成器
@Component
public class RedisIdWorker {
/**
* 开始时间戳
/
private static final long BEGIN_TIMESTAMP = 1640995200L;
/*
* 序列号的位数
*/
private static final int COUNT_BITS = 32;
private StringRedisTemplate stringRedisTemplate;
public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
public long nextId(String keyPrefix) {
// 1.生成时间戳
LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long timestamp = nowSecond - BEGIN_TIMESTAMP;
// 2.生成序列号
// 2.1.获取当前日期,精确到天
String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
// 2.2.自增长
long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
// 3.拼接并返回
return timestamp << COUNT_BITS | count;
}
}
### 实现优惠券秒杀下单
private void createVoucherOrder(VoucherOrder voucherOrder) {
Long userId = voucherOrder.getUserId();
Long voucherId = voucherOrder.getVoucherId();
// 创建锁对象
RLock redisLock = redissonClient.getLock(“lock:order:” + userId);
// 尝试获取锁
boolean isLock = redisLock.tryLock();
// 判断
if (!isLock) {
// 获取锁失败,直接返回失败或者重试
log.error(“不允许重复下单!”);
return;
}
try {
// 5.1.查询订单
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
// 5.2.判断是否存在
if (count > 0) {
// 用户已经购买过了
log.error("不允许重复下单!");
return;
}
// 6.扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1") // set stock = stock - 1
.eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0
.update();
if (!success) {
// 扣减失败
log.error("库存不足!");
return;
}
// 7.创建订单
save(voucherOrder);
} finally {
// 释放锁
redisLock.unlock();
}
}
![](https://zscblog.oss-cn-hangzhou.aliyuncs.com/img/image-20230805171054030.png)
### 超卖问题
使用乐观锁解决超卖问题
// 6.扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1") // set stock = stock - 1
.eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0
private void createVoucherOrder(VoucherOrder voucherOrder) {
Long userId = voucherOrder.getUserId();
Long voucherId = voucherOrder.getVoucherId();
// 创建锁对象
RLock redisLock = redissonClient.getLock(“lock:order:” + userId);
// 尝试获取锁
boolean isLock = redisLock.tryLock();
// 判断
if (!isLock) {
// 获取锁失败,直接返回失败或者重试
log.error(“不允许重复下单!”);
return;
}
try {
// 5.1.查询订单
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
// 5.2.判断是否存在
if (count > 0) {
// 用户已经购买过了
log.error("不允许重复下单!");
return;
}
// 6.扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1") // set stock = stock - 1
.eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0
.update();
if (!success) {
// 扣减失败
log.error("库存不足!");
return;
}
// 7.创建订单
save(voucherOrder);
} finally {
// 释放锁
redisLock.unlock();
}
}
### 一人一单
先获取锁再下订单
Long userId UserHolder.getUser().getId();
synchronized (userId.tostring().intern()){
return createVoucherorder(voucherId);
}
下单
@Transactional
public Result createVoucherOrder(Long voucherId) {
// 5.一人一单
Long userId = UserHolder.getUser().getId();
synchronized (userId.toString().intern()) {
// 5.1.查询订单
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
// 5.2.判断是否存在
if (count > 0) {
// 用户已经购买过了
return Result.fail("用户已经购买过一次!");
}
// 6.扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1") // set stock = stock - 1
.eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0
.update();
if (!success) {
// 扣减失败
return Result.fail("库存不足!");
}
// 7.创建订单
VoucherOrder voucherOrder = new VoucherOrder();
// 7.1.订单id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
// 7.2.用户id
voucherOrder.setUserId(userId);
// 7.3.代金券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
// 7.返回订单id
return Result.ok(orderId);
}
}
但是在高并发情况下会出现安全问题
### 使用分布式锁实现一人一单
### 使用Redis实现分布式锁
![](https://zscblog.oss-cn-hangzhou.aliyuncs.com/img/image-20230805200850006.png)
- 获取锁
#添加锁,NX是互斥、EX是设置超时时间
SET lock thread1 NX EX 10
- 释放锁
#释放锁,删除即可
DEL key
### 基于Redis实现分布式锁初级版本
@Override
public Result seckillVoucher(Long voucherId) {
// 1.查询优惠券
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
// 2.判断秒杀是否开始
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
// 尚未开始
return Result.fail(“秒杀尚未开始!”);
}
// 3.判断秒杀是否已经结束
if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
// 尚未开始
return Result.fail(“秒杀已经结束!”);
}
// 4.判断库存是否充足
if (voucher.getStock() < 1) {
// 库存不足
return Result.fail(“库存不足!”);
}
Long userId = UserHolder.getUser().getId();
//创建锁对象(新增代码)
SimpleRedisLock lock = new SimpleRedisLock(“order:” + userId, stringRedisTemplate);
//获取锁对象
boolean isLock = lock.tryLock(1200);
//加锁失败
if (!isLock) {
return Result.fail(“不允许重复下单”);
}
try {
//获取代理对象(事务)
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
} finally {
//释放锁
lock.unlock();
}
}
private static final String KEY_PREFIX=”lock:”
@Override
public boolean tryLock(long timeoutSec) {
// 获取线程标示
String threadId = Thread.currentThread().getId()
// 获取锁
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId + “”, timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
public void unlock() {
//通过del删除锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}
### 利用Java代码调用Lua脚本改造分布式锁
private static final DefaultRedisScript
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setLocation(new ClassPathResource(“unlock.lua”));
UNLOCK_SCRIPT.setResultType(Long.class);
}
public void unlock() {
// 调用lua脚本
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX + name),
ID_PREFIX + Thread.currentThread().getId());
}