Redis学习笔记——以黑马点评为例(更新中...)

一、短信登录:Session

1.1 实现验证码发送

根据页面发送的请求,找到/user/code,编写userConntroller
在这里插入图片描述

/**
     * 发送手机验证码
     */
@PostMapping("code")
    public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
        // TODO 发送短信验证码并保存验证码
        //return Result.fail("功能未完成");
        return userService.sendCode(phone,session);
    }

ctrl +左击userService,进入IUserService中,编写sendCode方法

Result sendCode(String phone, HttpSession session);

进入对应Impl实现类,实现sendCode方法,并对其具体实现

@Override
    public Result sendCode(String phone, HttpSession session) {
        //1.校验手机号
        if (RegexUtils.isPhoneInvalid(phone)){
            //2.如果不符合,返回错误信息
            return Result.fail("手机号格式错误");
        }
        //3.符合,生成验证码
        String code = RandomUtil.randomNumbers(6);
        //4.保存验证码到session
        session.setAttribute("code",code);
        //5.发送验证码
        log.info("发送短信验证码成功,验证码:{}",code);
        //返回ok
        return Result.ok();
    }

1.2 实现短信验证码登录和注册功能

根据页面发送的请求,找到/user/login,编写userConntroller
在这里插入图片描述

/**
     * 登录功能
     * @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
     */
    @PostMapping("/login")
    public Result login(@RequestBody LoginFormDTO  loginForm, HttpSession session){
        // TODO 实现登录功能
        //return Result.fail("功能未完成");
        return userService.login(loginForm,session);
    }

ctrl +左击userService,进入IUserService中,编写sendCode方法

Result login(LoginFormDTO loginForm, HttpSession session);

进入对应Impl实现类,实现sendCode方法,并对其具体实现

@Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        //1.校验手机号
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)){
            //2.如果不符合,返回错误信息
            return Result.fail("手机号格式错误!");
        }
        //2.校验验证码
        Object cacheCode = session.getAttribute("code");        //取出session中的code值
        String code = loginForm.getCode();                          //取出前端的code值
        if ( cacheCode == null || !cacheCode.toString().equals(code)){
            //3.不一致,报错
            return Result.fail("验证码错误!");
        }
        
        //4.一致,根据手机号查询用户  select * from tb_user where phone = ?
        User user = query().eq("phone", phone).one();

        //5.判断用户是否存在
        if (user == null){
            //6.不存在,创建新用户并保存
             user = creatUserWithPhone(phone);
        }

        //7.保存用户信息到session中
        session.setAttribute("user",user);

        return Result.ok();
    }

    private User creatUserWithPhone(String phone) {
        //1.创建用户
        User user = new User();
        user.setPhone(phone);
        user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));    //使用系统常量
        //2.保存用户
        save(user);
        return user;
    }

执行后,可查看后台数据库更新了一条记录
在这里插入图片描述

1.3 登录校验拦截器

用户的请求会带Cookie,从Session中获取用户。
在用户的请求和所有的Controller之间设置拦截器,将拦截到的用户信息存放在ThreadLocal(线程)中,每一个进入到Tomcat的请求都是一个独立的线程。
在utils下编写拦截器LoginInterceptor类,实现preHandle和afterCompletion这两个方法

public class LoginInterceptor implements HandlerInterceptor {
    /**
     * 前置拦截
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1.获取session
        HttpSession session = request.getSession();
        //2.获取session中的用户
        Object user = session.getAttribute("user");
        //3.判断用户是否存在
        if (user == null){
            //4.不存在,拦截,返回401状态码
            response.setStatus(401);
            return false;
        }
        //5.存在,保存用户信息到ThreadLocal
        UserHolder.saveUser((UserDTO) user);
        //6.放行
        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();
    }
}

拦截器编写完后,需要在程序运行过程中添加该拦截器
在Config包下创建MvcConfig

@Configuration
public class MvcConfig implements WebMvcConfigurer {
    //添加拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                //排除不需要拦截的路径
                .excludePathPatterns(
                        "/shop/**",
                        "/voucher/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                );
    }
}

通过封装UserDTO,减少返回的信息,隐藏用户敏感信息
在这里插入图片描述
更改前述代码,使信息在存放至session中时就以UserDTO的形式存放,则后述从ThreadLocal线程中获取时也以UserDTO形式获取
在这里插入图片描述
在这里插入图片描述

1.4 集群的session共享

多台Tomcat不共享session存储空间,当请求切换到不同tomcat服务时导致数据丢失问题。
session拷贝的缺点:内存空间的浪费,存在延迟等
其替代方案需满足:数据共享,内存存储,key/value结构

1.5 基于Redis实现共享session登录

根据手机号生成的验证码以key/value形式存入到Redis,登录注册时,以手机号为key,验证码为value在Redis中校验。并根据手机号查询用户,判断是否存在,最终以Hash结构,随机token为key存储用户数据存入Redis(可以将对象中的每个字段独立存储,可以针对单个字段做CURD,并且内存占用更少),返回token给客户端,登录校验时,请求并携带token,从redis获取用户信息,判断是否存在,保存用户到ThreadLocal,放行,结束。

发送验证码
在这里插入图片描述
在这里插入图片描述

验证码登录注册
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在LoginInterceptor中,由于LoginInterceptor是手动创建的,因此无法通过依赖注入StringRedisTemplate,只能通过构造函数拦截器中注入
在这里插入图片描述
在这里插入图片描述
在LoginInterceptor中继续修改
在这里插入图片描述
至此,启动程序后,再次点击我的,仍需进行登录操作。用户访问拦截器拦截的页面会刷新页面,访问的是不需要拦截的页面则不会导致页面的刷新。

解决状态登录刷新问题

对此,我们添加一个拦截器,让其进行刷新拦截,拷贝LoginInterceptor,命名为RefreshTokenInterceptor,修改代码:
在这里插入图片描述
原来的LoginInterceptor拦截器更改为:
在这里插入图片描述
MvcConfig中添加刚才编写的拦截器,通过order()哎定义拦截器的先后执行顺序
在这里插入图片描述
运行程序后,报错
在这里插入图片描述
此时我们查看UserController中/me的代码,对照前端代码,发现所传数据不一致,后端给的是user,前端给的是token,将前端改为token,同时将跳转页面修改为:跳转到info.html首页
在这里插入图片描述
由于在拦截器配置中加入了/user/me,那将不会执行LoginInterceptor中的代码,也就是不会从session中拿到用户信息放到threadlocal中去,也就是/user/me接口没有从返回的浏览器数据中拿到相应的user数据,因此登录后点击“我的”,需要重新登陆,我们可以直接在me()方法里面拿到用户信息并返回,方法是直接从HttpServletRequest中拿到session,之后从session中直接拿到用户信息
在userController类中注入

@Resource
    private HttpServletRequest httpServletRequest;

修改代码:

@GetMapping("/me")
    public Result me(){
        // TODO 获取当前登录的用户并返回
        //return Result.fail("功能未完成");
        //UserDTO user = UserHolder.getUser();
        HttpSession session =httpServletRequest.getSession();
        UserDTO user = (UserDTO) session.getAttribute("user");
        return Result.ok(user);
        //return Result.ok("/blog/of/me");
    }

二、商户查询缓存

2.1 什么是缓存&&添加Redis缓存

缓存
缓存是数据交换的缓冲区,是存储数据的临时地方,一般读写性能比较高。
缓存的作用:降低后端负载;提高读写效率,降低响应时间
缓存的成本:数据的一致性成本;代码维护成本;运维成本
在缓存数据一致性处理过程中,会出现缓存穿透击穿等问题,代码复杂度会大大提高

添加Redis缓存
在这里插入图片描述

添加商户id缓存

shopController控制层
在这里插入图片描述
service服务层
在这里插入图片描述
Impl实现层

@Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Result queryById(Long id) {
        //提前创建一个key值,将"cache:shop:"封装为一个常量,放在RedisConstants中
        String key =  CACHE_SHOP_KEY + id;

        //1.从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //2.判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            //3.存在,直接返回
            //将shopJson转换为shop实体类型
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
        //4.不存在,根据id查询数据库
        Shop shop = getById(id);
        //5.不存在,返回错误
        if (shop == null) {
            return Result.fail("店铺不存在!");
        }
        //6.存在,写入redis   将shop转为一个Jsonstring类型存入redis
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop));
        //7.返回
        return Result.ok(shop);
    }

添加商户类型缓存

shopController控制层
在这里插入图片描述
service服务层
在这里插入图片描述
Impl实现层

@Resource
    private StringRedisTemplate stringRedisTemplate;
    @Override
    public Result queryShopList() {
        //提前创建一个key值,将"cache:shoptype:"封装为一个常量,放在RedisConstants中
        String key =  CACHE_SHOPTYPE_KEY;

        //1.从redis查询商铺类型缓存
        List<String> shopTypeList =new ArrayList<>();
        shopTypeList = stringRedisTemplate.opsForList().range(key,0,-1);
        //2.判断是否存在
        if (!shopTypeList.isEmpty()) {
            //3.存在,直接返回
            //将shopTypeJson转换为shopType实体类型
            //创建一个新的List,存放shopType实体类型
            List<ShopType> typeList = new ArrayList<>();
            for (String  s:shopTypeList) {
                ShopType shopType = JSONUtil.toBean(s, ShopType.class);
                typeList.add(shopType);
            }
            return Result.ok(typeList);
        }

        //4.不存在,根据分类查询数据库
        List<ShopType> typeList = query().orderByAsc("sort").list();
        //5.不存在,返回错误
        if (typeList.isEmpty()) {
            return Result.fail("不存在该分类!");
        }
        //6.添加该分类进数据库
        for (ShopType shopType:typeList) {
            //将该分类类型转换为字符串形式,放入数据库List中
            String s = JSONUtil.toJsonStr(shopType);
            shopTypeList.add(s);
        }
        //6.存在,写入redis   ,以List形式,从右边直接全部推入
        stringRedisTemplate.opsForList().rightPushAll(key,shopTypeList);
        //7.返回
        return Result.ok(typeList);
    }

2.2 缓存更新策略

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
先删除缓存,再操作数据库
正常情况
在这里插入图片描述
特殊情况
在这里插入图片描述
先写数据库,再删除缓存
正常情况
在这里插入图片描述
特殊情况
在这里插入图片描述
缓存更新策略的最佳实践方案:
1.低一致性需求:使用Redis自带的内存淘汰机制
2.高一致性需求:主动更新,并以超时剔除作为兜底方案
读操作:
缓存命中则直接返回
缓存未命中则查询数据库,并写入缓存,设定超时时间
写操作:
先写数据库,然后再删除缓存
要确保数据库与缓存操作的原子性

2.3 实现商铺缓存与数据库的双写一致

controller控制层:
在这里插入图片描述
service服务层:
在这里插入图片描述
Impl实现层:

@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 + shop.getId());
        return Result.ok();
    }

2.4 缓存穿透

缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会达到数据库。
产生原因:
用户请求的数据在缓存中和数据库中都不存在,不断发起这样的请求,给数据库带来巨大压力
解决思路:
1、缓存空对象(空串或其他标志字符):
优点:实现简单,维护方便
缺点:额外的内存消耗;可能造成短期的不一致
2、布隆过滤:
优点:内存占用较少,没有多余key
缺点:实现复杂;存在误判可能
3、增加id的复杂度,避免被猜测id规律
4、做好数据的基础格式校验
5、加强用户权限校验
6、做好热点参数的限流
在这里插入图片描述

解决商铺查询的缓存穿透问题(缓存空对象)

在这里插入图片描述
在ShopServiceImpl中的queryById方法中进行代码修改:
在这里插入图片描述

2.5 缓存雪崩

缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
解决方案
1、给不同的key的TTL添加随机值
2、利用Redis集群提高服务的可用性(Redis哨兵)
3、给缓存也无添加降级限流策略
4、给业务添加多级缓存
在这里插入图片描述

2.5 缓存击穿(热点Key)

缓存击穿问题也叫热点key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
在这里插入图片描述
解决方案:
1、互斥锁(一致性)
优点:没有额外的内存消耗;保证一致性;实现简单
缺点:线程需要等待,性能受影响;可能有死锁风险
在这里插入图片描述
2、逻辑过期(可用性)
优点:线程无需等待;性能较好
缺点:不能保证一致性(返回旧数据);有额外内存消耗;实现复杂

在这里插入图片描述
在这里插入图片描述

修改根据id查询商铺的业务,基于互斥锁方式来解决缓存击穿问题

在这里插入图片描述
在ShopServiceImpl中添加方法

//TODO 获取锁
    private boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }
    //TODO 释放锁
    private void unLock(String key){
        stringRedisTemplate.delete(key);
    }

将缓存穿透和互斥锁解决缓存击穿方法分别进行单独封装,并在queryById方法中进行调用
在这里插入图片描述
定义queryWithPassThrough,进行缓存穿透
在这里插入图片描述
定义queryWithMutes方法,进行缓存击穿

//TODO 互斥锁解决缓存击穿
    public Shop queryWithMutes(Long id){
        //提前创建一个key值,将"cache:shop:"封装为一个常量,放在RedisConstants中
        String key =  CACHE_SHOP_KEY + id;

        //1.从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //2.判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            //3.存在,直接返回
            //将shopJson转换为shop实体类型
            return JSONUtil.toBean(shopJson, Shop.class);
        }
        //缓存穿透,判断命中的是否是空值
        if (shopJson != null){    //不等于null,也就是空字符串
            //返回错误信息
            return null;
        }

        //TODO 4.实现缓存重建
        //TODO 4.1 获取互斥锁
        String lockKey = "lock:shop:" + id;
        Shop shop = null;
        try {
            boolean isLock = tryLock(lockKey);
            //TODO 4.2 判断是否获取成功
            if (!isLock){
                //TODO 4.3 失败,则休眠
                Thread.sleep(50);
                 return queryWithMutes(id);
            }

            //4.4 成功,根据id查询数据库
            shop = getById(id);
            //TODO 模拟重建的延时
            Thread.sleep(200);
            //5.不存在,返回错误
            if (shop == null) {
                //将空值写入redis
                stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL, TimeUnit.MINUTES);
                //返回错误信息
                return null;
            }
            //6.存在,写入redis   将shop转为一个Jsonstring类型存入redis
            stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            //TODO 7. 释放互斥锁
            unLock(lockKey);
        }

        //8.返回
        return shop;
    }

修改根据id查询商铺的业务,利用逻辑过期来解决缓存击穿问题

在这里插入图片描述
数据预热,将要存放到redis的数据封装为data,放在redisData中,并在shopServiceImpl中添加方法saveShop2Redis,将shop添加到redis中
在这里插入图片描述

/将shop添加到redis中
    public void saveShop2Redis(Long id,Long expireSeconds) throws InterruptedException{
        //1.查询店铺数据
        Shop shop = getById(id);
        Thread.sleep(200);
        //2.封装逻辑过期时间
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
        //3.写入Redis
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
    }

编写测试类进行检验

@SpringBootTest
class HmDianPingApplicationTests {

    @Resource
    private ShopServiceImpl shopService;

    @Test
    void testSave(){
        shopService.saveShop2Redis(1L,10L);
    }

}

打开redis查看,数据以data和过期时间
在这里插入图片描述
单独封装逻辑过期解决缓存穿击穿方法,并在queryById方法中进行调用
在这里插入图片描述

//TODO 创建线程池
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    //TODO 逻辑过期解决缓存击穿问题
    public Shop queryWithLogicalExpire(Long id){
        //提前创建一个key值,将"cache:shop:"封装为一个常量,放在RedisConstants中
        String key =  CACHE_SHOP_KEY + id;
        //1.从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //2.判断是否存在
        if (StrUtil.isBlank(shopJson)) {
            //3.不存在,直接返回空
            return null;
        }
        //TODO 4.命中,需要先把json反序列化为对象
        RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
        JSONObject data =(JSONObject) redisData.getData();
        Shop shop = JSONUtil.toBean(data, Shop.class);
        LocalDateTime expireTime = redisData.getExpireTime();

        //TODO 5.判断是否过期
        if (expireTime.isAfter(LocalDateTime.now())) {
            //TODO 5.1 未过期,直接返回店铺信息
            return shop;
        }
        //TODO 5.2 已过期,需要缓存重建
        //TODO 6.缓存重建
        //TODO 6.1 获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        boolean isLock = tryLock(lockKey);
        //TODO 6.2 判断是否获取锁成功
        if (isLock){
            //TODO 6.3 成功,开启独立线程,实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(()->{
                try {
                    //重建缓存
                    this.saveShop2Redis(id,20L);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }finally {
                    //释放锁
                    unLock(lockKey);
                }
            });
        }
        //TODO 6.4 返回过期的商铺信息
        return shop;
    }

2.6 封装Redis工具类

在utils包下创建CacheClient类

private final StringRedisTemplate stringRedisTemplate;

    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    public void set(String key, Object value, Long time, TimeUnit unit){
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
    }

    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));
    }

拷贝queryWithPassThrough的代码到CacheClient类中进行改写,并注释掉原有的queryWithPassThrough方法:

//TODO 缓存穿透
    public <R,ID> R queryWithPassThrough(           //函数逻辑
            String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit){
        //提前创建一个key值,将"cache:shop:"封装为一个常量,放在RedisConstants中
        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){    //不等于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   将shop转为一个Jsonstring类型存入redis
        this.set(key, r, time, unit);
        //7.返回
        return r;
    }

到ShopServiceImpl中直接调用新的方法(提前引入CacheClient类):
在这里插入图片描述

拷贝queryWithLogicalExpire的代码到CacheClient类中进行改写,并注释掉原有的queryWithLogicalExpire方法:

//TODO 逻辑过期解决缓存击穿问题
    //TODO 创建线程池
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    public <R,ID> R queryWithLogicalExpire(
            String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback, Long time, TimeUnit unit){
        //提前创建一个key值,将"cache:shop:"封装为一个常量,放在RedisConstants中
        String key =  keyPrefix + id;
        //1.从redis查询商铺缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        //2.判断是否存在
        if (StrUtil.isBlank(json)) {
            //3.不存在,直接返回空
            return null;
        }
        //TODO 4.命中,需要先把json反序列化为对象
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        JSONObject data =(JSONObject) redisData.getData();
        R r = JSONUtil.toBean(data, type);
        LocalDateTime expireTime = redisData.getExpireTime();

        //TODO 5.判断是否过期
        if (expireTime.isAfter(LocalDateTime.now())) {
            //TODO 5.1 未过期,直接返回店铺信息
            return r;
        }
        //TODO 5.2 已过期,需要缓存重建
        //TODO 6.缓存重建
        //TODO 6.1 获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        boolean isLock = tryLock(lockKey);
        //TODO 6.2 判断是否获取锁成功
        if (isLock){
            //TODO 6.3 成功,开启独立线程,实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(()->{
                try {
                    //重建缓存
                    //查询数据库
                    R r1 = dbFallback.apply(id);
                    //写入Redis
                    this.setWithLogicalExpire(key, r1, time, unit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }finally {
                    //释放锁
                    unLock(lockKey);
                }
            });
        }
        //TODO 6.4 返回过期的商铺信息
        return r;
    }

    //TODO 获取锁
    private boolean tryLock(String key){

        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }
    //TODO 释放锁
    private void unLock(String key){
        stringRedisTemplate.delete(key);
    }

改写测试类:
在这里插入图片描述
测试:首先运行HmDianPingApplicationTests类里的测试方法,10秒后逻辑过期,redis中仍显示数据,但数据为过期状态。此时运行后台程序,通过Jmeter进行测试,对原有的测试结果做清空处理,重新运行,在idea中可以看到仅对数据库做一次查询。
在这里插入图片描述
在这里插入图片描述

三、优惠券秒杀

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值