redis中的hash结构数据结构,就是value类型为hash【和java中的hash结构一样】
Map<String,HashMap<String,String>> hash=newHashMap<String,HashMap<String,String>>();
1.redis的hash结构经典场景:存储java对象
1.Redis的String和Hash存储Java对象的区别
redis 存储java对象一般是string 和 hash两种,但是两个使用的场景不同:
1.String的存储通常用在频繁读操作场景,它的存储格式是json,即将java对象转成json格式,然后存入redis。
2.hash的存储通常用在频繁写操作场景,即当对象的某个属性需要频繁修改时,就不适合使用String+Json 这种数据格式【不灵活,每次修改都需要将对象转成json】
如果采用hash,直接对某个属性直接修改,不用序列化去修改整个对象,例如:商品的库存,价格,评论数,关注度经常变换,就需要使用hash存储。
2.使用Hash存储商品数据
把一个Product对象,存储进redis的hash结构:
1.对象属性:
```
@Data
public class Product {
//商品id
private Long id;
//商品名称
private String name;
//商品价格
private Integer price;
//商品详情
private String detail;
}
```
2.redis的配置文件:
注意:使用redis的hash仍然需要 序列化,注意不要使用默认的序列化,要重写
@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;
}
}
3. 将对象中的属性和值转成map对象 :
使用了反射原理:
public static Map<String, Object> objectToMap(Object obj) {
if (obj == null) {
return null;
}
Map<String, Object> map = new HashMap<String, Object>();
try {
Field[] declaredFields = obj.getClass().getDeclaredFields();
for (Field field : declaredFields) {
field.setAccessible(true);
map.put(field.getName(), field.get(obj));
}
} catch (Exception e) {
}
return map;
}
代码如下:
2.淘宝短链接Redis实战
输入 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
========================
1. 什么是短链接?
就是把普通网址,转换成比较短的网址。
2. 短链接有什么好处?
- 节省网址长度,便于社交化传播。
- 方便后台跟踪点击量、统计。
3.为什么要使用redis的hash类型存储?
-使用String类型也可以存储,但是还要统计一些其他的数据【例:点击量等等】,所以使用hash类型比较方便,并且这些其他的数据的值是很容易改变的。
4.《短链接转换器》的原理:
1. 长链接转换为短链接
实现原理:长链接转换为短链接加密串key,然后存储于redis的hash结构中。
2. 重定向到原始的url
实现原理:通过加密串key到redis找出原始url,然后重定向出去
长链接转换成短链接方法代码:
/**
* 将长网址 md5 生成 32 位签名串,分为 4 段, 每段 8 个字节
* 对这四段循环处理, 取 8 个字节, 将他看成 16 进制串与 0x3fffffff(30位1) 与操作, 即超过 30 位的忽略处理
* 这 30 位分成 6 段, 每 5 位的数字作为字母表的索引取得特定字符, 依次进行获得 6 位字符串
* 总的 md5 串可以获得 4 个 6 位串,取里面的任意一个就可作为这个长 url 的短 url 地址
*/
public class ShortUrlGenerator {
//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;
}
public static void main(String[] args) {
// 长连接
String longUrl = "https://detail.tmall.com/item.htm?id=597254411409";
// 转换成的短链接后6位码,返回4个短链接
String[] shortCodeArray = shortUrl(longUrl);
for (int i = 0; i < shortCodeArray.length; i++) {
// 任意一个都可以作为短链接码
System.out.println(shortCodeArray[i]);
}
}
}
controller层代码:
@RestController
@Slf4j
public class ShortUrlController {
@Autowired
private HttpServletResponse response;
@Autowired
private RedisTemplate redisTemplate;
private final static String SHORT_URL_KEY="short:url";
/**
* 长链接转换为短链接
* 实现原理:长链接转换为短加密串key,然后存储在redis的hash结构中。
*/
@GetMapping(value = "/encode")
public String encode(String url) {
//一个长链接url转换为4个短加密串key
String [] keys= ShortUrlGenerator.shortUrl(url);
//任意取出其中一个,我们就拿第一个
String key=keys[0];
//用hash存储,key=加密串,value=原始url
this.redisTemplate.opsForHash().put(SHORT_URL_KEY,key,url);
log.info("长链接={},转换={}",url,key);
return "http://127.0.0.1:9090/"+key;
}
/**
* 重定向到原始的URL
* 实现原理:通过短加密串KEY到redis找出原始URL,然后重定向出去
*/
@GetMapping(value = "/{key}")
public void decode(@PathVariable String key) {
//到redis中把原始url找出来
String url=(String) this.redisTemplate.opsForHash().get(SHORT_URL_KEY,key);
try {
//重定向到原始的url
response.sendRedirect(url);
} catch (IOException e) {
e.printStackTrace();
}
}
}
3.京东双11购物车实战
购物车的redis经典场景:
购物车页面的数据都是从redis中获取:
购物车只存储商品数量和商品id,那购物车界面那些图片标题价格等等是由商品服务模块来查询
购物车实体:
@Data
public class Cart {
private Long userId;
private Long productId;
private int amount;
}
1.用户登录状态下的方法
添加购物车,如果之前没有添加过,就设置过期时间
@PostMapping(value = "/addCart")
public void addCart(CookieCart obj) {
String cartId=this.getCookiesCartId();
String key=COOKIE_KEY+cartId;
Boolean hasKey = redisTemplate.opsForHash().getOperations().hasKey(key);
//存在
if(hasKey){
this.redisTemplate.opsForHash().put(key, obj.getProductId().toString(),obj.getAmount());
}else{
this.redisTemplate.opsForHash().put(key, obj.getProductId().toString(), obj.getAmount());
//设置过期时间
this.redisTemplate.expire(key,90, TimeUnit.DAYS);
}
//todo 用mq将其存入数据库中
}
修改购物车数量:
/**
* 修改购物车的数量
*/
@PostMapping(value = "/updateCart")
public void updateCart(Cart obj) {
String key = CART_KEY + obj.getUserId();
this.redisTemplate.opsForHash().put(key, obj.getProductId().toString(), obj.getAmount());
//todo 用mq将其存入数据库中
}
删除购物车:
/**
*删除购物车
*/
@PostMapping(value = "/delCart")
public void delCart(Long userId, Long productId) {
String key = CART_KEY + userId;
this.redisTemplate.opsForHash().delete(key, productId.toString());
//todo 用mq将其存入数据库中
}
查询购物车的所有商品数据:
@PostMapping(value = "/findAll")
public CartPage findAll(Long userId) {
String key = CART_KEY + userId;
CartPage cartPage = new CartPage();
//查购物车的总数
long size = this.redisTemplate.opsForHash().size(key);
cartPage.setCount((int) size);
//查询购物车的所有商品
//entries=hgetall命令
Map<String, Integer> map = this.redisTemplate.opsForHash().entries(key);
List<Cart> cartList = new ArrayList<>();
for (Map.Entry<String, Integer> entry : map.entrySet()) {
Cart cart = new Cart();
cart.setUserId(userId);
cart.setProductId(Long.parseLong(entry.getKey()));
cart.setAmount(entry.getValue());
cartList.add(cart);
}
cartPage.setCartList(cartList);
return cartPage;
}
CartPage实体:
@Data
public class CartPage<T> {
private List<T> cartList;
private int count;
}
2.用户未登录状态下对购物车的操作
1.添加购物车:
/**
* 添加购物车
*/
@PostMapping(value = "/addCart")
public void addCart(CookieCart obj) {
//先获取cookie
String cartId=this.getCookiesCartId();
String key=COOKIE_KEY+cartId;
Boolean hasKey = redisTemplate.opsForHash().getOperations().hasKey(key);
//存在
if(hasKey){
this.redisTemplate.opsForHash().put(key, obj.getProductId().toString(),obj.getAmount());
}else{
this.redisTemplate.opsForHash().put(key, obj.getProductId().toString(), obj.getAmount());
this.redisTemplate.expire(key,90, TimeUnit.DAYS);
}
}
获取cookie的方法:
/**
* 获取cookies
*/
public String getCookiesCartId(){
//第一步:先检查cookies是否有cartid
Cookie[] cookies = request.getCookies();
if(cookies != null){
for(Cookie cookie : cookies){
if(cookie.getName().equals("cartId")){
return cookie.getValue();
}
}
}
//第二步:cookies没有cartid,直接生成全局id,并设置到cookie里面
//生成全局唯一id
long id=this.idGenerator.incrementId();
//设置到cookies
Cookie cookie=new Cookie("cartId",String.valueOf(id));
response.addCookie(cookie);
return id+"";
}
生成全局id代码:
@Service
public class IdGenerator {
@Autowired
private StringRedisTemplate stringRedisTemplate;
private static final String ID_KEY = "id:generator:cart";
/**
* 生成全局唯一id
*/
public Long incrementId() {
long n=this.stringRedisTemplate.opsForValue().increment(ID_KEY);
return n;
}
}
2.修改购物车:
@PostMapping(value = "/updateCart")
public void updateCart(CookieCart obj) {
String cartId=this.getCookiesCartId();
String key=COOKIE_KEY+cartId;
this.redisTemplate.opsForHash().put(key, obj.getProductId().toString(),obj.getAmount());
}
3.删除购物车:
/**
* 删除购物车
*/
@PostMapping(value = "/delCart")
public void delCart(Long productId) {
String cartId=this.getCookiesCartId();
String key=COOKIE_KEY+cartId;
this.redisTemplate.opsForHash().delete(key, productId.toString());
}
4.查询所有购物车商品数据:
/**
* 查询某个用户的购物车
*/
@PostMapping(value = "/findAll")
public CartPage findAll() {
String cartId=this.getCookiesCartId();
String key=COOKIE_KEY+cartId;
CartPage<CookieCart> cartPage=new CartPage();
//查询该用户购物车的总数
long size=this.redisTemplate.opsForHash().size(key);
cartPage.setCount((int)size);
//查询购物车的所有商品
Map<String,Integer> map= this.redisTemplate.opsForHash().entries(key);
List<CookieCart> cartList=new ArrayList<>();
for (Map.Entry<String,Integer> entry:map.entrySet()){
CookieCart cart=new CookieCart();
cart.setProductId(Long.parseLong(entry.getKey()));
cart.setAmount(entry.getValue());
cartList.add(cart);
}
cartPage.setCartList(cartList);
return cartPage;
}
3.登陆后发起合并购物车请求
1.合并购物车
/**
* 合并购物车
* 把cookie中的购物车合并到登录用户的购物车
*/
@PostMapping(value = "/mergeCart")
public void mergeCart(Long userId) {
//第一步:提取未登录用户的cookie的购物车数据
String cartId=this.getCookiesCartId();
String keycookie=COOKIE_KEY+cartId;
Map<String,Integer> map= this.redisTemplate.opsForHash().entries(keycookie);
//第二步:把cookie中得购物车合并到登录用户的购物车
// 这一步不一致,看规则,有的 是数量累加,有的不是,这里不是累加
String keyuser = "cart:user:" + userId;
this.redisTemplate.opsForHash().putAll(keyuser,map);
//第三步:删除redis未登录的用户cookies的购物车数据
this.redisTemplate.delete(keycookie);
//第四步:删除未登录用户cookies的cartid
Cookie cookie=new Cookie("cartId",null);
cookie.setMaxAge(0);
response.addCookie(cookie);
}
4.用户注册微博的redis技术方案
参考:《redis入门指南》进阶场景之用户注册和登录与在线保持 - 简书
新用户注册流程:先写入db,然后写入redis,
redis采用hash结构,key=weibo:user:id
为什么要使用hash结构来存储用户信息?
答:一个基本的用户注册,需要如下的字段:
- 用户名
- 邮箱
- 密码
- 注册时间
- 手机等其他需要额外标注的信息
- 可扩展的字段(你不知道之后业务的形态发展,如果你不想连表查询的话)
-
那么在这种需求之下,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);
}
ObjectUtil:
public class ObjectUtil {
/**
* 将Object对象里面的属性和值转化成Map对象
*/
public static Map<String, Object> objectToMap(Object obj) {
Map<String, Object> map = new HashMap<String,Object>();
Class<?> clazz = obj.getClass();
for (Field field : clazz.getDeclaredFields()) {
field.setAccessible(true);
String fieldName = field.getName();
Object value = null;
try {
value = field.get(obj);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
map.put(fieldName, value);
}
return map;
}
//Map转Object
public static Object mapToObject(Map<Object, Object> map, Class<?> beanClass) throws Exception {
if (map == null)
return null;
Object obj = beanClass.newInstance();
Field[] fields = obj.getClass().getDeclaredFields();
for (Field field : fields) {
int mod = field.getModifiers();
if (Modifier.isStatic(mod) || Modifier.isFinal(mod)) {
continue;
}
field.setAccessible(true);
if (map.containsKey(field.getName())) {
field.set(obj, map.get(field.getName()));
}
}
return obj;
}
}
5.用户发微博的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='内容表';
```
2.发微博逻辑
@ApiOperation(value="用户发微博")
@PostMapping(value = "/post")
public void post(@RequestBody ContentVO contentVO) {
Content content=new Content();
BeanUtils.copyProperties(contentVO,content);
contentService.post(content);
}
/**
* 用户发微博
*/
public Content addContent(Content obj){
//步骤1:先入库
this.contentMapper.insertSelective(obj);
//步骤2:入库成功后 写redis
obj=this.contentMapper.selectByPrimaryKey(obj.getId());
//将Object对象里面的属性和值转化成Map对象
Map<String, Object> map= ObjectUtil.objectToMap(obj);
//设置缓存key
String key= Constants.CACHE_CONTENT_KEY+obj.getId();
//微博内容的redis数据结构 用hash
HashOperations<String, String ,Object> opsForHash=redisTemplate.opsForHash();
opsForHash.putAll(key,map);
//步骤3:设置30天过期
this.redisTemplate.expire(key,30, TimeUnit.DAYS);
return obj;
}
```