Redis数据结构Hash应用场景-存储商品、购物车、淘宝短链接、分布式Session、用户注册、发微博功能

Hash

  1. redis的hash数据结构,其实就是string的升级版,它把string 数据结构的key value,中的value类型升级为hash(和java的hash一样的结构)
    Map<String, HashMap<String,String>> hash=new HashMap<String,HashMap<String,String>>();
  2. 每个 hash的存储大小: 可以存储 2的(32 - 1)方的 键值对(40多亿)

Hash应用场景

本质上是存储java对象

  • HSET key field value
    将哈希表 key 中的字段 field 的值设为 value 。
  • HGET key field
    获取存储在哈希表中指定字段的值。
127.0.0.1:6379> hset product:100 name iphone11
(integer) 1
127.0.0.1:6379> hget product:100 name
"iphone11"
  • HMSET key field1 value1 [field2 value2 ]
    同时将多个 field-value (域-值)对设置到哈希表 key 中。
  • HMGET key field1 [field2 field3 …]
    获取所有给定字段的值
127.0.0.1:6379> hmset product:100 price 5000 detail "I love iphone"
OK
127.0.0.1:6379> hmget product:100 name price detail
1) "iphone11"
2) "5000"
3) "I love iphone"
  • HKEYS key
    获取指定hash中所有field值
127.0.0.1:6379> hkeys product:100
1) "name"
2) "price"
3) "detail"
  • HVALS key
    获取指定hash中所有value值
127.0.0.1:6379> hvals product:100
1) "iphone11"
2) "5000"
3) "I love iphone"
  • HGETALL key
    获取指定hash中所有field、value值
127.0.0.1:6379> hgetall product:100
1) "name"
2) "iphone11"
3) "price"
4) "5000"
5) "detail"
6) "I love iphone"
  • HLEN key
    获取指定hash中元素的个数
127.0.0.1:6379> hlen product:100
(integer) 3
  • HINCRBY key field data (整形)
    给指定 field 对应的 value 值加上 data 数值
  • HINCRBYFLOAT key field data(浮点数)
    给指定 field 对应的 value 值加上 data 数值
127.0.0.1:6379> hincrby product:100 price 100
(integer) 5100
127.0.0.1:6379> hgetall product:100
1) "name"
2) "iphone11"
3) "price"
4) "5100"
5) "detail"
6) "I love iphone"
  • HEXISTS key field
    检查指定的field是否存在
127.0.0.1:6379> hexists product:100 name
(integer) 1
  • HDEL key field1 [field2 fiedl3 …]
    删除一个或多个哈希表字段
127.0.0.1:6379> hdel product:100 name
(integer) 1
127.0.0.1:6379> hgetall product:100
1) "price"
2) "5100"
3) "detail"
4) "I love iphone"

redis存储java对象常用String,那为什么还要用hash来存储?

Redis存储java对象,一般是String 或 Hash 两种,那到底什么时候用String ? 什么时候用hash ?

String的存储通常用在频繁读操作,它的存储格式是json,即把java对象转换为json,然后存入redis.
Hash的存储场景应用在频繁写操作,即,当对象的某个属性频繁修改时,不适用string+json的数据结构,因为不灵活,每次修改都需要把整个对象转换为json存储。
如果采用hash,就可以针对某个属性单独修改,不用序列号去修改整个对象。例如,商品的库存、价格、关注数、评价数经常变动时,就使用存储hash结果。

SpringBoot+redis+hash存储商品数据

  • maven依赖

        <!--swagger-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <!--swagger-ui-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>



        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <version>2.4.4</version>
        </dependency>
  • 配置文件
logging.level.com.agan=debug

spring.swagger2.enabled=true

spring.redis.database=0
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=
  • Redis配置
/**
 * @author qh
 */
@Configuration
public class RedisConfiguration {
    /**
     * 重写Redis序列化方式,使用Json方式:
     * 当我们的数据存储到Redis的时候,我们的键(key)和值(value)都是通过Spring提供的Serializer序列化到Redis的。
     * RedisTemplate默认使用的是JdkSerializationRedisSerializer,
     * StringRedisTemplate默认使用的是StringRedisSerializer。
     *
     * Spring Data JPA为我们提供了下面的Serializer:
     * GenericToStringSerializer、Jackson2JsonRedisSerializer、
     * JacksonJsonRedisSerializer、JdkSerializationRedisSerializer、
     * OxmSerializer、StringRedisSerializer。
     * 在此我们将自己配置RedisTemplate并定义Serializer。
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        //创建一个json的序列化对象
        GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
        //设置value的序列化方式json
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        //设置key序列化方式string
        redisTemplate.setKeySerializer(new StringRedisSerializer());

        //设置hash key序列化方式string
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        //设置hash value的序列化方式json
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}
  • Swagger配置
@Configuration
@EnableSwagger2
public class SwaggerConfig {

    @Value(value = "${spring.swagger2.enabled}")
    private Boolean swaggerEnabled;

    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .enable(swaggerEnabled)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.redis.hash"))
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("接口文档")
                .description("阿甘讲解 Spring Boot")
                .termsOfServiceUrl("https://study.163.com/provider/1016671292/index.htm")
                .version("1.0")
                .build();
    }
}

Redis和Swagger配置一般都是不变的,如果创建新项目,需要注意Swagger的apis路径;

  • controller

@Api(description = "接口")
@Slf4j
@RestController
public class ProductController {
    @Autowired
    public RedisTemplate redisTemplate;
    @ApiOperation("保存")
    @RequestMapping("save")
    public Object save(Product product){
        String key="product::"+product.getId();
        HashMap<String , Object> map=new HashMap<>();
        map.put("productName",product.getProductName());
        map.put("productPrice",product.getProductPrice());
        map.put("productNum",product.getProductNum());
        //TODO : 先插入DB、再插入Redis缓存,这里省略DB过程
        redisTemplate.opsForHash().putAll(key,map);
        return "OK";
    }
    @ApiOperation("更新")
    @RequestMapping("updateNum")
    public Object updateNum(Integer productId, Integer num){
        String key="product::"+productId;

        //TODO : 先更新DB、再更新Redis缓存,这里省略DB过程
        redisTemplate.opsForHash().increment(key,"productNum",num);
        return "OK";
    }

    @ApiOperation("获取所有")
    @GetMapping("getALl")
    public Object getAll(Integer id){
        List values = redisTemplate.opsForHash().values("product::" + id);
        return values;
    }
}


@Data
public class Product {
    private Long id;
    private String productName;
    private Integer productNum;
    private Integer productPrice;
}

  • 运行项目:打开localhost:8080/swagger-ui.html测试;
    插入数据后,打开redis-cli客户端
127.0.0.1:6379> keys *
1) "product:1000"
127.0.0.1:6379> hgetall product:1000
1) "price"
2) "2000"
3) "name"
4) "\"huawei\""
5) "id"
6) ""
7) "detail"
8) "\"www\""

短链接

场景1:淘宝短信

你们应该收到淘宝的短信

【天猫】有优惠啦!黄皮金煌芒果(水仙芒)带箱10斤49.8元!
核薄无丝很甜喔!购买: c.tb.cn/c.ZzhFZ0 急鲜丰 退订回TD

这个 c.tb.cn/c.ZzhFZ0 就是短链接;
打开IE,输入 c.tb.cn/c.ZzhFZ0 就转变为如下一大坨

https://h5.m.taobao.com/ecrm/jump-to-app.html?scm=20140608.2928562577.LT_ITEM.1699166744&target_url=
http%3A%2F%2Fh5.m.taobao.com%2Fawp%2Fcore%2Fdetail.htm%3Fid%3D567221004504%26scm=20140607.2928562577.
LT_ITEM.1699166744&spm=a313p.5.1cfl9ch.947174560063&short_name=c.ZzhFZ0&app=chrome
  • 短链接就是把普通网址,转换成比较短的网址。
  • 短链接有什么好处?
  • 节省网址长度,便于社交化传播。
  • 方便后台跟踪点击量、统计。

SpringBoot+Redis《短链接转换器》

《短链接转换器》的原理:

  1. 长链接转换为短链接
    实现原理:长链接转换为短链接加密串key,然后存储于redis的hash结构中。
  2. 重定向到原始的url
    实现原理:通过加密串key到redis找出原始url,然后重定向出去

代码

  • controller
@Api(description = "URL生成器")
@RestController
public class UrlController {
    @Autowired
    RedisTemplate redisTemplate;
    @Autowired
    ShortUrlGenerator shortUrlGenerator;

    private  final static  String SHORT_URL_KEY="product:short:url";
    @ApiOperation("生成URL")
    @PostMapping("/generate")
    public Object generate(String url){
        System.out.println(url);
        String shortUrl= shortUrlGenerator.generate(url);
        redisTemplate.opsForHash().put(SHORT_URL_KEY,shortUrl,url);
        return "127.0.0.1:8080/"+shortUrl;
    }

    @ApiOperation("重定向")
    @GetMapping("/{key}")
    public Object redirect(@PathVariable String key, HttpServletResponse response) throws IOException {
        String url = (String) redisTemplate.opsForHash().get(SHORT_URL_KEY, key);
        //TODO:重定向操作 response
         return url;
    }
}

  • service
    简单一点的作法就是对路径进行MD5加密,然后返回前8个字符就可以了;
@Service
public class ShortUrlGenerator {
    public String generate(String url) {
        String digestAsHex = DigestUtils.md5DigestAsHex(url.getBytes()).substring(0,8);
        return digestAsHex;
    }
}

复杂一点的作法:

public class RealUrlGenerator {
    //26+26+10=62
    public static  final  String[] chars = new String[]{"a", "b", "c", "d", "e", "f", "g", "h",
            "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t",
            "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5",
            "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H",
            "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T",
            "U", "V", "W", "X", "Y", "Z"};



    /**
     * 一个长链接URL转换为4个短KEY
     */
    public static String[] shortUrl(String url) {
        String key = "";
        //对地址进行md5
        String sMD5EncryptResult = DigestUtils.md5Hex(key + url);
        System.out.println(sMD5EncryptResult);
        String hex = sMD5EncryptResult;
        String[] resUrl = new String[4];
        for (int i = 0; i < 4; i++) {
            //取出8位字符串,md5 32位,被切割为4组,每组8个字符
            String sTempSubString = hex.substring(i * 8, i * 8 + 8);

            //先转换为16进账,然后用0x3FFFFFFF进行位与运算,目的是格式化截取前30位
            long lHexLong = 0x3FFFFFFF & Long.parseLong(sTempSubString, 16);

            String outChars = "";
            for (int j = 0; j < 6; j++) {
                //0x0000003D代表什么意思?他的10进制是61,61代表chars数组长度62的0到61的坐标。
                //0x0000003D & lHexLong进行位与运算,就是格式化为6位,即61内的数字
                //保证了index绝对是61以内的值
                long index = 0x0000003D & lHexLong;

                outChars += chars[(int) index];
                //每次循环按位移5位,因为30位的二进制,分6次循环,即每次右移5位
                lHexLong = lHexLong >> 5;
            }

            // 把字符串存入对应索引的输出数组
            resUrl[i] = outChars;
        }
        return resUrl;
    }

}

购物车

登录淘宝后,逛淘宝时,点击商品加入购物车时,购物车中就会有一件对应的商品;
往购物车加入2件商品
采用hash数据结果,key=cart:user:用户id

127.0.0.1:6379> hset cart:user:1000 101 1
(integer) 1
127.0.0.1:6379> hset cart:user:1000 102 1
(integer) 1
127.0.0.1:6379> hgetall cart:user:1000
1) "101"
2) "1"
3) "102"
4) "1"

修改购物车的数据,为某件商品添加数量

127.0.0.1:6379> hincrby cart:user:1000 101 1
(integer) 2
127.0.0.1:6379> hincrby cart:user:1000 102 10
(integer) 11
127.0.0.1:6379> hgetall cart:user:1000
1) "101"
2) "2"
3) "102"
4) "11"

统计购物车有多少件商品

127.0.0.1:6379> hlen cart:user:1000
(integer) 2

删除购物车某件商品

127.0.0.1:6379> hdel cart:user:1000 102
(integer) 1
127.0.0.1:6379> hgetall cart:user:1000
1) "101"
2) "2"

SpringBoot+Redis模拟购物车

package com.redis.hash.cart.controller;

import io.swagger.annotations.Api;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

@Api
@RestController
@RequestMapping("cart")
public class TbCartController {
    @Autowired
    RedisTemplate redisTemplate;

    public final String carKey="car::user";
    @GetMapping("getCar")
    public Object get(Integer userId){
        String key=carKey+userId;
        Long size = redisTemplate.opsForHash().size(key);
        //TODO:可能会进行分页查询;
        Map entries = redisTemplate.opsForHash().entries(key);
        Set set = entries.keySet();
        StringBuilder psb=new StringBuilder();
        for (Object productId : set){
            psb.append("productId:"+productId);
            psb.append("; productNum:"+entries.get(productId));
        }
        return "size:"+size+" "+psb.toString();
    }
    @GetMapping("addCar")
    public Object add(Integer userId,Integer productId,Integer num){
        //TODO :通常情况下通过Session获取用户信息;
        String key=carKey+userId;
        Boolean hasKey = redisTemplate.opsForHash().getOperations().hasKey(key);
        if (hasKey){
            redisTemplate.opsForHash().put(key,productId+"",num);
        }else {
        //如果redis没有用户的购物车,则进行初始化,并设置过期时间。
            redisTemplate.opsForHash().put(key,productId+"",num);
            redisTemplate.expire(key,90, TimeUnit.DAYS);
        }
        return "添加成功";
    }
    @GetMapping("updateProductNum")
    public Object updateNum(Integer userId,Integer productId,Integer num){
        String key=carKey+userId;
        redisTemplate.opsForHash().put(key,productId+"",num);
        return "更新成功,值为:"+redisTemplate.opsForHash().get(key,productId+"");
    }
    @GetMapping("deleteProduct")
    public Object deleteProduct(Integer userId,Integer productId){
        String key=carKey+userId;
        redisTemplate.opsForHash().delete(key,productId+"");
        return "已经删除";
    }
}

  • 用swagger体验http://127.0.0.1:9090/swagger-ui.html#/

京东购物车

京东在未登录的情况下,用户点击商品加入购物车后,购物车中自动就有了商品的信息,当用户退出重新进入网站后,再次点开购物车,商品还是存在的;
也就是京东网站使用Cookie机制为未登录的用户提供一个购物车ID;
当用户登录后,会将未登录时的购物车与登录后的购物车进行合并;


@Api
@RestController
@RequestMapping("/jd")
public class JdCartController {
    @Autowired
    RedisTemplate redisTemplate;

    @Autowired
    IdGenerator idGenerator;
    @Autowired
    HttpServletRequest request;
    @Resource
    HttpServletResponse response;

    public final static String commonKey="car::cookie";
    public final static String userCar="car::user";


    @GetMapping("/mergeCar")
    public Object merge(Integer userId,Integer product){
        if (userId==null){
            return "ok";
        }

        String carId = this.getCookieId(userId);
        String carKey=commonKey+carId;
        Map cookeCar = redisTemplate.opsForHash().entries(carKey);

        String userCarKey=userCar+userId;
        Boolean hasKey = redisTemplate.opsForHash().getOperations().hasKey(userCarKey);

        redisTemplate.opsForHash().putAll(userCarKey,cookeCar);

        //第三步:删除redis未登录的用户cookies的购物车数据
        this.redisTemplate.delete(carKey);

        //第四步:删除未登录用户cookies的cartid
        Cookie cookie=new Cookie("carId",null);
        cookie.setMaxAge(0);
        response.addCookie(cookie);
        return "merge OK";
    }
    @GetMapping("/addCar")
    public Object add(Integer userId,Integer productId,Integer num){
        //TODO :通常情况下通过Session获取用户信息;
        String carId=this.getCookieId(userId);
        String key=commonKey+carId;
        Boolean hasKey = redisTemplate.opsForHash().getOperations().hasKey(key);
        if (hasKey){
            redisTemplate.opsForHash().put(key,productId+"",num);
        }else {
            redisTemplate.opsForHash().put(key,productId+"",num);
            redisTemplate.expire(key,90, TimeUnit.DAYS);
        }

        return "添加成功";
    }



    @GetMapping("/updateProductNum")
    public Object updateNum(Integer userId,Integer productId,Integer num){
        String carId = this.getCookieId(userId);
        String key=commonKey+carId;
        redisTemplate.opsForHash().put(key,productId.toString(),num);
        return "更新成功,值为:"+redisTemplate.opsForHash().get(key,productId.toString());
    }
    @GetMapping("/deleteProduct")
    public Object deleteProduct(Integer userId,Integer productId){
        String carId = this.getCookieId(userId);
        String key=commonKey+carId;
        redisTemplate.opsForHash().delete(key,productId.toString());
        return "已经删除";
    }
    @GetMapping("/getCar")
    public Object get(Integer userId){
        String carId = this.getCookieId(userId);

        String key=commonKey+carId;
        Long size = redisTemplate.opsForHash().size(key);
        //TODO:可能会进行分页查询;
        Map entries = redisTemplate.opsForHash().entries(key);
        Set set = entries.keySet();
        StringBuilder psb=new StringBuilder();
        for (Object productId : set){
            psb.append("productId:"+productId);
            psb.append("; productNum:"+entries.get(productId));
        }
        return "size:"+size+" "+psb.toString();
    }
    private String getCookieId(Integer userId) {
        Cookie[] cookies = request.getCookies();
        if (cookies!=null){
            for (Cookie cookie : cookies){
                if (cookie.getName().equals("carId")){
                    String carID=cookie.getValue();
                    return carID;
                }
            }
        }
        String carId=idGenerator.generateId("carId");
        Cookie cookie=new Cookie("carId",carId);
        response.addCookie(cookie);
        return carId;
    }

}



@Service
public class IdGenerator {
    @Autowired
    RedisTemplate redisTemplate;

    public String generateId(String commonKey) {
        Long carId = redisTemplate.opsForValue().increment(commonKey);
        return carId.toString();
    }
}

分布式Session

一、Session有什么作用?

  • Session 是客户端与服务器通讯会话跟踪技术,服务器与客户端保持整个通讯的会话基本信息。
  • 客户端在第一次访问服务端的时候,服务端会响应一个sessionId并且将它存入到本地cookie中,
  • 在之后的访问会将cookie中的sessionId放入到请求头中去访问服务器,
    如果通过这个sessionid没有找到对应的数据,那么服务器会创建一个新的sessionid并且响应给客户端。

二、Springboot实现用户登录session管理功能

核心代码

session.setAttribute(session.getId(), user);
session.removeAttribute(session.getId());

@Slf4j
@RestController
@RequestMapping(value = "/user")
public class UserController {

    Map<String, User> userMap = new HashMap<>();

    public UserController() {
        //初始化2个用户,用于模拟登录
        User u1=new User(1,"agan1","agan1");
        userMap.put("agan1",u1);
        User u2=new User(2,"agan2","agan2");
        userMap.put("agan2",u2);
    }

    @GetMapping(value = "/login")
    public String login(String username, String password, HttpSession session) {
        //模拟数据库的查找
        User user = this.userMap.get(username);
        if (user != null) {
            if (!password.equals(user.getPassword())) {
                return "用户名或密码错误!!!";
            } else {
                session.setAttribute(session.getId(), user);
                log.info("登录成功{}",user);
            }
        } else {
            return "用户名或密码错误!!!";
        }
        return "登录成功!!!";
    }

    /**
     * 通过用户名查找用户
     */
    @GetMapping(value = "/find/{username}")
    public User find(@PathVariable String username) {
        User user=this.userMap.get(username);
        log.info("通过用户名={},查找出用户{}",username,user);
        return user;
    }

    /**
     *拿当前用户的session
     */
    @GetMapping(value = "/session")
    public String session(HttpSession session) {
        log.info("当前用户的session={}",session.getId());
        return session.getId();
    }

    /**
     * 退出登录
     */
    @GetMapping(value = "/logout")
    public String logout(HttpSession session) {
        log.info("退出登录session={}",session.getId());
        session.removeAttribute(session.getId());
        return "成功退出!!";
    }

}
//编写session拦截器
//session拦截器的作用:
//验证当前用户发来的请求是否有携带sessionid,如果没有携带,提示用户重新登录。
@Slf4j
@Configuration
public class SessionInterceptor implements WebMvcConfigurer{
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new RegisterInterceptor()).excludePathPatterns("/temp/login")
                .excludePathPatterns("/temp/logout").addPathPatterns("/**");
    }
    @Configuration
    public class RegisterInterceptor implements  HandlerInterceptor{
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            HttpSession session = request.getSession();
            String username = request.getParameter("username");
            Object user = session.getAttribute(username);
            if (user!=null){
                log.info("session拦截器,session={},验证通过",session.getId());
                return true;
            }

            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json; charset=utf-8");
            response.getWriter().write("请登录!!!!!");
            log.info("session拦截器,session={},验证失败",session.getId());
            return false;
        }
    }
}


三、存在的问题

单机服务器情况下,这种方式是没有问题的、一旦到了分布式集群,就会存在多个服务器;
1. 用户第一次访问Nginx,请求落到了服务器A,服务器A生成了一个sessionId,并保存在用户的cookie中,同时用户的session放在服务器A中;
2. 用户第二次再来访问Nginx,它这次把cookie里面的sessionId加入http的请求头中,这时请求落到了服务器B,服务器B发现没有找到sessionId,于是创建了一个新的sessionId并保存在用户的cookie中。同时用户的Session放在服务器B中;

以上2个步骤,在分布式系统中,必将导致session错乱。

四、SpringSession+redis解决分布式session不一致性问题

  • maven依赖
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
  • 修改配置文件
# 应用名称
spring.application.name=session

# 应用服务 WEB 访问端口
server.port=9090



logging.level.com.agan=debug

spring.swagger2.enabled=true

spring.redis.database=0
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=


# 设置session的存储方式,采用redis存储
spring.session.store-type=redis

# session有效时长为10分钟
server.servlet.session.timeout=PT10M

这就完成分布式Session不一致的问题、即将用户Session信息放入到Redis;

五、分析SpringSession的redis数据结构

  • 用户登录后,会自动将用户Session插入到Redis中;
127.0.0.1:6379> keys *
1) "spring:session:expirations:1578227700000"
2) "spring:session:sessions:5eddb9a3-5b1e-4bdd-a289-394b6d42388e"
3) "spring:session:sessions:expires:5eddb9a3-5b1e-4bdd-a289-394b6d42388e"
  • 共同点:3个key都是以spring:session:开头的,代表了SpringSession的redis数据。

  • 查看"spring:session:sessions:5eddb9a3-5b1e-4bdd-a289-394b6d42388e"类型:

127.0.0.1:6379> type "spring:session:sessions:5eddb9a3-5b1e-4bdd-a289-394b6d42388e"
hash
  • 获取hash的内容
127.0.0.1:6379> hgetall "spring:session:sessions:5eddb9a3-5b1e-4bdd-a289-394b6d42388e"

//失效时间 100分钟
1) "maxInactiveInterval"
2) "\xac\xed\x00\x05sr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x17p"

// sesson的属性,存储了user对象
3) "sessionAttr:5eddb9a3-5b1e-4bdd-a289-394b6d42388e"
4) "\xac\xed\x00\x05sr\x00\x1ecom.agan.redis.controller.User\x16\"_m\x1b\xa0W\x7f\x02\x00\x03I\x00\x02idL\x00\bpasswordt\x00\x12Ljava/lang/String;L\x00\busernameq\x00~\x00\x01xp\x00\x00\x00\x01t\x00\x05agan1q\x00~\x00\x03"

// session的创建时间
5) "creationTime"
6) "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01ouW<K"

//最后的访问时间
7) "lastAccessedTime"
8) "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01ouW<L"

六、分析SpringSession的redis过期策略

对于过期数据,一般有三种删除策略:

  1. 定时删除,即在设置键的过期时间的同时,创建一个定时器, 当键的过期时间到来时,立即删除。
  2. 惰性删除,即在访问键的时候,判断键是否过期,过期则删除,否则返回该键值。
  3. 定期删除,即每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。
    ​redis 删除过期数据采用的是懒性删除+定期删除组合策略,也就是数据过期了并不会及时被删除。
    但由于redis是单线程,并且redis对删除过期的key优先级很低;如果有大量的过期key,就会出现key已经过期但是未删除。

为了实现 session 过期的及时性,spring session 采用了定时删除+惰性删除的策略。
“spring:session:expirations:1578227700000”

127.0.0.1:6379> type "spring:session:expirations:1578228240000"
set
127.0.0.1:6379> smembers "spring:session:expirations:1578228240000"
1) "\xac\xed\x00\x05t\x00,expires:5eddb9a3-5b1e-4bdd-a289-394b6d42388e"

springsession 定时(1分钟)轮询,删除spring:session:expirations:[?] 的过期members
例如:spring:session:expirations:1578228240000 的1578228240000=2020-01-05 20:44:00:000 即在2020-01-05 20:44:00:000过期。
springsesion 定时检测超过2020-01-05 20:44:00:000 就删除spring:session:expirations:1578228240000的members的值
sessionId=5eddb9a3-5b1e-4bdd-a289-394b6d42388e
即删除

1) "spring:session:expirations:1578228240000"
2) "spring:session:sessions:5eddb9a3-5b1e-4bdd-a289-394b6d42388e"
3) "spring:session:sessions:expires:5eddb9a3-5b1e-4bdd-a289-394b6d42388e"

惰性删除
spring:session:sessions:expires:5eddb9a3-5b1e-4bdd-a289-394b6d42388e

127.0.0.1:6379> type spring:session:sessions:expires:5eddb9a3-5b1e-4bdd-a289-394b6d42388e
string
127.0.0.1:6379> get spring:session:sessions:expires:5eddb9a3-5b1e-4bdd-a289-394b6d42388e
""
127.0.0.1:6379> ttl spring:session:sessions:expires:5eddb9a3-5b1e-4bdd-a289-394b6d42388e
(integer) 4719

访问 spring:session:sessions:expires:5eddb9a3-5b1e-4bdd-a289-394b6d42388e的时候,判断key是否过期,过期则删除,否则返回改进的值。
例如 访问spring:session:sessions:expires:5eddb9a3-5b1e-4bdd-a289-394b6d42388e的时候
判断 ttl spring:session:sessions:expires:5eddb9a3-5b1e-4bdd-a289-394b6d42388e是否过期,过期就直接删除

1) "spring:session:expirations:1578228240000"
2) "spring:session:sessions:5eddb9a3-5b1e-4bdd-a289-394b6d42388e"
3) "spring:session:sessions:expires:5eddb9a3-5b1e-4bdd-a289-394b6d42388e"

七、 为什么引入Maven依赖后,无感知的情况下,用户Session就进入了Redis?

个人查看了Spring-Session的源码、内部重新定义了Request,将我们传统情况下的request换成了SpringSession的request,SpringSession的Request内部替换了原来的session,重写了setAttribute()方法;在调用session.setAttribute(key,value)的时候,本质上是执行SpringSession的setAttribute(key,value),他在原来的操作后面,加入插入redis的操作;

注册

Redis在互联网公司中是必选的技术,因为互联网公司的系统天生就是高并发特征。但是能把redis运用的最好的就属微博了。
Redis技术基本覆盖了微博的每个应用场景,比如像现在春晚必争的“红包飞”活动,还有像粉丝数、用户数、阅读数、转评赞、评论盖楼、广告推荐、负反馈、音乐榜单等等都有用到 Redis。
正因为Redis的广泛应用,使得微博能够快速支撑日活跃用户超2亿,每日访问量百亿级,历史数据高达千亿级。
微博线上规模,100T+ 存储,1000+ 台物理机,10000+Redis 实例

SpringBoot+Redis模拟注册

  • 步骤1:创建user表
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `username` varchar(50) NOT NULL DEFAULT '' COMMENT '用户名',
  `password` varchar(50) NOT NULL DEFAULT '' COMMENT '密码',
  `sex` tinyint(4) NOT NULL DEFAULT '0' COMMENT '性别 0=女 1=男 ',
  `deleted` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '删除标志,默认0不删除,1删除',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='用户表';

SET FOREIGN_KEY_CHECKS = 1;

  • 步骤2:注册逻辑
@Api(description = "用户接口")
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;


    @ApiOperation(value="微博注册")
    @PostMapping(value = "/createUser")
    public void createUser(@RequestBody UserVO userVO) {
        User user=new User();
        BeanUtils.copyProperties(userVO,user);
        userService.createUser(user);
    }
}



    /**
     * 微博注册
     */
    public void createUser(User obj) {
        //步骤1:先入库
        this.userMapper.insertSelective(obj);
        //步骤2:入库成功后,写入redis
        obj = this.userMapper.selectByPrimaryKey(obj.getId());
        //将Object对象里面的属性和值转化成Map对象
        Map<String, Object> map = ObjectUtil.objectToMap(obj);
        //设置缓存key
        String key = Constants.CACHE_KEY_USER + obj.getId();

        //微博用户的存储采用reids的hash
        HashOperations<String, String, Object> opsForHash = redisTemplate.opsForHash();
        opsForHash.putAll(key, map);

        //步骤3:设置过期30天
        this.redisTemplate.expire(key, 30, TimeUnit.DAYS);
    }
  }

SpringBoot+Redis 实现用户发微博、帖子

  • 步骤1:创建content表
CREATE TABLE `content` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `user_id` int(10) NOT NULL DEFAULT '0' COMMENT '用户id',
  `content` varchar(5000) NOT NULL DEFAULT '' COMMENT '内容',
  `deleted` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '删除标志,默认0不删除,1删除',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='内容表';
  • controller
@Api
@RestController
@RequestMapping("/content")
public class ContentController {
    @Autowired
    ContentService contentService;

    @PostMapping("/save")
    public Object addContent(@RequestBody Content content){
        return contentService.saveContent(content);
    }
    @PostMapping("get")
    public Object getContent(@RequestBody String id){
        return contentService.getContent(id);
    }
}

  • service
@Service
public class ContentService {

    @Autowired
    HttpSession session;

    @Resource
    ContentMapper contentMapper;

    @Autowired
    RedisTemplate redisTemplate;

    public final String contentKey="content::";

    @Transactional
    public Object saveContent(Content content) {
        User user = (User) session.getAttribute("user");
        if (user==null) {
            return "请先登录";
        }

        //先DB 再Redsi
        content.setUserId(user.getId());
        contentMapper.insert(content);
        String key=contentKey+content.getId();

        HashMap<Object, Object> contentMap = new HashMap<>();
        contentMap.put("content",content.getContent());
        contentMap.put("userId",content.getUserId());

        redisTemplate.opsForHash().putAll(key,contentMap);
        return "发表成功";
    }

    public Object getContent(String id) {
        User user = (User) session.getAttribute("user");
        if (user==null) {
            return "请先登录";
        }

        String key=contentKey+id;
        Map map = redisTemplate.opsForHash().entries(key);
        if (map != null){
            return  map;
        }else {
            Content content = contentMapper.selectByPrimaryKey(id);
            map = new HashMap<>();
            map.put("content",content.getContent());
            map.put("userId",content.getUserId());
            redisTemplate.opsForHash().putAll(key,map);
        }
        return map;
    }
}

总结

  • Redis的hash结构更适合存储写频率高的Java对象。
  • Redis只能对key进行设置过期时间,不能对key的field设置过期时间,这是值得注意的点;
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值