redis的使用场景

1. redis的使用场景

redis使用场景的案例:
   [1]热点数据的缓存
   [2]分布式锁
   [3]短信业务(登录注册时)

2. redis实现注册登录功能

在这里插入图片描述

代码

在发送验证码时,先判断数据库是否有该手机号,有则发送验证码(此时redis缓存中有发送过该验证码,则返回已发送,防止多次发送验证码)并储存到redis中(手机号作为唯一的在加上过期时间)。

在登录验证手机号和验证码时,根据输入的手机号(唯一key在redis中查询)是否跟输入的验证码(不为空)时相同,登录成功,并删除该redis数据(验证码只能作为一次登录)。

@Autowired
    private StringRedisTemplate redisTemplate;
    @GetMapping("send")
    public R send(String phone) throws Exception {
        //1. 校验手机号是否存在---连接数据库
        if(phone.equals("15137437506")||phone.equals("15959715454")){
            if(redisTemplate.hasKey("code::"+phone)){
                return new R(500,"验证码已发送",null);
            }
            //2. 发生验证码
            String code = SendMsgUtil.sendCode(phone);
            //3. 保存验证码到redis.
            redisTemplate.opsForValue().set("code::"+phone,code,5, TimeUnit.MICROSECONDS);
            return new R(200,"发送成功",null);
        }

        return new R(500,"手机号未注册",null);
    }


    @PostMapping("login")
    public R login(@RequestBody LoginVo loginVo){
        //1. 校验验证码
        String code = redisTemplate.opsForValue().get("code::" + loginVo.getPhone());
        String phone = loginVo.getPhone();
        if(StringUtils.hasText(loginVo.getCode()) && loginVo.getCode().equals(code)){
            if(phone.equals("18839986970")||phone.equals("15137437506")){
                redisTemplate.delete("code::"+phone);
                return new R(200,"登录成功",null);
            }else{
                return new R(500,"手机号错误",null);
            }
        }
        return new R(500,"验证码错误",null);
    }

3. 热点数据缓存

为了把一些经常访问的数据,放入缓存中以减少对数据库的访问频率。从而减少数据库的压力,提高程序的性能。【内存中存储】

3.1 缓存的原理

在这里插入图片描述

3.2 java使用redis如何实现缓存功能

在增删改查中模拟redis缓存

@Service
public class StockServiceImpl02 implements StockService {
    @Autowired
    private StockDao stockDao;
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    @Override
    public Stock getById(Integer id) {
        //1.查询redis缓存是否命中
        ValueOperations<String, Object> forValue = redisTemplate.opsForValue();
        Object o = forValue.get("stock::" + id);
        //System.out.println(o);
        //表示缓存命中
        if(o!=null){
            return (Stock) o;
        }
        //查询数据库
        Stock stock = stockDao.selectById(id);
        if(stock!=null){
            forValue.set("stock::" + id,stock);
        }
        return stock;
    }

    @Override
    public Stock insert(Stock stock) {
        int insert = stockDao.insert(stock);
        return stock;
    }

    @Override
    public Stock update(Stock stock) {
        //修改数据库
        int i = stockDao.updateById(stock);
        if(i>0){
            //修改缓存
            redisTemplate.opsForValue().set("stock::"+stock.getProductid(),stock);
        }
        return stock;
    }

    @Override
    public int delete(Integer productid) {
        int i = stockDao.deleteById(productid);
        if(i>0){
            //删除缓存
            redisTemplate.delete("stock::"+productid);
        }
        return i;
    }
}

发现在查询时会访问redis缓存,如果命中则直接返回数据,未命中则查询数据库并放入redis缓存;在修改时保持数据一致,需要redis中数据一起改变;在删除时,数据库和redis缓存一起删除。

3.2 使用缓存注解完成缓存功能

使用AOP面向切面编程—spring缓存使用的组件

配置文件

 @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        //解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // 配置序列化(解决乱码的问题),过期时间600秒
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(600)) //缓存过期10分钟 ---- 业务需求。
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))//设置key的序列化方式
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)) //设置value的序列化
                .disableCachingNullValues();
        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
        return cacheManager;
    }

在主函数上需要启动:@EnableCaching

修改上述代码

@Service
public class StockServiceImpl implements StockService {
    @Autowired
    private StockDao stockDao;
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;


    //Cacheable:表示查询时使用的注解。 cacheNames:缓存的名称  key:缓存的唯一表示值
    //   1. 查询缓存中是否存在名称为cacheNames::key的值
    //   2. 如果存在则方法不会执行
    //   3. 如果不存在则执行方法体并把方法的返回结果放入缓存中cacheNames::key
    @Cacheable(cacheNames ={ "stock"}, key = "#id")
    @Override
    public Stock getById(Integer id) {
        Stock stock = stockDao.selectById(id);
        return stock;
    }

    @Override
    public Stock insert(Stock stock) {
        int insert = stockDao.insert(stock);
        return stock;
    }


    //CachePut:表示修改时使用的注解.
    // 1. 先执行方法体
    // 2. 把方法的返回结果放入缓存中
    @CachePut(cacheNames = "stock", key = "#stock.productid")
    @Override
    public Stock update(Stock stock) {
        int i = stockDao.updateById(stock);
        return stock;
    }

    //CacheEvict:表示删除时使用的注解
    // 1. 先执行方法体
    // 2. 把缓存中名称为cacheNames::key的值删除
    @CacheEvict(cacheNames = "stock", key = "#productid")
    @Override
    public int delete(Integer productid) {
        int i = stockDao.deleteById(productid);
        return i;
    }
}

4. 分布式锁

为了模拟高并发:---使用jmeter压测工具

模拟客户端请求环境

在这里插入图片描述

 public String decrement(Integer productid) {
        //根据id查询商品的库存
    int num = stockDao.findById(productid);
    if (num > 0) {
        //修改库存
        stockDao.update(productid);
        System.out.println("商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个");
                return "商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个";
     } else {
         System.out.println("商品编号为:" + productid + "的商品库存不足。");
         return "商品编号为:" + productid + "的商品库存不足。";
            }
        }
}

运行结果:发现出现超卖重卖现象

在这里插入图片描述

解决办法:我们使用synchronized或者lock锁

public String decrement(Integer productid) {
    //根据id查询商品的库存
    synchronized (this) {
    int num = stockDao.findById(productid);
    if (num > 0) {
        //修改库存
        stockDao.update(productid);
        System.out.println("商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个");
                return "商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个";
     } else {
         System.out.println("商品编号为:" + productid + "的商品库存不足。");
         return "商品编号为:" + productid + "的商品库存不足。";
            }
          }
     }
}

再次测试:发现超卖重卖现象没有发生

在这里插入图片描述

上面使用syn和lock虽然解决了并发问题,但是我们未来项目部署时可能要部署集群模式。

在springboot模拟项目集群

在这里插入图片描述

在这里插入图片描述

接下来使用nginx代理集群

在nginx.conf配置文件中设置代理服务器

在这里插入图片描述

启动nginx,再次压测发现依旧会超卖

在这里插入图片描述

在多线程环境中上述的锁对象是本地锁,每个服务器都有本地锁,导致锁不是唯一的

通过压测发现本地锁 无效了。使用redis解决分布式锁文件

在这里插入图片描述

引入redis依赖和配置

		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
spring.redis.host=172.16.7.110
spring.redis.port=6379

修改原有代码

Redis提供了一个命令setnx 可以来实现分布式锁,该命令只在键 key 不存在的情况下 将键 key 的值设置为 value ,若键 key 已经存在, 则 SETNX 命令不做任何动作。根据这一特性我们就可以制定Redis实现分布式锁的方案了。

@Autowired
private StringRedisTemplate redisTemplate;
    //
public String decrement(Integer productid) {
    ValueOperations<String, String> opsForValue = redisTemplate.opsForValue();
     //1.获取共享锁资源
    Boolean flag = opsForValue.setIfAbsent("product::" + productid, "1111", 30, TimeUnit.SECONDS);
     //表示获取锁成功
    if(flag) {
        try {
            //根据id查询商品的库存
            int num = stockDao.findById(productid);
       if (num > 0) {
         //修改库存
        stockDao.update(productid);
        System.out.println("商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个");
        return "商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个";
       } else {
         System.out.println("商品编号为:" + productid + "的商品库存不足。");
         return "商品编号为:" + productid + "的商品库存不足。";
            }
        }finally {
                //释放锁资源
                redisTemplate.delete("product::"+productid);
            }
        }else{
            //休眠100毫秒 在继续抢锁
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            return decrement(productid);
        }
    }

再次压测

在这里插入图片描述

上述发现并没有超卖重卖,,但是

锁超时问题
这里有一个问题,如果获取到锁的服务在执行方法体(释放锁的时候)宕机了,那锁不就释放不了么,别的服务也就没办法获取到锁,就造成了死锁。

使用redisson

在执行方法体的时候锁的时间到了或者宕机,watch dog会检测持有锁的进程给其增加锁时间(大约3次如果还没执行完或者宕机,该线程会结束)可以自动删除锁,别的服务就可以获取锁了,

引入依赖

    <!--ression依赖-->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.24.3</version>
        </dependency>

设置配置文件

@Configuration
public class RedissonConfig {

    @Bean
    public RedissonClient redisson(){
        Config config = new Config();
//        //连接的为redis集群
//        config.useClusterServers()
//                // use "rediss://" for SSL connection
//                .addNodeAddress("redis://127.0.0.1:7181","","","")
//        ;
        //连接单机
        config.useSingleServer().setAddress("redis://192.168.111.188:6379");
        RedissonClient redisson = Redisson.create(config);
        return redisson;
    }
}

修改原有代码

 @Autowired
  private RedissonClient redisson;

  public String decrement(Integer productid) {
      RLock lock = redisson.getLock("product::" + productid);
      lock.lock();
      try {
          int num = stockDao.findById(productid);
          if (num > 0) {
              //修改库存
         stockDao.update(productid);
         System.out.println("商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个");
         return "商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个";
          } else {
              System.out.println("商品编号为:" + productid + "的商品库存不足。");
              return "商品编号为:" + productid + "的商品库存不足。";
          }
      } finally {
          lock.unlock();
      }
   }

可以解决该问题。

  • 27
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值