Hash应用场景

  • Hash
    • Hash应用场景
    • redis存储java对象常用String,那为什么还要用hash来存储?
    • SpringBoot+redis+hash存储商品数据
    • 短链接
        • 场景1:淘宝短信
      • SpringBoot+Redis《短链接转换器》
      • 代码
    • 购物车
    • SpringBoot+Redis模拟购物车
    • 京东购物车
    • 分布式Session
      • 一、Session有什么作用?
      • 二、Springboot实现用户登录session管理功能
      • 三、存在的问题
      • 四、SpringSession+redis解决分布式session不一致性问题
      • 五、分析SpringSession的redis数据结构
      • 六、分析SpringSession的redis过期策略
      • 七、 为什么引入Maven依赖后,无感知的情况下,用户Session就进入了Redis?
    • 注册
      • SpringBoot+Redis模拟注册
    • SpringBoot+Redis 实现用户发微博、帖子
  • 总结

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=debugspring.swagger2.enabled=truespring.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。*/@Beanpublic RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();redisTemplate.setConnectionFactory(redisConnectionFactory);//创建一个json的序列化对象GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();//设置value的序列化方式jsonredisTemplate.setValueSerializer(jackson2JsonRedisSerializer);//设置key序列化方式stringredisTemplate.setKeySerializer(new StringRedisSerializer());//设置hash key序列化方式stringredisTemplate.setHashKeySerializer(new StringRedisSerializer());//设置hash value的序列化方式jsonredisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);redisTemplate.afterPropertiesSet();return redisTemplate;}
}
  • Swagger配置
@Configuration
@EnableSwagger2
public class SwaggerConfig {@Value(value = "${spring.swagger2.enabled}")private Boolean swaggerEnabled;@Beanpublic 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 {@Autowiredpublic 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 {@AutowiredRedisTemplate redisTemplate;@AutowiredShortUrlGenerator 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:重定向操作 responsereturn 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=62public 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 = "";//对地址进行md5String 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 {@AutowiredRedisTemplate 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 {@AutowiredRedisTemplate redisTemplate;@AutowiredIdGenerator idGenerator;@AutowiredHttpServletRequest request;@ResourceHttpServletResponse 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的cartidCookie 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 {@AutowiredRedisTemplate 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{@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new RegisterInterceptor()).excludePathPatterns("/temp/login").excludePathPatterns("/temp/logout").addPathPatterns("/**");}@Configurationpublic class RegisterInterceptor implements  HandlerInterceptor{@Overridepublic 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=9090logging.level.com.agan=debugspring.swagger2.enabled=truespring.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 {@Autowiredprivate 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:入库成功后,写入redisobj = this.userMapper.selectByPrimaryKey(obj.getId());//将Object对象里面的属性和值转化成Map对象Map<String, Object> map = ObjectUtil.objectToMap(obj);//设置缓存keyString key = Constants.CACHE_KEY_USER + obj.getId();//微博用户的存储采用reids的hashHashOperations<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 {@AutowiredContentService 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 {@AutowiredHttpSession session;@ResourceContentMapper contentMapper;@AutowiredRedisTemplate redisTemplate;public final String contentKey="content::";@Transactionalpublic Object saveContent(Content content) {User user = (User) session.getAttribute("user");if (user==null) {return "请先登录";}//先DB 再Redsicontent.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设置过期时间,这是值得注意的点;

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

  1. Redis数据结构Hash实战之淘宝短链接

    网址链接过长给用户不好的体验,缩短链接长度方便社交化传播,还能跟踪点击量和统计. 算法解析 生成a~z A~z 0~9的字符,后面有用 public static void main(String[] ...

  2. php根据淘宝短链接获取商品ID

    很多人在做淘宝客API开发时,不知道怎么根据短链接获取商品ID,其实很简单,废话不多说,直接开干 //根据复制出来的宝贝短链接,提取商品ID $cp_url = 你要解析的URL; $tao302 = ...

  3. 根据淘宝商品 num_iid 批量生成淘宝客链接的 PHP 函数

    根据淘宝商品 num_iid 批量生成淘宝客(什么是淘宝客?)链接的 PHP 文件内容如下. 淘宝 API 有调用次数限制,一次 API 调用,可以最大返回40个商品的淘宝客链接,因此,在本函数内,如 ...

  4. redis数据结构及使用场景

    redis数据结构及使用场景 1.字符串(String) String是最常用的一种数据类型,普通的k/v存储都可以归为此类.redis是使用C语言开发,但C中并没有字符串类型,只能使用指针或符数组的 ...

  5. Redis数据结构Set应用场景--黑名单校验器、京东与支付宝抽奖、微博榜单与QQ群的随机展示、帖子点赞、关注与粉丝、微关系计算、HyperLogLog的入门使用

    Set应用场景 set命令使用 淘宝黑名单 一.黑名单过滤器业务场景分析 二 .解决的技术方案 三.SpringBoot+redis模仿实现校验器 京东京豆抽奖 一.京东京豆抽奖的业务场景分析 二.京 ...

  6. 玩玩Redis系列(八)--redis数据结构及使用场景

    redis数据结构及使用场景 数据结构 String 相关命令 使用场景 List 相关命令 使用场景 Set 相关命令 使用场景 Hash 相关命令 使用场景 ZSet 相关命令 使用场景 Hype ...

  7. 淘口令解析api接口/淘口令解密还原api接口/淘口令短链接搜索商品详情接口,API接口获取方案

    一.淘宝/天猫/1688 平台item_password - 获得淘口令真实url接口返回说明 淘宝淘口令短链接地址例子:urlencode(fu置本段内容₤qQkYcb86z2d₤咑幵τao寶或点几 ...

  8. 将淘宝客链接转换为正常淘宝、天猫商品链接

    前一阵有个朋友采集了一堆的淘宝客推广链接,但很多都是加密后的格式 http://s.click.taobao.com/t?e=zGU34CA7K%2BPkqB07S4%2FK0CITy7klxxrJ3 ...

  9. java将淘宝客链接转换为正常商品链接

    参考文章:PHP将淘宝客链接转换提取成普通淘宝链接 最近在做淘宝客网站,有个地方需要把淘宝客链接转换成正常的淘宝链接,找了很多文章都没有java的,只要按照一个php的文章自己改写了一下,文章内容有很 ...

最新文章

  1. 本地运行github上的vue2.0仿饿了么webapp项目
  2. 机器学习概念 — 线性感知机、线性回归、单个神经元、多层次神经元
  3. 标准模板库之容器-《C++标准库(第二版)》读书笔记
  4. Spring Boot应用的打包和部署
  5. 一步一步安装服务器监视软件MRTG
  6. 木其工作室(专业程序代写服务)[转]Linux设备驱动程序学习-中断处理
  7. arcgis离线地图Java_ArcGIS API For Android离线地图的实现
  8. 找出大于200的最小的质数
  9. 【倒计时1天】PPP全球数字资产投资峰会-中国区北京首站之金融科技区块链支持可持续发展...
  10. android 异常 android Removing unused resources requires unused code shrinking to be turned on.
  11. 南开计算机考研真题,(NEW)南开大学《812计算机综合基础》历年考研真题汇编(83页)-原创力文档...
  12. python将多个表的数据合并到一个表
  13. C++程序设计:考研路茫茫——早起看书
  14. 深度学习之迁移学习介绍与使用
  15. 干货|FOF资产配置方案全解析
  16. OpenCV从摄像头中检测人脸
  17. 百度搜索问答卡API提交
  18. 江苏省计算机一级考试题型分数,全国计算机一级考试时间、题型、分值
  19. Go语言如何实现删除Winmail邮箱系统中收件箱的所有邮件
  20. yum install时提示This system is not registered with an entitlement server

热门文章

  1. 计算机的历史发展和应用
  2. win11进系统就黑屏怎么办
  3. 歌德巴赫猜想---java
  4. python求2数之和
  5. 网络协议-TCP协议详解
  6. 项立刚:FDD牌照发放 难改行业大格局
  7. uni-app iOS发布
  8. 定义一个复数类Complex,重载运算符“+”,“ -”,“*”,“/”使之能用于计算两个复数的加减乘除。
  9. USB转串口驱动代码分析
  10. C语言数组名、数组名取地址、数组首元素地址之间的关系