springBoot对jedis的整合and顶替登录续租和缓存查询的后台代码功能实现案例

1.springBoot对jedis的整合:

springBoot对jedis做了很好的整合帮我们把槽道这一类算法已经给封装了起来,我们需要的仅仅是学会使用他的api即可

第一步:在pom文件中使用依赖:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-redis</artifactId>
    </dependency>

第二步:springBoot自动配置需要一些参数,根据redis的结构不同,可以去application中配置不同的参数:

  • 单节点redis:
    spring.redis.host=ip地址
    spring.redis.port=端口

  • 哨兵集群:
    spring.redis.sentinel.master=主从代号
    spring.redis.sentinel.nodes=哨兵节点信息 ip1:port1,ip2,port2

  • 集群结构:
    spring.redis.cluster.nodes=若干个集群节点信息 ip1:port1,ip2:port2

  • 不同结构都共享连接池属性配置:
    spring.redis.pool.maxActive=最大连接
    spring.redis.pool.maxIdle=最大空闲
    spring.redis.pool.minIdle=最小空闲

这里的测试案例我们使用的是cluster结构,所以配置如下:

spring.redis.cluster.nodes=10.9.100.26:8000,10.9.100.26:8001,10.9.100.26:8002

第三步:就可以开始写java代码来使用redis了!
声明一个

    @Autowired
    private StringRedisTemplate template;

因为redis中又五种数据格式,所以template对应着5种操作redis数据的方式:

        //查询redis中的key
        template.hasKey(KeyName);
        //String
        ValueOperations<String, String> StringOps = template.opsForValue();
        StringOps.set(key, UUID.randomUUID().toString());
        
        //hash
        HashOperations<String, Object, Object> hashOps = template.opsForHash();
        //list
        ListOperations<String, String> listOps = template.opsForList();
        //set opsForSet
        //zset opsForZset

2.顶替登录

redis可以解决微服务中的session共享问题,原因看我上一篇微博,这里我们就使用stringredistemplate和一个redis的cluster结构来实现这个功能:

扫描二维码关注公众号,回复: 11325655 查看本文章

首先我们需要一个思路来解决这个问题:账号的登录状态从什么地方获取?存储在什么地方?怎么判断用户是否登录?
答: 当用户第一次登录,他的登录状态会以key-value的形式保存在redis中,并且以cookie的形式返回一份给当前浏览器,当这个key在redis中没有被删除,我们就认为用户是登录状态。当用户使用另外一个浏览器进行登录,这个时候我们就会获取到上一个浏览器的登录key,通过这个key把它在redis中删除,旧浏览器再去拿登录信息的时候,就会发现拿到的是一个null,这样就退出了登录状态。

   public String doLogin(User user) {
        user.setUserPassword(MD5Util.md5(user.getUserPassword()));
         //数据库中查找此人账号密码是否正确
        User existUser=userMapper.selectUserByNameAndPw(user);

        if(existUser==null){
            return "";
        }else {
            //存在user 先解决登录顶替的问题
            //生成一个和userName有关的key
            String userLoginKey="user_login_"+user.getUserName();

            //判断userLoginKey是否存在,存在则说明有相同用户曾经登录过
            if(template.hasKey(userLoginKey)){
                //说明有人登陆过,相同用户,把上次ticket获取删除
                //获取value值就是上次的ticket
                String oldTicket=template.opsForValue().get(userLoginKey);
                template.delete(oldTicket);
            }

            String ticket="EM_TICKET_"+user.getUserName()+System.currentTimeMillis();
            try{
                existUser.setUserPassword("");
                String userJson = mapper.writeValueAsString(existUser);
                //第一次登录时在redis中存入ticket和userjson
                template.opsForValue().set(ticket,userJson,2, TimeUnit.HOURS);
                //为后续登录顶替我做准备
                template.opsForValue().set(userLoginKey,ticket,2,TimeUnit.HOURS);
                return ticket;
            }catch(Exception e){
                e.printStackTrace();
                return "";
            }
        }
    }

这里需要注意的是,每次登录的时候,都会set两个key-value到redis中,一个是用来记录这次登录的key-value,另外一个是用来记录这次登录的key,这样顶替登录的时候,就可以找到上一次登录的key,用这个key去redis中做删除操作。

关于登录的续租问题: 我们在redis中的key-value时间总是固定的,超时就会被销毁,销毁用户就会退出登录,为了提高用户的需求,我们就需要给用户的key-value做一个超时续租。具体原理就是,每次浏览器刷新的时候,都会拿着自己cookie中的key,去后台找redis,看看自己还能活多长时间,如果活不了多久了,就赶紧叫redis给自己增加寿命。

前端传递过来带参数的请求链接,这是一个rest风格,可以从链接中获取参数,拿着这个参数去调用service层

    @RequestMapping("/query/{ticket}")
    public SysResult queryTicket(@PathVariable String ticket){

        String userJson=userService.queryTicket(ticket);

        if(userJson==null||"".equals(userJson)){
            return SysResult.build(201,"超时",null);
        }else {
            return SysResult.build(200,"ok",userJson);
        }
    }

service层:
service层拿着这个key值(ticket),就去redis中找,找得到就进行一系列操作,找不到就retrun null

    @Autowired
    private StringRedisTemplate template;
//续租时间
    public String queryTicket(String ticket) {
        Long leftTime=template.getExpire(ticket,TimeUnit.SECONDS);
        if(leftTime>0&&leftTime<60*60){
            //给第一个set到redis的key-value延长寿命
            template.expire(ticket,60*60*2,TimeUnit.SECONDS);
            String userName=ticket.substring(9,(ticket.length()-13));
            String userLoginKey="user_login_"+userName;
            //给第二个set到redis的key-value延长寿命
            template.expire(userLoginKey,60*60*2,TimeUnit.SECONDS);
        }
        return template.opsForValue().get(ticket);
    }

注意这里如果要续租,要给两个redis中的key-value续租,不然顶替登录就会出现找不到上一个登录浏览器的key值情况,从而导致顶替登录失败。

3.缓存查询:

思路: 当查询商品的时候,先到缓存中进行查询,如果有则直接返回,没有的话就去数据库中查,查到了就去缓存中保存一份,然后返回查询结果,没有查到就返回null。

   @Autowired
    private ProductMapper productMapper;
    //json和对象互相转换的序列化
    ObjectMapper mapper=new ObjectMapper();
    //调用redis的对象
    @Autowired
    private StringRedisTemplate template;

 public Product queryOneProduct(String productId) {
        //被动缓存
        //判断缓存中有没有当前商品的数据key-value 有则直接使用无则到数据库查询
        //存放缓存一份
        String productKey="product_"+productId;
        //缓存中查到了数据
        if(template.hasKey(productKey)){
            //获取缓存json 
            String pJson = template.opsForValue().get(productKey);
            try{
            //反序列化成product
                return mapper.readValue(pJson,Product.class);
            }catch (Exception e){
                e.printStackTrace();
                return null;
            }
        }else{
            //缓存没有数据,查询数据库
            Product product = productMapper.selectProductById(productId);
            //存放到缓存一份 key=productKey value=pJson
            try{
            //把对象转化为json,存入缓存中
                String pJson = mapper.writeValueAsString(product);
                template.opsForValue().set(productKey,pJson,2, TimeUnit.DAYS);
            }catch (Exception e ){
                e.printStackTrace();
                return null;
            }
            return product;
        }
    }

缓存查询的数据一致性问题: 当给查询加入了缓存逻辑后,就会出现一个缓存一致性问题,即缓存数据要和DB中的数据一致。例如:当你查询一条数据,发现缓存中有,于是就去了缓存中查询,但是在DB中已经被修改了,你所查出来的数据就不一致。
解决办法:当使用更新数据功能的时候加上一个缓存锁,用人话来说就是,当你使用更新数据功能的时候,把你修改的商品id存入redis中,当你修改过程中有人去缓存中查这个商品的时候,我们就先判断一下,这个商品上有没有锁,如果有就去DB中查询(需要注意,更新商品数据完成后,要把在缓存中旧数据删除)

更新代码:

public void editProduct(Product product) {
        //保证1 2 同步执行完整
        //添加一个内存锁,被动缓存中,只要发现这个内存锁,说明有人在更细年数据
        //不会使用缓存
        String lock="product_"+product.getProductId()+".lock";
        Boolean ok = template.opsForValue().setIfAbsent(lock, "");//set NX
        if(ok){//设置锁成功
            //删除旧缓存
            template.delete("product_"+product.getProductId());
            //执行数据库更新
            productMapper.updateProductById(product);
        }else{
            throw new RuntimeException("更新缓存发现有冲突,有人正在更新");
        }
        //释放锁
        template.delete(lock);
    }

查询代码:

public Product fqueryOneProduct(String productId) {
        //判断锁是否存在
        String lock="product_"+productId+".lock";
        if(template.hasKey(lock)){
            return productMapper.selectProductById(productId);
        }
        //被动缓存
        //判断缓存中有没有当前商品的数据key-value 有则直接使用无则到数据库查询
        //存放缓存一份
        String productKey="product_"+productId;
        if(template.hasKey(productKey)){
            //获取缓存json 反序列化成product
            String pJson = template.opsForValue().get(productKey);
            try{
                return mapper.readValue(pJson,Product.class);
            }catch (Exception e){
                e.printStackTrace();
                return null;
            }
        }else{
            //缓存没有数据,查询数据库
            Product product = productMapper.selectProductById(productId);
            //存放到缓存一份 key=productKey value=pJson
            try{
                String pJson = mapper.writeValueAsString(product);
                template.opsForValue().set(productKey,pJson,2, TimeUnit.DAYS);
            }catch (Exception e ){
                e.printStackTrace();
                return null;
            }
            return product;
        }
    }

猜你喜欢

转载自blog.csdn.net/weixin_42596778/article/details/106480893
今日推荐