Hash应用场景
Hash
- redis的hash数据结构,其实就是string的升级版,它把string 数据结构的key value,中的value类型升级为hash(和java的hash一样的结构)
Map<String, HashMap<String,String>> hash=new HashMap<String,HashMap<String,String>>(); - 每个 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《短链接转换器》
《短链接转换器》的原理:
- 长链接转换为短链接
实现原理:长链接转换为短链接加密串key,然后存储于redis的hash结构中。 - 重定向到原始的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过期策略
对于过期数据,一般有三种删除策略:
- 定时删除,即在设置键的过期时间的同时,创建一个定时器, 当键的过期时间到来时,立即删除。
- 惰性删除,即在访问键的时候,判断键是否过期,过期则删除,否则返回该键值。
- 定期删除,即每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。
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设置过期时间,这是值得注意的点;