redis缓存实战讲解

1. 为什么要用 Redis

redis 是一个高性能的key-value内存数据库,它支持常用的5种数据结构:String字符串、Hash哈希表、List列表、Set集合、Zset有序集合 等数据类型。

Redis它解决了两个问题:

  1. ==性能==

    通常数据库的连接操作,一般都要几十毫秒,而Redis的读操作一般仅需不到1毫秒。通常只要把数据库的数据缓存进redis,就能得到几十倍甚至上百倍的性能提升。

  2. ==并发==

    在最大并发的情况下,所有的请求直接访问数据库,数据库会出现连接异常,甚至卡死在数据库中。为了解决大并发卡死的问题,一般的做法是采用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包

     
      
    1. <dependency>
    2. <groupId>org.springframework.boot</groupId>
    3. <artifactId>spring-boot-starter-redis</artifactId>
    4. <version>1.4.7.RELEASE</version>
    5. </dependency>

📕 步骤二 :配置文件中加入redis配置信息

  • ```yaml

    设置端口

    server:

     
      
    1. port: 9090

    指定mapper.xml的位置

    mybatis:

     
      
    1. mapper-locations: classpath*:/com/rikc/redis/mapper/xml/*.xml

    数据库驱动和ip

    spring:

     
      
    1. datasource:
    2. driverClassName: com.mysql.jdbc.Driver
    3. url: jdbc:mysql://localhost:3306/boot_user?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
    4. username: root
    5. password: jiejie01
    6. #redis配置信息
    7. redis:
    8. database: 0
    9. host: 192.168.150.132
    10. port: 6379
    11. password: jiejie01
    12. #swagger配置信息
    13. swagger2:
    14. enabled: true

    logging:

     
      
    1. level:
    2. com:
    3. rikc: debug
 
  1. 📕 步骤三 : 使用redisTemplate操作数据进行增删改查
  2. - ```java
  3. package com.example.demo.service.impl;
  4. import com.example.demo.entity.User;
  5. import com.example.demo.mapper.UserMapper;
  6. import com.example.demo.service.UserService;
  7. import org.springframework.beans.factory.annotation.Autowired;
  8. import org.springframework.cache.annotation.Cacheable;
  9. import org.springframework.data.redis.core.RedisTemplate;
  10. import org.springframework.data.redis.core.StringRedisTemplate;
  11. import org.springframework.data.redis.core.ValueOperations;
  12. import org.springframework.stereotype.Service;
  13. import org.springframework.util.ObjectUtils;
  14. import javax.annotation.Resource;
  15. @Service
  16. public class UserServiceImpl implements UserService {
  17. private static final String CATCH_KEY_USER="user:";
  18. @Resource
  19. private UserMapper userMapper;
  20. @Autowired
  21. private RedisTemplate redisTemplate;
  22. @Autowired
  23. private StringRedisTemplate stringRedisTemplate;
  24. public void createUser(User user) {
  25. userMapper.insertSelective(user);
  26. //缓存key
  27. String key = CATCH_KEY_USER+user.getId();
  28. //到数据库里面再重新捞出新数据出来,做缓存。
  29. user = userMapper.selectByPrimaryKey(user.getId());
  30. redisTemplate.opsForValue().set(key,user);
  31. }
  32. public void updateUser(User user1) {
  33. userMapper.updateByPrimaryKeySelective(user1);
  34. String key = CATCH_KEY_USER+user1.getId();
  35. redisTemplate.opsForValue().set(key,user1);
  36. }
  37. public User findUserById(int id) {
  38. ValueOperations valueOperations = redisTemplate.opsForValue();
  39. User user = (User) valueOperations.get(CATCH_KEY_USER + id);
  40. if(ObjectUtils.isEmpty(user)){
  41. user = userMapper.selectByPrimaryKey(id);
  42. valueOperations.set(CATCH_KEY_USER + id,user);
  43. }
  44. return user;
  45. }
  46. @Override
  47. public void deletedUser(int id) {
  48. }
  49. }

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,会出现两个问题:

  1. 被序列化的对象必须实现Serializable接口。
  2. 被序列化会出现乱码,导致value值可读性差。

​ 解决方案 :

  • 先把user实体类的序列化删除

  • 创建redis的配置类RedisConfiguration

     
      
    1. package com.example.demo.config;
    2. import org.springframework.context.annotation.Bean;
    3. import org.springframework.context.annotation.Configuration;
    4. import org.springframework.data.redis.connection.RedisConnectionFactory;
    5. import org.springframework.data.redis.core.RedisTemplate;
    6. import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
    7. import org.springframework.data.redis.serializer.StringRedisSerializer;
    8. @Configuration
    9. public class RedisConfig {
    10. /**
    11. * 重写redis序列化方式,使用json方式:
    12. * 当我们的数据存储到Redis的时候,我们的键(key)和值(value)都是通过Spring提供的Serialize序列化到Redis的内存中。
    13. * RedisTemplate默认使用的是JdkSerializationRedisSerializer
    14. * StringRedisTemplate默认使用的是StringRedisSerializer。
    15. *
    16. * Spring Data JPA为我们提供了下面的Serializer:
    17. * GenericToStringSerializer、Jackson2JsonRedisSerializer、
    18. * JacksonJsonRedisSerializer、JdkSerializationRedisSerializer、
    19. * OxmSerializer、StringRedisSerializer。
    20. */
    21. @Bean
    22. public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
    23. RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
    24. redisTemplate.setConnectionFactory(redisConnectionFactory);
    25. //创建一个json的序列化对象
    26. GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
    27. //设置value的序列化方式json
    28. redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
    29. //设置key的序列化方式string
    30. redisTemplate.setKeySerializer(new StringRedisSerializer());
    31. //设置HashKey的序列化方式为String
    32. redisTemplate.setHashKeySerializer(new StringRedisSerializer());
    33. //设置HashValue的序列化方式为Json
    34. redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
    35. redisTemplate.afterPropertiesSet();
    36. return redisTemplate;
    37. }
    38. }
  • flushdb 清空redis的旧数据,因为修改了序列化,老数据以及不能兼容了,必须清空。

  • 往redis重写初始化数据。

5. SpringCache 集成 Redis

  1. 为什么要使用SpringCache,它解决了什么问题。

    ​ 解决方案 : SpringCache 是Spring3.1版本发布出来的,它是对使用缓存进行封装和抽象,通过在方法上使用annotation注解就能拿到缓存结果。正是因为使用了annotation,所以它解决了业务代码和缓存代码的耦合度问题,即在不侵入业务代码的基础上让现有代码即可支持缓存,让开发人员无感知的使用了缓存。

    ​ ==(注意:对于redis的缓存,springcache只支持String,其他的Hash、List、Set、Zset都不支持,要特别注意。)==

  2. SpringCache 集成 Redis

    • pom文件加入依赖

       
          
      1. <dependency>
      2. <groupId>org.springframework.boot</groupId>
      3. <artifactId>spring-boot-starter-cache</artifactId>
      4. </dependency>
      5. <dependency>
      6. <groupId>org.apache.commons</groupId>
      7. <artifactId>commons-pool2</artifactId>
      8. <version>2.6.2</version>
      9. </dependency>
      10. <dependency>
      11. <groupId>org.springframework.boot</groupId>
      12. <artifactId>spring-boot-starter-data-redis</artifactId>
      13. </dependency>
    • 配置文件,加入redis配置信息

       
          
      1. spring.redis.database=0
      2. spring.redis.host=192.168.150.132
      3. spring.redis.port=6379
      4. spring.redis.password=jiejie01
      5. # 连接池最大连接数(使用负数表示没有限制)
      6. spring.redis.lettuce.pool.max-active=8
      7. # 连接池最大阻塞等待时间
      8. spring.redis.lettuce.pool.max-wait=-1ms
      9. # 连接池中的最大空闲连接 8
      10. spring.redis.lettuce.pool.max-idle=8
      11. # 连接池中的最小空闲连接
      12. spring.redis.lettuce.pool.min-idle=0
      13. # 连接超时时间(毫秒)
      14. spring.redis.timeout=5000ms
    • 开启缓存配置,设置序列化

      重点是 @EnableCaching

       
          
      1. @Configuration
      2. @EnableCaching
      3. public class RedisConfig {
      4. @Primary
      5. @Bean
      6. public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory){
      7. RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
      8. redisCacheConfiguration = redisCacheConfiguration
      9. //设置缓存的默认超时时间:30分钟
      10. .entryTtl(Duration.ofMinutes(30L))
      11. //如果是空值,不缓存
      12. .disableCachingNullValues()
      13. //设置key序列化器
      14. .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(keySerialize()))
      15. //设置value序列化器
      16. .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerialize()));
      17. return RedisCacheManager
      18. .builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory))
      19. .cacheDefaults(redisCacheConfiguration)
      20. .build();
      21. }
      22. /**
      23. * key序列化器
      24. * @return
      25. */
      26. private RedisSerializer<String> keySerialize(){
      27. return new StringRedisSerializer();
      28. }
      29. /**
      30. * value序列化器
      31. * @return
      32. */
      33. private RedisSerializer<Object> valueSerialize(){
      34. return new GenericJackson2JsonRedisSerializer();
      35. }
      36. }
    • 使用注解进行业务开发

      ```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 {

       
          
      1. private static final String CATCH_KEY_USER="user:";
      2. @Resource
      3. private UserMapper userMapper;
      4. @Autowired
      5. private RedisTemplate redisTemplate;
      6. @Autowired
      7. private StringRedisTemplate stringRedisTemplate;
      8. public void createUser(User user) {
      9. userMapper.insertSelective(user);
      10. //缓存key
      11. String key = CATCH_KEY_USER+user.getId();
      12. //到数据库里面再重新捞出新数据出来,做缓存。
      13. user = userMapper.selectByPrimaryKey(user.getId());
      14. redisTemplate.opsForValue().set(key,user);
      15. }
      16. @CachePut(key = "#user1.id")
      17. public User updateUser(User user1) {
      18. userMapper.updateByPrimaryKeySelective(user1);
      19. return userMapper.selectByPrimaryKey(user1.getId());
      20. }
 
  1. @Cacheable(key = "#id")
  2. public User findUserById(Integer id) {
  3. return userMapper.selectByPrimaryKey(id);
  4. }
  5. @CacheEvict(key = "#id")
  6. public void deletedUser(Integer id) {
  7. userMapper.deleteByPrimaryKey(id);
  8. }
  9. }
  10. ```
  • 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>

 
  1. ==@CachePut==
  2. 🌳 @CachePut是方法级别的注解,一般用于更新缓存。
  3. ```java
  4. @CachePut(key = "#user1.id")
  5. public User updateUser(User user1) {
  6. userMapper.updateByPrimaryKeySelective(user1);
  7. return userMapper.selectByPrimaryKey(user1.getId());
  8. }
  9. ```
  10. 以上方法被调用时,先执行方法体,然后SpringCache通过返回更新缓存,即key = "user1.id",value = User
  11. ==@CacheEvict==
  12. 🌳 @CacheEvict是方法级别的注解,用于删除缓存。当方法被调用时,先执行方法体,再通过方法参数删除缓存。
  1. SpringCache的大坑小坑

    1. 对于Redis的缓存,SpringCache只支持String,其他的Hash、List、Set、ZSet都不支持。所以其他类型只能使用RedisTemplate。
    2. 对于多表查询的数据缓存,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测试

 
  1. @Test
  2. public void test4(){
  3. Integer id = 1003;
  4. String key = "article:"+id;
  5. long n = stringRedisTemplate.opsForValue().increment(key);
  6. log.info("key={},阅读量为{}",key,n);
  7. }

​ :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结构。

 
  1. @Data
  2. public class Product {
  3. //商品id
  4. private long id;
  5. //商品名称
  6. private String name;
  7. //商品价格
  8. private Integer price;
  9. //商品详情
  10. private String detail;
  11. }

==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: 京东购物车多种场景分析 :

  1. 先登录京东账号,清空以前购物车,然后添加一件商品A,保证你的购物车只有一件商品A。
  2. 退出登录,购物车添加商品B,然后关闭浏览器再打开(请问购物车的商品B是否存在?)
  3. 再次登录你的京东账号。(请问:你的购物车有几件商品?)

:taco: 图解分析:双11高并发的京东购物车技术实现

  • image-20210823011329535

  • image-20210823012155470

  • image-20210823012443099

:taco: 购物车的经典redis场景

  1. 往购物车中添加两件商品,采用hash数据结果。key = cart:user:用户id

     
      
    1. hset cart:user:1000 101 1
    2. hset cart:user:1000 102 2
    3. hgetall cart:user:1000
    4. ## 结果
    5. 1)"101"
    6. 2)"1"
    7. 3)"102"
    8. 4)"2"
  1. 修改购物车的数据,为某件商品添加数量

     
      
    1. hincrby cart:user:1000 101 1
    2. hincrby cart:user:1000 102 200
    3. hgetall cart:user:1000
    4. ## 结果
    5. 1)"101"
    6. 2)"2"
    7. 3)"102"
    8. 4)"202"
  2. 统计购物车有多少种商品

     
      
    1. hlen cart:user:1000
    2. ## 结果
    3. (integer) 2
  3. 删除购物车某件商品

     
      
    1. hdel cart:user:1000 101
    2. ## 结果
    3. (integer) 1

:taco: SpringDataRedis代码实现

  • SpringBoot+redis实现登录用户购物车功能

    ```java

     
      
    1. /**
    2. * 添加购物车
    3. * @param obj
    4. */
    5. @PostMapping("/addCart")
    6. public void addCart(Cart obj) {
    7. try {
    8. String key = CART_KEY + obj.getUserId();
    9. Boolean hashKey = redisTemplate.opsForHash().getOperations().hasKey(key);
    10. //存在
    11. if(hashKey){
    12. redisTemplate.opsForHash().put(key,obj.getProductId(),obj.getAmount());
    13. }else{
    14. redisTemplate.opsForHash().put(key,obj.getProductId(),obj.getAmount());
    15. redisTemplate.expire(key,90, TimeUnit.DAYS);
    16. }
    17. //TOOD 发rabbitmq出去
    18. }catch (Exception e){
    19. log.error(e.getMessage());
    20. }
    21. }
    22. /**
    23. * 修改购物车
    24. * @param obj
    25. */
    26. @PostMapping("/updateCart")
    27. public void updateCart(Cart obj){
    28. String key = CART_KEY+obj.getProductId();
    29. redisTemplate.opsForHash().put(key,obj.getProductId(),obj.getAmount());
    30. }
    31. /**
    32. * 查看购物车
    33. * @param userId
    34. * @return
    35. */
    36. @PostMapping("/findAll")
    37. public CartPage findAll(String userId){
    38. Long size = redisTemplate.opsForHash().size(CART_KEY+userId);
 
  1. Map<String,Integer> entries = redisTemplate.opsForHash().entries(userId);
  2. List<Cart> cartList = new ArrayList<>();
  3. for(Map.Entry<String,Integer> entry:entries.entrySet()){
  4. Cart cart = new Cart();
  5. cart.setUserId(userId);
  6. cart.setProductId(entry.getKey());
  7. cart.setAmount(String.valueOf(entry.getValue()));
  8. cartList.add(cart);
  9. }
  10. CartPage cartPage = new CartPage();
  11. cartPage.setCartList(cartList);
  12. cartPage.setCount(size);
  13. return cartPage;
  14. }
  15. /**
  16. * 删除购物车
  17. * @param userId
  18. * @param productId
  19. */
  20. @PostMapping("/del")
  21. public void delCart(Long userId,Long productId){
  22. String key = CART_KEY+userId;
  23. redisTemplate.opsForHash().delete(key,productId.toString());
  24. //TOOD 发rabbitmq出去
  25. }
 
  1. - SpringBoot+Redis+Cookies实现未登录用户的购物车
  2. ```java
  3. @RestController
  4. public class CookieCartController {
  5. @Autowired
  6. private HttpServletResponse response;
  7. @Autowired
  8. private HttpServletRequest request;
  9. @Autowired
  10. private IdGenerator idGenerator;
  11. @Autowired
  12. private RedisTemplate redisTemplate;
  13. public static final String COOKIE_KEY = "cart:cookie";
  14. /**
  15. * 添加购物车
  16. * @param obj
  17. */
  18. @PostMapping("/addCart")
  19. public void addCart(CookieCart obj){
  20. String cartId = getCookieCartId();
  21. String key = COOKIE_KEY+cartId;
  22. Boolean hashKey = redisTemplate.opsForHash().getOperations().hasKey(key);
  23. if(hashKey){
  24. redisTemplate.opsForHash().put(key,obj.getProductId(),obj.getAmount());
  25. }else{
  26. redisTemplate.opsForHash().put(key,obj.getProductId(),obj.getAmount());
  27. redisTemplate.expire(key,90, TimeUnit.DAYS);
  28. }
  29. }
  30. /**
  31. * 更新购物车
  32. * @param obj
  33. */
  34. @PostMapping("/updateCart")
  35. public void updateCart(CookieCart obj){
  36. String cartId = getCookieCartId();
  37. String key = COOKIE_KEY+cartId;
  38. redisTemplate.opsForHash().put(key,obj.getProductId(),obj.getAmount());
  39. }
  40. /**
  41. * 删除购物车
  42. */
  43. @PostMapping("/delCart")
  44. public void delCart(Long productId){
  45. String cartId = getCookieCartId();
  46. String key = COOKIE_KEY+cartId;
  47. redisTemplate.opsForHash().delete(key,productId.toString());
  48. }
  49. /**
  50. * 查看购物车
  51. */
  52. @PostMapping("/findAll")
  53. public CartPage findAll(){
  54. String cartId = getCookieCartId();
  55. String key = COOKIE_KEY+cartId;
  56. CartPage cartPages = new CartPage();
  57. List<Cart> cartPage = new ArrayList<>();
  58. Long size = redisTemplate.opsForHash().size(key);
  59. Map<String,Integer> entries = redisTemplate.opsForHash().entries(key);
  60. for(Map.Entry<String,Integer> entry: entries.entrySet()){
  61. Cart cart = new Cart();
  62. cart.setProductId(entry.getKey());
  63. cart.setAmount(String.valueOf(entry.getValue()));
  64. cartPage.add(cart);
  65. }
  66. cartPages.setCartList(cartPage);
  67. return cartPages;
  68. }
  69. private String getCookieCartId(){
  70. Cookie[] cookies = request.getCookies();
  71. if (cookies!=null){
  72. for (Cookie cookie:cookies){
  73. if (cookie.getName().equals("cartId")){
  74. return cookie.getValue();
  75. }
  76. }
  77. }
  78. long id = idGenerator.incrementId();
  79. Cookie cookie = new Cookie("cartId",String.valueOf(id));
  80. response.addCookie(cookie);
  81. return id+"";
  82. }
  83. }
  84. @Service
  85. public class IdGenerator {
  86. @Autowired
  87. private StringRedisTemplate redisTemplate;
  88. private static final String ID_KEY = "id:generator:cart";
  89. /**
  90. * 生成唯一Id
  91. * @return
  92. */
  93. public long incrementId(){
  94. return redisTemplate.opsForValue().increment(ID_KEY);
  95. }
  96. }
  • 合并登录用户和未登录用户的购物车

     
      
    1. @PostMapping("/mergeCart")
    2. public void mergeCart(Long userId){
    3. //第一步提取未登录用户的cookie的购物车数据
    4. String cartId = getCookieCartId();
    5. String keyCookie = COOKIE_KEY+cartId;
    6. Map<String,Integer> map = redisTemplate.opsForHash().entries(keyCookie);
    7. //第二步 把cookie中的购物车合并到登录用户的购物车
    8. String keyUser = "cart:user:"+userId;
    9. redisTemplate.opsForHash().putAll(keyUser,map);
    10. //第三步,删除redis未登录用户的cookie的购物车数据
    11. redisTemplate.delete(keyCookie);
    12. //第四步,删除未登录用户的cookie的cartId
    13. Cookie cookie = new Cookie("cartId",null);
    14. cookie.setMaxAge(0);
    15. response.addCookie(cookie);
    16. }
    17. zuoyong

9.Redis解决分布式系统的session不一致性问题

1. Session有什么作用

​ Session是客户端与服务端通讯会话跟踪技术,==服务端与客户端保持整个通讯的会话基本信息。==

​ 客户端在第一次访问服务端的时候,服务端会响应一个sesssionId并且把它存储到本地cookie中。在之后的访问会将cookie中的sessionId放入到请求头去访问服务器。如果通过这个sessionId没有找到对应的数据,那么服务器会创建一个新的sesssionId响应给客户端。

2. 分布式Session有什么问题?

  • image-20210824000718184

  • image-20210824005958864

3.优雅的设计session一致性,低耦合

  • UserController session中获取或存放用户信息

     
      
    1. @RequestMapping("/login")
    2. public String login(HttpServletRequest request){
    3. String username = request.getParameter("username");
    4. String password = request.getParameter("password");
    5. HttpSession session = request.getSession();
    6. session.setAttribute("username",username);
    7. session.setAttribute("password",password);
    8. return "login successful";
    9. }
    10. @PostMapping("/queryUserInfo")
    11. public String queryUserInfo(HttpServletRequest request){
    12. HttpSession session = request.getSession();
    13. String username = (String)session.getAttribute("username");
    14. String password = (String)session.getAttribute("password");
    15. return "username"+username+",password="+password;
    16. }
    17. }
  • 自定义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;

 
  1. HttpServletRequest request;
  2. HttpServletResponse response;
  3. public MyHttpServletSession(HttpServletRequest request, HttpServletResponse response) {
  4. this.request = request;
  5. this.response = response;
  6. }
  7. private ThreadLocal<String> local = new ThreadLocal<>();
  8. @Override
  9. public void setAttribute(String name, Object value) {
  10. //将我们的登录信息保存到redis中。
  11. String sessionId = getSessionFromCookie();
  12. if(sessionId!=null){
  13. sessionId = local.get();
  14. if(sessionId==null){
  15. sessionId = "user:"+ UUID.randomUUID();
  16. local.set(sessionId);
  17. }
  18. }
  19. JSONObject jsonpObject = new JSONObject();
  20. jsonpObject.put(name,value);
  21. redisTemplate.opsForValue().set(sessionId,jsonpObject.toString());
  22. //将sessionId添加到cookie中。
  23. Cookie cookie = new Cookie("sessionId", sessionId);
  24. cookie.setPath("/");
  25. response.addCookie(cookie);
  26. }
  27. @Override
  28. public Object getAttribute(String name) {
  29. String sessionFromCookie = getSessionFromCookie();
  30. String userInfo = (String) redisTemplate.opsForValue().get(sessionFromCookie);
  31. JSONObject jsonObject = JSONObject.parseObject(userInfo);
  32. if (jsonObject.containsKey(name)){
  33. return jsonObject.get(name);
  34. }
  35. return null;
  36. }
  37. private String getSessionFromCookie(){
  38. Cookie[] cookies = request.getCookies();
  39. if(cookies!=null){
  40. for (Cookie cookie:cookies){
  41. if("sessionId".equals(cookie.getName())){
  42. return cookie.getValue();
  43. }
  44. }
  45. };
  46. return null;
  47. }
  48. @Override
  49. public long getCreationTime() {
  50. return 0;
  51. }
  52. @Override
  53. public String getId() {
  54. return null;
  55. }
  56. @Override
  57. public long getLastAccessedTime() {
  58. return 0;
  59. }
  60. @Override
  61. public ServletContext getServletContext() {
  62. return null;
  63. }
  64. @Override
  65. public void setMaxInactiveInterval(int i) {
  66. }
  67. @Override
  68. public int getMaxInactiveInterval() {
  69. return 0;
  70. }
  71. @Override
  72. public HttpSessionContext getSessionContext() {
  73. return null;
  74. }
  75. @Override
  76. public Object getValue(String s) {
  77. return null;
  78. }
  79. @Override
  80. public Enumeration<String> getAttributeNames() {
  81. return null;
  82. }
  83. @Override
  84. public String[] getValueNames() {
  85. return new String[0];
  86. }
  87. @Override
  88. public void putValue(String s, Object o) {
  89. }
  90. @Override
  91. public void removeAttribute(String s) {
  92. }
  93. @Override
  94. public void removeValue(String s) {
  95. }
  96. @Override
  97. public void invalidate() {
  98. }
  99. @Override
  100. public boolean isNew() {
  101. return false;
  102. }

}

 
  1. - 自定义HttpServlet对象,保证获取到的是我们自定义的Session(一致性)
  2. ```java
  3. package com.example.demo.filter;
  4. import javax.servlet.http.HttpServletRequest;
  5. import javax.servlet.http.HttpServletRequestWrapper;
  6. import javax.servlet.http.HttpServletResponse;
  7. import javax.servlet.http.HttpSession;
  8. public class MyHttpServletRequest extends HttpServletRequestWrapper {
  9. HttpServletRequest request;
  10. HttpServletResponse response;
  11. public MyHttpServletRequest(HttpServletRequest request, HttpServletResponse response){
  12. super(request);
  13. this.request = request;
  14. this.response = response;
  15. }
  16. @Override
  17. public HttpSession getSession() {
  18. //返回我们自己创建的session对象
  19. return new MyHttpServletSession(request,response);
  20. }
  21. }
  • 自定义过滤器,拦截请求

    ```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 {

 
  1. @Override
  2. public void init(FilterConfig filterConfig) throws ServletException {
  3. }
  4. @Override
  5. public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
  6. HttpServletRequest request = new MyHttpServletRequest((HttpServletRequest) servletRequest,(HttpServletResponse) servletResponse);
  7. filterChain.doFilter(request,servletResponse);
  8. }
  9. @Override
  10. public void destroy() {
  11. }

}

 
  1. - 启动器上指定自定义过滤器包路径
  2. ```java
  3. package com.example.demo;
  4. import org.springframework.boot.SpringApplication;
  5. import org.springframework.boot.autoconfigure.SpringBootApplication;
  6. import org.springframework.boot.web.servlet.ServletComponentScan;
  7. import tk.mybatis.spring.annotation.MapperScan;
  8. @SpringBootApplication
  9. @MapperScan("com.example.demo.mapper")
  10. @ServletComponentScan(basePackages = {"com.example.demo"})
  11. public class DemoApplication {
  12. public static void main(String[] args) {
  13. SpringApplication.run(DemoApplication.class, args);
  14. }
  15. }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值