1. 为什么要用 Redis
redis 是一个高性能的key-value内存数据库,它支持常用的5种数据结构:String字符串、Hash哈希表、List列表、Set集合、Zset有序集合 等数据类型。
Redis它解决了两个问题:
-
==性能==
通常数据库的连接操作,一般都要几十毫秒,而Redis的读操作一般仅需不到1毫秒。通常只要把数据库的数据缓存进redis,就能得到几十倍甚至上百倍的性能提升。
-
==并发==
在最大并发的情况下,所有的请求直接访问数据库,数据库会出现连接异常,甚至卡死在数据库中。为了解决大并发卡死的问题,一般的做法是采用Redis做一个缓冲,让请求先访问到redis,而不是直接访问到数据库。
2. Redis启动
1.简单启动
进入redis 安装目录下,启动命令:./redis-server
进入redis客户端交互,./redis-cli -h {host} -p {port} [-a pwd]
然后所有的操作都是在交互的操作下进行。
2.以守护进程启动(~为什么要以守护进程启动~)
因为以方式一启动redis,则必须保证redis窗口必须打开,且无法进行其他操作。
( 1 ) 修改redis.conf配置文件 将 daemonize no
修改为 daemonize yes
( 2 ) 指定redis.conf配置文件启动 ./redis-server ./redis.conf
3. String 数据结构的字符串、整数、浮点操作
字符串操作
==SET语法:==
SET key value [NX] [EX <seconds>] [PX [millseconds]]
设置一对key value
必选参数说明:
- SET:命令
- key:待设置的key
- value:设置key的value
可选参数说明:
- NX:表示key不存在才设置,如果存在则返回null
- XX:表示key存在时才设置,如果不在村则返回null
- EX seconds:设置过期时间,过期时间精准为秒
- PX millseconds:设置过期时间,过期时间精准为毫秒
==SETNX语法:==
SETNX key value
所有参数必须为必选参数,设置一对key value,如果key存在,则设置失败,等同于SET key value NX
==SETEX语法:==
SETEX key value
所有参数为必选参数,设置一对key value,并设置过期时间,单位为秒,等同于 SET key value EX seconds
==PSETEX语法:==
PSETEX key value
所有参数为必选参数,设置一对key value,并设置过期时间,单位为毫秒,等同于PSETEX key value EX seconds
==MSET语法:==
MSET key1 value [key2 value2 key3 value3 ...]
批量存值,所有参数为必选,key value至少为一对,该命令的功能是设置多对key-value值。
==MGET语法:==
MGET key1 [key2 key3]
批量取值,所有参数为必选,key的值至少为一个,获取多个key 的value值,key值返回对应的value,不存在的返回null
==GETSET语法:==
GETSET key value1
先拿key查出value的值,然后再修改新值。所有参数为必选参数,既获取指定key的value,并设置key的新值为value1
==SETRANGE语法:==
SETRANGE key offset value
为某个key,修改偏移量offset后的值为value。所有参数为必选参数,设置指定key,偏移量offset后的值为value,影响范围为value 的长度,offset不能小于0
==GETRANGE语法:==
GETRANGE key start end
截取字符串,所有参数为必选参数,获取指定key的指定区间的value值,start、end可以为负数,如果为负数则反向取区间
==APPEND语法:==
APPEND key str
字符串拼接
==SUBSTR语法:==
SUBSTR key start end
截取字符串
整数操作
==INCR语法:==~应用场景—>点赞~
INCR key
计数器,所有参数为必选,指定key做加1操作,指定key对应的值必须为整型,否则返回错误,操作成功后返回操作后的值
==DECR语法:==
DECR key
所有参数为必选,指定key做减1操作,指定key对应的值必须为整型,否则返回错误,操作成功后返回操作后的值。为INCR的逆操作。
==INCRBY语法:==
INCRBY key data
加法。所有参数为必选参数,指定key做加data操作,指定key对应的值和data必须为整型,否则返回错误,操作成功后返回操作的值。
==DECRBY语法:==
DECRBY key data
减法。所有参数为必选参数,指定key做减data操作,指定key对应的值和data必须为整型,否则返回错误,操作成功后返回操作后的值。
浮点数操作
==INCRBYFLOAT语法:==
INCRBYFLOAT key num
增量。在原有key上加上浮点数。
4.SpringBoot 集成 Redis
📕 步骤一 : pom文件引入redis依赖jar包
-
引入swagger2、swagger-ui、mysql驱动、web、test、junit、lombok、连接池、redis等依赖jar包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
<version>1.4.7.RELEASE</version>
</dependency>
📕 步骤二 :配置文件中加入redis配置信息
-
```yaml
设置端口
server:
port: 9090
指定mapper.xml的位置
mybatis:
mapper-locations: classpath*:/com/rikc/redis/mapper/xml/*.xml
数据库驱动和ip
spring:
datasource:
driverClassName: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/boot_user?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
username: root
password: jiejie01
#redis配置信息
redis:
database: 0
host: 192.168.150.132
port: 6379
password: jiejie01
#swagger配置信息
swagger2:
enabled: true
logging:
level:
com:
rikc: debug
📕 步骤三 : 使用redisTemplate操作数据进行增删改查
- ```java
package com.example.demo.service.impl;
import com.example.demo.entity.User;
import com.example.demo.mapper.UserMapper;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import javax.annotation.Resource;
@Service
public class UserServiceImpl implements UserService {
private static final String CATCH_KEY_USER="user:";
@Resource
private UserMapper userMapper;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private StringRedisTemplate stringRedisTemplate;
public void createUser(User user) {
userMapper.insertSelective(user);
//缓存key
String key = CATCH_KEY_USER+user.getId();
//到数据库里面再重新捞出新数据出来,做缓存。
user = userMapper.selectByPrimaryKey(user.getId());
redisTemplate.opsForValue().set(key,user);
}
public void updateUser(User user1) {
userMapper.updateByPrimaryKeySelective(user1);
String key = CATCH_KEY_USER+user1.getId();
redisTemplate.opsForValue().set(key,user1);
}
public User findUserById(int id) {
ValueOperations valueOperations = redisTemplate.opsForValue();
User user = (User) valueOperations.get(CATCH_KEY_USER + id);
if(ObjectUtils.isEmpty(user)){
user = userMapper.selectByPrimaryKey(id);
valueOperations.set(CATCH_KEY_USER + id,user);
}
return user;
}
@Override
public void deletedUser(int id) {
}
}
1.问题搜集
1.为什么使用IDEA配置文件无法连接Redis。
解决方案 : 修改Redis配置文件redis.conf。 bind 0.0.0.0
即使Redis可以被外网连接,同时关闭linux防火墙 systemctl disable firewalld
2.redis存储数据为什么需要序列化?
解决方案 : 进redis的数据必须序列化 Serializable
3.优化Redis的序列化,修改为JSON形式。
原因 : 因为redisTemplate默认使用的是JdkSerializationRedisSeralizer,会出现两个问题:
- 被序列化的对象必须实现Serializable接口。
- 被序列化会出现乱码,导致value值可读性差。
解决方案 :
-
先把user实体类的序列化删除
-
创建redis的配置类RedisConfiguration
package com.example.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
/**
* 重写redis序列化方式,使用json方式:
* 当我们的数据存储到Redis的时候,我们的键(key)和值(value)都是通过Spring提供的Serialize序列化到Redis的内存中。
* RedisTemplate默认使用的是JdkSerializationRedisSerializer
* StringRedisTemplate默认使用的是StringRedisSerializer。
*
* Spring Data JPA为我们提供了下面的Serializer:
* GenericToStringSerializer、Jackson2JsonRedisSerializer、
* JacksonJsonRedisSerializer、JdkSerializationRedisSerializer、
* OxmSerializer、StringRedisSerializer。
*/
@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());
//设置HashKey的序列化方式为String
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
//设置HashValue的序列化方式为Json
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
-
flushdb 清空redis的旧数据,因为修改了序列化,老数据以及不能兼容了,必须清空。
-
往redis重写初始化数据。
5. SpringCache 集成 Redis
-
为什么要使用SpringCache,它解决了什么问题。
解决方案 : SpringCache 是Spring3.1版本发布出来的,它是对使用缓存进行封装和抽象,通过在方法上使用annotation注解就能拿到缓存结果。正是因为使用了annotation,所以它解决了业务代码和缓存代码的耦合度问题,即在不侵入业务代码的基础上让现有代码即可支持缓存,让开发人员无感知的使用了缓存。
==(注意:对于redis的缓存,springcache只支持String,其他的Hash、List、Set、Zset都不支持,要特别注意。)==
-
SpringCache 集成 Redis
-
pom文件加入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
-
配置文件,加入redis配置信息
spring.redis.database=0
spring.redis.host=192.168.150.132
spring.redis.port=6379
spring.redis.password=jiejie01
# 连接池最大连接数(使用负数表示没有限制)
spring.redis.lettuce.pool.max-active=8
# 连接池最大阻塞等待时间
spring.redis.lettuce.pool.max-wait=-1ms
# 连接池中的最大空闲连接 8
spring.redis.lettuce.pool.max-idle=8
# 连接池中的最小空闲连接
spring.redis.lettuce.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=5000ms
-
开启缓存配置,设置序列化
重点是 @EnableCaching
@Configuration
@EnableCaching
public class RedisConfig {
@Primary
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory){
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
redisCacheConfiguration = redisCacheConfiguration
//设置缓存的默认超时时间:30分钟
.entryTtl(Duration.ofMinutes(30L))
//如果是空值,不缓存
.disableCachingNullValues()
//设置key序列化器
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(keySerialize()))
//设置value序列化器
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerialize()));
return RedisCacheManager
.builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory))
.cacheDefaults(redisCacheConfiguration)
.build();
}
/**
* key序列化器
* @return
*/
private RedisSerializer<String> keySerialize(){
return new StringRedisSerializer();
}
/**
* value序列化器
* @return
*/
private RedisSerializer<Object> valueSerialize(){
return new GenericJackson2JsonRedisSerializer();
}
}
-
使用注解进行业务开发
```java
package com.example.demo.service.impl;import com.example.demo.entity.User;
import com.example.demo.mapper.UserMapper;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;import javax.annotation.Resource;
@Service
@CacheConfig(cacheNames = {“user”})
public class UserServiceImpl implements UserService {private static final String CATCH_KEY_USER="user:";
@Resource
private UserMapper userMapper;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private StringRedisTemplate stringRedisTemplate;
public void createUser(User user) {
userMapper.insertSelective(user);
//缓存key
String key = CATCH_KEY_USER+user.getId();
//到数据库里面再重新捞出新数据出来,做缓存。
user = userMapper.selectByPrimaryKey(user.getId());
redisTemplate.opsForValue().set(key,user);
}
@CachePut(key = "#user1.id")
public User updateUser(User user1) {
userMapper.updateByPrimaryKeySelective(user1);
return userMapper.selectByPrimaryKey(user1.getId());
}
-
@Cacheable(key = "#id")
public User findUserById(Integer id) {
return userMapper.selectByPrimaryKey(id);
}
@CacheEvict(key = "#id")
public void deletedUser(Integer id) {
userMapper.deleteByPrimaryKey(id);
}
}
```
-
SpringCache 常用注解
==@CacheConfig==
🌳 @CacheConfig(cacheNames={“user”})是类级别的注解,统一该类的所有缓存注解前缀。前缀名称为
user::
==@Cacheable==
🌳 @Cacheable(key = “#id”[,cacheNames = {“user”}]) 是方法级别的注解,用于将方法的结果缓存起来。key的结果为
user::id
。方法执行时,先从缓存中读取数据,如果缓存中没有数据,再执行方法体,并将结果存储到缓存中。<u>注意事项 :@CacheConfig 一般配合 @Cacheable 一起使用。</u>
<u>调用方法时传入id = 100,那么redis对应的key = user::100,value通过采用GenericJackson2JsonRedisSerializer序列化为json</u>
==@CachePut==
🌳 @CachePut是方法级别的注解,一般用于更新缓存。
```java
@CachePut(key = "#user1.id")
public User updateUser(User user1) {
userMapper.updateByPrimaryKeySelective(user1);
return userMapper.selectByPrimaryKey(user1.getId());
}
```
以上方法被调用时,先执行方法体,然后SpringCache通过返回更新缓存,即key = "user1.id",value = User
==@CacheEvict==
🌳 @CacheEvict是方法级别的注解,用于删除缓存。当方法被调用时,先执行方法体,再通过方法参数删除缓存。
-
SpringCache的大坑小坑
- 对于Redis的缓存,SpringCache只支持String,其他的Hash、List、Set、ZSet都不支持。所以其他类型只能使用RedisTemplate。
- 对于多表查询的数据缓存,springcache是不支持的,对于多表的整体缓存,只能用redisTemplate。
6. 微信文章阅读量场景及解决方案
:name_badge: 微信文章阅读量场景场景介绍:
在微信公众号里面的文章,每个用户阅读一遍文章,该篇文章的阅读量就会加1。对于这种并发量大的场景,不可能采用数据库来做计算,通常都是使用redis的incr命令来实现。
:name_badge: 微信文章的阅读计数量原理:redis INCR命令
==INCR命令==,它的全称是increment,用途就是计数器。
每次执行一次INCR命令,都将key的value值自动加1。
如果key不存在,那么key的值初始化为0,然后再执行INCR命令。
:name_badge: 简单demo测试
@Test
public void test4(){
Integer id = 1003;
String key = "article:"+id;
long n = stringRedisTemplate.opsForValue().increment(key);
log.info("key={},阅读量为{}",key,n);
}
:name_badge: 技术缺陷
需要频繁修改redis,耗费CPU,高并发修改会导致redis CPU 100%
7. Hash 数据结构:存储Java对象
1. 什么是redis的hash数据结构
==redis的hash数据结构==,其实就是string的升级版,它把string数据结构的key-value中的value升级为hash(和java中的hash一样的结构)。等同于 Map<String,HashMap<String,String>> hash = new HashMap();
每个hash的存储大小 :可以存储 2 的(32-1)方的键值对(40多亿)
2. redis的Hash结构经典场景:存储Java对象
把一个Product对象,存储进redis的hash结构。
@Data
public class Product {
//商品id
private long id;
//商品名称
private String name;
//商品价格
private Integer price;
//商品详情
private String detail;
}
==HSET Key field value语法:==
hset key field value
将哈希表 key 中的字段field的值设为value。
==HGET key field语法:==
hget key field
获取存储在哈希表中指定字段的值。
==HMSET key field value1 [field2 value2…]语法:==
hmset key field value1 field2 value2...
同时将多个field-value[阈 - 值]设置到哈希表key中。
==HMGET key field1 [field2 field3…]语法:==
hmget key field1 field2 field3...
获取所有给定字段的值。
==HKEY key语法:==
HKEY key
获取指定hash中所有value值
==HVALS key语法:==
hvals key
获取指定hash中所有value值。
==HGETALL key语法:==
hgetall key
获取指定hash中所有field value值
==HLEN key语法:==
hlen key
获取指定hash中元素的个数
==HINCRBY key field data (整形)语法:==
hincrby key field data (整形)
给指定field对应的value值加上data数值。
==HINCRBYFLOAT key field data (浮点型)语法:==
HINCRBYFLOAT key field data(浮点型)
给指定field对应的value 值加上data 数值
==HEXISTS key field语法:==
hexists key field
检查指定的field是否存在
==HDEL key field1 [field2、field3]语法:==
hdel key field1 field2 field3...
删除一个或多个哈希字段
3.String和Hash的不同应用场景
📕 Redis存储Java对象,一般是String或Hash两种。那么到底什么时候用String?什么时候用Hash?
解决方案 :
-
String的存储通常用在频繁读操作,它的存储格式是json,即把java对象转换成json,然后存入redis中
-
Hash的存储场景应用在频繁写操作。即,当对象的某个属性值频繁修改时,不适用string+json的数据结构,因为不灵活,每次修改都要把整个对象转存为json存储。
如果采用hash,就可以针对某个属性单独修改,不用序列号去修改整个对象。例如:商品的库存、价格、关注数、评价数经常变动时,就使用存储hash结果。
8. 京东购物车实战
:taco: 京东购物车多种场景分析 :
- 先登录京东账号,清空以前购物车,然后添加一件商品A,保证你的购物车只有一件商品A。
- 退出登录,购物车添加商品B,然后关闭浏览器再打开(请问购物车的商品B是否存在?)
- 再次登录你的京东账号。(请问:你的购物车有几件商品?)
:taco: 图解分析:双11高并发的京东购物车技术实现
:taco: 购物车的经典redis场景
-
往购物车中添加两件商品,采用hash数据结果。key = cart:user:用户id
hset cart:user:1000 101 1
hset cart:user:1000 102 2
hgetall cart:user:1000
## 结果
1)"101"
2)"1"
3)"102"
4)"2"
-
修改购物车的数据,为某件商品添加数量
hincrby cart:user:1000 101 1
hincrby cart:user:1000 102 200
hgetall cart:user:1000
## 结果
1)"101"
2)"2"
3)"102"
4)"202"
-
统计购物车有多少种商品
hlen cart:user:1000
## 结果
(integer) 2
-
删除购物车某件商品
hdel cart:user:1000 101
## 结果
(integer) 1
:taco: SpringDataRedis代码实现
-
SpringBoot+redis实现登录用户购物车功能
```java
/**
* 添加购物车
* @param obj
*/
@PostMapping("/addCart")
public void addCart(Cart obj) {
try {
String key = CART_KEY + obj.getUserId();
Boolean hashKey = redisTemplate.opsForHash().getOperations().hasKey(key);
//存在
if(hashKey){
redisTemplate.opsForHash().put(key,obj.getProductId(),obj.getAmount());
}else{
redisTemplate.opsForHash().put(key,obj.getProductId(),obj.getAmount());
redisTemplate.expire(key,90, TimeUnit.DAYS);
}
//TOOD 发rabbitmq出去
}catch (Exception e){
log.error(e.getMessage());
}
}
/**
* 修改购物车
* @param obj
*/
@PostMapping("/updateCart")
public void updateCart(Cart obj){
String key = CART_KEY+obj.getProductId();
redisTemplate.opsForHash().put(key,obj.getProductId(),obj.getAmount());
}
/**
* 查看购物车
* @param userId
* @return
*/
@PostMapping("/findAll")
public CartPage findAll(String userId){
Long size = redisTemplate.opsForHash().size(CART_KEY+userId);
Map<String,Integer> entries = redisTemplate.opsForHash().entries(userId);
List<Cart> cartList = new ArrayList<>();
for(Map.Entry<String,Integer> entry:entries.entrySet()){
Cart cart = new Cart();
cart.setUserId(userId);
cart.setProductId(entry.getKey());
cart.setAmount(String.valueOf(entry.getValue()));
cartList.add(cart);
}
CartPage cartPage = new CartPage();
cartPage.setCartList(cartList);
cartPage.setCount(size);
return cartPage;
}
/**
* 删除购物车
* @param userId
* @param productId
*/
@PostMapping("/del")
public void delCart(Long userId,Long productId){
String key = CART_KEY+userId;
redisTemplate.opsForHash().delete(key,productId.toString());
//TOOD 发rabbitmq出去
}
- SpringBoot+Redis+Cookies实现未登录用户的购物车
```java
@RestController
public class CookieCartController {
@Autowired
private HttpServletResponse response;
@Autowired
private HttpServletRequest request;
@Autowired
private IdGenerator idGenerator;
@Autowired
private RedisTemplate redisTemplate;
public static final String COOKIE_KEY = "cart:cookie";
/**
* 添加购物车
* @param obj
*/
@PostMapping("/addCart")
public void addCart(CookieCart obj){
String cartId = getCookieCartId();
String key = COOKIE_KEY+cartId;
Boolean hashKey = redisTemplate.opsForHash().getOperations().hasKey(key);
if(hashKey){
redisTemplate.opsForHash().put(key,obj.getProductId(),obj.getAmount());
}else{
redisTemplate.opsForHash().put(key,obj.getProductId(),obj.getAmount());
redisTemplate.expire(key,90, TimeUnit.DAYS);
}
}
/**
* 更新购物车
* @param obj
*/
@PostMapping("/updateCart")
public void updateCart(CookieCart obj){
String cartId = getCookieCartId();
String key = COOKIE_KEY+cartId;
redisTemplate.opsForHash().put(key,obj.getProductId(),obj.getAmount());
}
/**
* 删除购物车
*/
@PostMapping("/delCart")
public void delCart(Long productId){
String cartId = getCookieCartId();
String key = COOKIE_KEY+cartId;
redisTemplate.opsForHash().delete(key,productId.toString());
}
/**
* 查看购物车
*/
@PostMapping("/findAll")
public CartPage findAll(){
String cartId = getCookieCartId();
String key = COOKIE_KEY+cartId;
CartPage cartPages = new CartPage();
List<Cart> cartPage = new ArrayList<>();
Long size = redisTemplate.opsForHash().size(key);
Map<String,Integer> entries = redisTemplate.opsForHash().entries(key);
for(Map.Entry<String,Integer> entry: entries.entrySet()){
Cart cart = new Cart();
cart.setProductId(entry.getKey());
cart.setAmount(String.valueOf(entry.getValue()));
cartPage.add(cart);
}
cartPages.setCartList(cartPage);
return cartPages;
}
private String getCookieCartId(){
Cookie[] cookies = request.getCookies();
if (cookies!=null){
for (Cookie cookie:cookies){
if (cookie.getName().equals("cartId")){
return cookie.getValue();
}
}
}
long id = idGenerator.incrementId();
Cookie cookie = new Cookie("cartId",String.valueOf(id));
response.addCookie(cookie);
return id+"";
}
}
@Service
public class IdGenerator {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String ID_KEY = "id:generator:cart";
/**
* 生成唯一Id
* @return
*/
public long incrementId(){
return redisTemplate.opsForValue().increment(ID_KEY);
}
}
-
合并登录用户和未登录用户的购物车
@PostMapping("/mergeCart")
public void mergeCart(Long userId){
//第一步提取未登录用户的cookie的购物车数据
String cartId = getCookieCartId();
String keyCookie = COOKIE_KEY+cartId;
Map<String,Integer> map = redisTemplate.opsForHash().entries(keyCookie);
//第二步 把cookie中的购物车合并到登录用户的购物车
String keyUser = "cart:user:"+userId;
redisTemplate.opsForHash().putAll(keyUser,map);
//第三步,删除redis未登录用户的cookie的购物车数据
redisTemplate.delete(keyCookie);
//第四步,删除未登录用户的cookie的cartId
Cookie cookie = new Cookie("cartId",null);
cookie.setMaxAge(0);
response.addCookie(cookie);
}
zuoyong
9.Redis解决分布式系统的session不一致性问题
1. Session有什么作用
Session是客户端与服务端通讯会话跟踪技术,==服务端与客户端保持整个通讯的会话基本信息。==
客户端在第一次访问服务端的时候,服务端会响应一个sesssionId并且把它存储到本地cookie中。在之后的访问会将cookie中的sessionId放入到请求头去访问服务器。如果通过这个sessionId没有找到对应的数据,那么服务器会创建一个新的sesssionId响应给客户端。
2. 分布式Session有什么问题?
3.优雅的设计session一致性,低耦合
-
UserController session中获取或存放用户信息
@RequestMapping("/login")
public String login(HttpServletRequest request){
String username = request.getParameter("username");
String password = request.getParameter("password");
HttpSession session = request.getSession();
session.setAttribute("username",username);
session.setAttribute("password",password);
return "login successful";
}
@PostMapping("/queryUserInfo")
public String queryUserInfo(HttpServletRequest request){
HttpSession session = request.getSession();
String username = (String)session.getAttribute("username");
String password = (String)session.getAttribute("password");
return "username"+username+",password="+password;
}
}
-
自定义HttpServletSession对象
```java
package com.example.demo.filter;
import com.alibaba.fastjson.JSONObject;
import org.json.JSONException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import javax.servlet.ServletContext;
import javax.servlet.http.*;
import java.util.Enumeration;
import java.util.UUID;
public class MyHttpServletSession implements HttpSession {
@Autowired
private RedisTemplate redisTemplate;
HttpServletRequest request;
HttpServletResponse response;
public MyHttpServletSession(HttpServletRequest request, HttpServletResponse response) {
this.request = request;
this.response = response;
}
private ThreadLocal<String> local = new ThreadLocal<>();
@Override
public void setAttribute(String name, Object value) {
//将我们的登录信息保存到redis中。
String sessionId = getSessionFromCookie();
if(sessionId!=null){
sessionId = local.get();
if(sessionId==null){
sessionId = "user:"+ UUID.randomUUID();
local.set(sessionId);
}
}
JSONObject jsonpObject = new JSONObject();
jsonpObject.put(name,value);
redisTemplate.opsForValue().set(sessionId,jsonpObject.toString());
//将sessionId添加到cookie中。
Cookie cookie = new Cookie("sessionId", sessionId);
cookie.setPath("/");
response.addCookie(cookie);
}
@Override
public Object getAttribute(String name) {
String sessionFromCookie = getSessionFromCookie();
String userInfo = (String) redisTemplate.opsForValue().get(sessionFromCookie);
JSONObject jsonObject = JSONObject.parseObject(userInfo);
if (jsonObject.containsKey(name)){
return jsonObject.get(name);
}
return null;
}
private String getSessionFromCookie(){
Cookie[] cookies = request.getCookies();
if(cookies!=null){
for (Cookie cookie:cookies){
if("sessionId".equals(cookie.getName())){
return cookie.getValue();
}
}
};
return null;
}
@Override
public long getCreationTime() {
return 0;
}
@Override
public String getId() {
return null;
}
@Override
public long getLastAccessedTime() {
return 0;
}
@Override
public ServletContext getServletContext() {
return null;
}
@Override
public void setMaxInactiveInterval(int i) {
}
@Override
public int getMaxInactiveInterval() {
return 0;
}
@Override
public HttpSessionContext getSessionContext() {
return null;
}
@Override
public Object getValue(String s) {
return null;
}
@Override
public Enumeration<String> getAttributeNames() {
return null;
}
@Override
public String[] getValueNames() {
return new String[0];
}
@Override
public void putValue(String s, Object o) {
}
@Override
public void removeAttribute(String s) {
}
@Override
public void removeValue(String s) {
}
@Override
public void invalidate() {
}
@Override
public boolean isNew() {
return false;
}
}
- 自定义HttpServlet对象,保证获取到的是我们自定义的Session(一致性)
```java
package com.example.demo.filter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
public class MyHttpServletRequest extends HttpServletRequestWrapper {
HttpServletRequest request;
HttpServletResponse response;
public MyHttpServletRequest(HttpServletRequest request, HttpServletResponse response){
super(request);
this.request = request;
this.response = response;
}
@Override
public HttpSession getSession() {
//返回我们自己创建的session对象
return new MyHttpServletSession(request,response);
}
}
-
自定义过滤器,拦截请求
```java
package com.example.demo.filter;
import javax.servlet.;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebFilter(urlPatterns = “/“)
public class SessionFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = new MyHttpServletRequest((HttpServletRequest) servletRequest,(HttpServletResponse) servletResponse);
filterChain.doFilter(request,servletResponse);
}
@Override
public void destroy() {
}
}
- 启动器上指定自定义过滤器包路径
```java
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
import tk.mybatis.spring.annotation.MapperScan;
@SpringBootApplication
@MapperScan("com.example.demo.mapper")
@ServletComponentScan(basePackages = {"com.example.demo"})
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}