Redis

168 阅读31分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。


写在前面

黑马程序员Redis入门到实战教程,全面透析redis底层原理+redis分布式锁+企业解决方案+redis实战

 175P
 ​
 # 知识全面
 ​
 # 与时俱进
 # 贴合企业开发 
 黑马点评网
 学到了直接拿到公司里面用 
 ​
 # 理论结合实际
 理论背熟,没写代码,心里没底,不够自信。一行一行手写代码,要自信。 
 ​
 # 由浅入深
 常用操作烂熟于心
 高级篇偏向运维
 原理篇深入底层
 ​
 ## 通俗易懂

实用篇

 # 初识Redis
 - 认识NoSQL
 - 认识Redis
 - 安装Redis
 ​
 # Redis常见命令
 - 5种常见数据结构
 - 通用命令
 - 不同数据结构的操作命令
 ​
 # Redis的Javae客户端
 - Jedis客户端
 - SpringDataRedis客户端

1. 初识Redis

键值型数据库,key-value

1.1 认识NoSQL

 # 结构化&非结构化
 # 关联的&无关联的
 # SQL查询&非SQL(语法格式不统一) 
 # 事务ACID&BASE

1.2 认识Redis

Redis诞生于2009年全称是Remote Dictionary Server,远程词典服务器,是一个基于内存的键值型NoSQL数据库。

特征:

  • 键值(key-value)型,value支持多种不同数据结构,功能丰富

  • 单线程,每个命令具备原子性

    • 6.0只是处理网络请求多线程,核心命令执行依然单线程
  • 低延迟,速度快(基于内存、IO多路复用、良好的编码)

    • 内存存储数据相比磁盘(核心)
    • IO多路复用提高了吞吐能力
    • C语言写的
  • 支持数据持久化

    • 解决安全性问题
  • 支持主从集群、分片集群

    • 从结点备份主节点的数据
    • 数据拆分到不同的结点,水平扩展
  • 支持多语言客户端

 8种不同的数据结构,2种消息队列,4个运维操作,Script脚本和事务操作。
 ​
 包含各个版本的新特性。

1.3 Linux安装

Redis是基于C语言编写的,因此首先需要安装Redis所需要的gcc依赖:

 yum install -y gcc tcl

Redis安装包上传到虚拟机的任意目录,解压缩

 tar -xzf redis-6.2.6.tar.gz

进入redis目录,编译

 make && make install
 默认的安装路径是在 `/usr/local/bin`目录下,该目录已经默认配置环境变量,因此可以在任意目录下运行这些命令。
 ​
 - redis-cli:是redis提供的命令行客户端
 - redis-server:是redis的服务端启动脚本
 - redis-sentinel:是redis的哨兵启动脚本  

1.4 三种启动方式

默认启动(不推荐)

安装完成后,在任意目录输入redis-server命令即可启动Redis。这种启动属于前台启动,会阻塞整个会话窗口,窗口关闭或者按下CTRL + C则Redis停止。不推荐使用。

 redis-server

指定配置启动

如果要让Redis以后台方式启动,则必须修改Redis配置文件,就在我们之前解压的redis安装包下(/usr/local/src/redis-6.2.6),名字叫redis.conf:

我们先将这个配置文件备份一份:

 cp redis.conf redis.conf.bck

然后修改redis.conf文件中的一些配置:

 # 允许访问的地址,默认是127.0.0.1,会导致只能在本地访问。修改为0.0.0.0则可以在任意IP访问,生产环境不要设置为0.0.0.0
 bind 0.0.0.0
 # 守护进程,修改为yes后即可后台运行
 daemonize yes 
 # 密码,设置后访问Redis必须输入密码
 requirepass 123456

Redis的其它常见配置:

 # 监听的端口
 port 6379
 # 工作目录,默认是当前目录,也就是运行redis-server时的命令,日志、持久化等文件会保存在这个目录
 dir .
 # 数据库数量,设置为1,代表只使用1个库,默认有16个库,编号0~15
 databases 1
 # 设置redis能够使用的最大内存
 maxmemory 512mb
 # 日志文件,默认为空,不记录日志,可以指定日志文件名
 logfile "redis.log"

启动Redis:

 # 进入redis安装目录 
 cd /usr/local/src/redis-6.2.6
 # 启动
 redis-server redis.conf

停止服务:

 # 利用redis-cli来执行 shutdown 命令,即可停止 Redis 服务,
 # 因为之前配置了密码,因此需要通过 -u 来指定密码
 redis-cli -u 123321 shutdown

开机自启

我们也可以通过配置来实现开机自启。

首先,新建一个系统服务文件:

 vi /etc/systemd/system/redis.service

内容如下:

 [Unit]
 Description=redis-server
 After=network.target
 ​
 [Service]
 Type=forking
 ExecStart=/usr/local/bin/redis-server /usr/local/src/redis-6.2.6/redis.conf
 PrivateTmp=true
 ​
 [Install]
 WantedBy=multi-user.target

然后重载系统服务:

 systemctl daemon-reload

现在,我们可以用下面这组命令来操作redis了:

 # 启动
 systemctl start redis
 # 停止
 systemctl stop redis
 # 重启
 systemctl restart redis
 # 查看状态
 systemctl status redis

执行下面的命令,可以让redis开机自启:

 systemctl enable redis
 ​
 # Created symlink from /etc/systemd/system/multi-user.target.wants/redis.service to /etc/systemd/system/redis.service.

启动Redis警告:WARNING overcommit_memory is set to 0 Background save may fail under low memory condition

blog.csdn.net/cnwyt/artic…

1.5 客户端

安装完成Redis,我们就可以操作Redis,实现数据的CRUD了。这需要用到Redis客户端,包括:

命令行客户端 redis-cli

Redis安装完成后就自带了命令行客户端:redis-cli,使用方式如下:

 redis-cli [options] [commonds]

其中常见的有:

  • -h 127.0.0.1:指定要连接的redis节点的IP地址,默认是127.0.0.1
  • -p 6379:指定要连接的redis节点的端口,默认是6379
  • -a 123321:指定redis的访问密码

其中的commonds就是Redis的操作命令,例如:

  • ping:与redis服务端做心跳测试,服务端正常会返回pong

不指定commond时,会进入redis-cli的交互控制台。

图形化桌面客户端 Github大神编写

Redis默认有16个仓库,编号从0至15。通过配置文件可以设置仓库数量,但是不超过16,并且不能自定义仓库名称。

如果是基于redis-cli连接Redis服务,可以通过select命令来选择数据库:

 # 选择 0号库
 select 0

编程客户端

2. Redis常用命令

Redis数据结构介绍

Redis为了方便我们学习,将操作不同数据类型的命令也做了分组,在官网( redis.io/commands )可以查看到不同的命令

Redis通用命令

所有数据类型都可以使用的指令,常见的有:

  • KEYS:查看符合模板的所有key
  • DEL:删除一个指定的key
  • EXISTS:判断key是否存在
  • EXPIRE:给一个key设置有效期,有效期到期时该key会被自动删除
  • TTL:查看一个KEY的剩余有效期

通过help [command] 可以查看一个命令的具体用法,例如:

String类型

Redis中最简单的存储类型。

不过根据字符串的格式不同,又可以分为3类:

  • string:普通字符串
  • int:整数类型,可以做自增、自减操作
  • float:浮点类型,可以做自增、自减操作

不管是哪种格式,底层都是字节数组形式存储,只不过是编码方式不同。字符串类型的最大空间不能超过512m。

命令说明
SET添加或者修改已经存在的一个String类型的键值对1
GET根据key获取String类型的value1
MSET批量添加多个String类型的键值对1
MGET根据多个key获取多个String类型的value1
INCR让一个整型的key自增12
INCRBY让一个整型的key自增并指定步长,例如:incrby num 2 让num值自增22
INCRBYFLOAT让一个浮点类型的数字自增并指定步长2
SETNX添加一个String类型的键值对,前提是这个key不存在,否则不执行3重要
SETEX添加一个String类型的键值对,并且指定有效期3

key的层级格式

例如,需要存储用户、商品信息到redis,有一个用户id是1,有一个商品id恰好也是1

key

Redis的key允许有多个单词形成层级结构,多个单词之间用':'隔开,格式如下:

 # 项目名:业务名:类型:id
 ​
 user相关的key:heima:user:1
 product相关的key:heima:product:1
value

如果Value是一个Java对象,例如一个User对象,则可以将对象序列化为JSON字符串后存储

KEYVALUE
heima:user:1{"id":1, "name": "Jack", "age": 21}
heima:product:1{"id":1, "name": "小米11", "price": 4999}

Hash类型

Hash类型,也叫散列,其value是一个无序字典,类似于Java中的HashMap结构。

String结构是将对象序列化为JSON字符串后存储,当需要修改对象某个字段时很不方便。

Hash结构可以将对象中的每个字段独立存储,可以针对单个字段做CRUD

KEYvaluevalue
fieldvalue
heima:user:1nameJack
age21
heima:user:2nameRose
age18

常见命令:

命令说明
HSET key field value添加或者修改hash类型key的field的值
HGET key field获取一个hash类型key的field的值
HMSET批量添加多个hash类型key的field的值

List类型(存储有序数据)

Redis中的List类型与Java中的LinkedList类似,可以看做是一个双向链表结构。既可以支持正向检索和也可以支持反向检索。

特征也与LinkedList类似:

  • 有序
  • 元素可以重复
  • 插入和删除快
  • 查询速度一般

常用来存储一个有序数据,例如:朋友圈点赞列表,评论列表等。

命令说明
LPUSH key element ...向列表左侧插入一个或多个元素
LPOP key移除并返回列表左侧的第一个元素,没有则返回nil
RPUSH key element ...向列表右侧插入一个或多个元素
RPOP key移除并返回列表右侧的第一个元素
LRANGE key star end返回一段角标范围内的所有元素
BLPOP和BRPOP与LPOP和RPOP类似,只不过在没有元素时等待指定时间,而不是直接返回nil

image-20220504223741051

 # 如何利用List结构模拟一个栈?
 入口和出口在同一边
 ​
 # 如何利用List结构模拟一个队列?
 入口和出口在不同边
 ​
 # 如何利用List结构模拟一个阻塞队列?
 - 入口和出口在不同边
 - 出队时采用BLPOP或BRPOP

Set类型

Redis的Set结构与Java中的HashSet类似,可以看做是一个value为null的HashMap。

因为也是一个hash表,因此具备与HashSet类似的特征:

  • 无序
  • 元素不可重复
  • 查找快
  • 支持交集、并集、差集等功能

常见命令:

命令说明
SADD key member...向set中添加一个或多个元素1
SREM key member...移除set中的指定元素1
SCARD key返回set中元素的个数1
SISMEMBER key member判断一个元素是否存在于set中1
SMEMBERS获取set中的所有元素1
SINTER key1 key2 ...求key1与key2的交集2
SDIFF key1 key2 ...差集2
SUNION key1 key2 ...并集2

练习

 # 将下列数据用Redis的Set集合来存储:
 张三的好友有:李四、王五、赵六
 李四的好友有:王五、麻子、二狗
 ​
 # 利用Set的命令实现下列功能: 
 计算张三的好友有几人
 计算张三和李四有哪些共同好友
 查询哪些人是张三的好友却不是李四的好友
 查询张三和李四的好友总共有哪些人
 判断李四是否是张三的好友
 判断张三是否是李四的好友
 将李四从张三的好友列表中移除

SortedSet类型(排行榜)

Redis的SortedSet是一个可排序的set集合,与Java中的TreeSet有些类似,但底层数据结构却差别很大。

SortedSet中的每一个元素都带有一个score属性,可以基于score属性对元素排序,底层的实现是一个跳表(SkipList)加 hash表。

SortedSet具备下列特性:

  • 可排序
  • 元素不重复
  • 查询速度快

因为SortedSet的可排序特性,经常被用来实现排行榜这样的功能。

常见命令

练习

 将班级的下列学生得分存入Redis的SortedSet中:
 Jack 85, Lucy 89, Rose 82, Tom 95, Jerry 78, Amy 92, Miles 76
 并实现下列功能:
 ​
 删除Tom同学
 获取Amy同学的分数
 获取Rose同学的排名
 查询80分以下有几个学生
 给Amy同学加2分
 查出成绩前3名的同学
 查出成绩80分以下的所有同学

3. Redis客户端

在Redis官网中提供了各种语言的客户端,地址:redis.io/clients

3.1 Jedis

Jedis的官网地址: github.com/redis/jedis,我们先来个快速入门

 # 使用
 引入依赖;
 创建Jedis对象,建立连接;
 使用Jedis,方法名与Redis命令一致;
 释放资源;

1

 <dependency>
     <groupId>redis.clients</groupId>
     <artifactId>jedis</artifactId>
     <version>3.7.0</version>
 </dependency>

2

3

4

 //@RunWith(SpringJUnit4ClassRunner.class)
 //@SpringBootTest(classes=SpringApplicationTest.class)
 public class SpringApplicationTest {
     private Jedis jedis;
 ​
     @Test
     @Before  // Junit4,在每个测试方法之前执行。注解在非静态方法上。
     // @BeforeEach  Junit5
     public void testSetUp() {
         jedis = new Jedis("39.101.189.62", 6379);
         jedis.auth("123456");
         jedis.select(2);
     }
 ​
     @Test
     public void testString() {
         System.out.println("-------------插入数据---------------");
         String result = jedis.set("name", "李四");
         System.out.println("result: " + result);
         
         System.out.println("-------------根据key获取数据---------------");
         String name = jedis.get("name");
         System.out.println("name: " + name);
     }
 ​
     @Test
     @After  // Junit4,在每个测试方法之后执行。注解在非静态方法上。
     // @AfterEach  Junit5
     public void testDown() {
         // 释放资源
         if (jedis != null){
             jedis.close();
         }
     }
 ​
 }

Jedis连接池

Jedis本身是线程不安全的,并且频繁的创建和销毁连接会有性能损耗,因此我们推荐大家使用Jedis连接池代替Jedis的直连方式

 @Component
 public class JedisConnectionFactory {
 ​
     private static final JedisPool jedisPool;
 ​
     static {
         JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
         // 最大连接
         jedisPoolConfig.setMaxTotal(8);
         // 最大空闲连接
         jedisPoolConfig.setMaxIdle(8);
         // 最小空闲连接
         jedisPoolConfig.setMinIdle(4);
         // 设置最长等待时间, ms
         jedisPoolConfig.setMaxWaitMillis(200);
         jedisPool = new JedisPool(jedisPoolConfig, "39.101.189.62", 6379,
                 1000, "123456");
     }
 ​
     // 获取Jedis对象
     public static Jedis getJedis() {
         return jedisPool.getResource();
     }
 ​
 }

3.2 SpringDataRedis 2.6.0

说明

SpringData是Spring中数据操作的模块,包含对各种数据库的集成

其中对Redis的集成模块就叫做SpringDataRedis,官网地址:spring.io/projects/sp…

SpringDataRedis中提供了RedisTemplate工具类,其中封装了各种对Redis的操作。

并且将不同数据类型的操作API封装到了不同的类型中:

填坑之路!Srpingboot+Redis redisTemplate 做测试时候报空指针异常(注入为NULL)

stringRedisTemplate报空指针错误,原因是注入时候stringRedisTemplate就是null

查了一下后面发现是容器没有启动,单纯的测试是无法注入的.要在测试类上加上

 @RunWith(SpringRunner.class)
 @SpringBootTest

这样子启动的话是启动spring容器来执行测试。加上后就会启动Spring ,这算是一个小小的坑。

具体参考:blog.csdn.net/weixin_4223…

使用

 SpringDataRedis的使用步骤:
 ​
 - 引入spring-boot-starter-data-redis依赖
 - 在application.yml配置Redis信息
 - 注入RedisTemplate使用

1 引入依赖

 <artifactId>Redis_02_SpringDataRedis</artifactId>
 ​
     <!--Redis依赖-->
     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-data-redis</artifactId>
     </dependency>
     <!--连接池依赖-->
     <!--底层基于实现连接池技术-->
     <dependency>
         <groupId>org.apache.commons</groupId>
         <artifactId>commons-pool2</artifactId>
     </dependency>

2 配置

 spring:
   redis:
     host: 39.101.189.62
     port: 6379
     password: 123456
     lettuce:
       pool:
         max-active: 8 # 最大连接
         max-idle: 8 # 最大空闲连接
         min-idle: 0 # 最小空闲连接
         max-wait: 100 # 连接等待时间

3 注入RedisTemplate

4 编写测试

 @RunWith(SpringRunner.class)
 @SpringBootTest
 public class RedisTest {
 ​
     @Autowired
     private RedisTemplate redisTemplate;
 ​
     @Test
     public void testString() {
         // 插入一条string类型数据
         redisTemplate.opsForValue().set("name", "李四");
         // 读取一条string类型数据
         Object name = redisTemplate.opsForValue().get("name");
         System.out.println("name = " + name);
     }
 }

序列化方式

RedisTemplate可以接收任意Object作为值写入Redis,只不过写入前会把Object序列化为字节形式默认是采用

JDK序列化,得到的结果是这样的:

缺点:

  • 可读性差
  • 内存占用较大
使用JSON序列化器

我们可以自定义RedisTemplate的序列化方式,代码如下:

  • key和 hashKey采用 string序列化
  • value和 hashValue采用 JSON序列化
 @Configuration
 public class JsonSerializable {
 ​
     @Bean
     public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
         // 创建Template
         RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
         // 设置连接工厂
         redisTemplate.setConnectionFactory(redisConnectionFactory);
 ​
         // 设置序列化工具
         GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
         // key和 hashKey采用 string序列化
         redisTemplate.setKeySerializer(RedisSerializer.string());
         redisTemplate.setHashKeySerializer(RedisSerializer.string());
         // value和 hashValue采用 JSON序列化
         redisTemplate.setValueSerializer(jsonRedisSerializer);
         redisTemplate.setHashValueSerializer(jsonRedisSerializer);
         return redisTemplate;
     }
 ​
 }

结果:

尽管JSON的序列化方式可以满足我们的需求,但依然存在一些问题,如图:

为了在反序列化时知道对象的类型,JSON序列化器会将类的class类型写入json结果中,存入Redis,会带来额外的内存开销。

String序列化器

为了节省内存空间,我们并不会使用JSON序列化器来处理value。

而是统一使用String序列化器,要求只能存储String类型的key和value。当需要存储Java对象时,手动完成对象的序列化和反序列化。

Spring默认提供了一个StringRedisTemplate类,它的key和value的序列化方式默认就是String方式。省去了我们自定义RedisTemplate的过程:

 @RunWith(SpringRunner.class)
 @SpringBootTest
 public class StringRedisTemplateTest {
 ​
     @Autowired
     private StringRedisTemplate stringRedisTemplate;
     // SpringMVC 的Json处理工具
     private static final ObjectMapper mapper = new ObjectMapper();
 ​
 ​
     @Test
     public void testStringRedisTemplate() throws JsonProcessingException {
         // 准备对象
         User user = new User("admin","123456");
         // 手动序列化
         String valueAsString = mapper.writeValueAsString(user);
         // 写入一条数据到Redis
         stringRedisTemplate.opsForValue().set("user",valueAsString);
 ​
         // 读取数据
         String userString = stringRedisTemplate.opsForValue().get("user");
         // 反序列化
         User userValue = mapper.readValue(userString, User.class);
         System.out.println(userValue);
     }
 ​
 }
 # RedisTemplate的两种序列化实践方案:
 方案一:
 自定义RedisTemplate
 修改RedisTemplate的序列化器为GenericJackson2JsonRedisSerializer
 ​
 方案二:
 使用StringRedisTemplate
 写入Redis时,手动把对象序列化为JSON
 读取Redis时,手动把读取到的JSON反序列化为对象

4. Redis使用场景(企业实战)

代码地址:gitee.com/huyi612/hm-…

仓库拉下来的代码,把处理订单的(VoucherOrderServiceImpl) 里面那两个死循环的方法注释掉,再启动,然后等后边讲到了再打开就好了

config包下面的 RedissonConfig里面的redis地址也要换一下

4.1 短信登陆(共享session)

(1)导入黑马点评项目

项目架构

 # 导入SQL文件:hmdp.sql
 tb_user:用户表
 tb_user_info:用户详情表
 tb_shop:商户信息表
 tb_shop_type:商户类型表
 tb_blog:用户日记表(达人探店日记)
 tb_follow:用户关注表
 tb_voucher:优惠券表
 tb_voucher_order:优惠券的订单表
 ​
 # 后端项目 hm-dianping
 https://gitee.com/huyi612/hm-dianping
 在浏览器访问:http://localhost:8081/shop-type/list ,如果可以看到数据则证明运行没有问题
 ​
 # 前端项目
 nginx-1.18.0
 直接下载并解压到任意位置,把nginx的html目录下的 hmdp文件拷贝到你自己的nginx的html目录下即可。
 总之:把html文件和nginx.conf替换掉就行了。
 注意,如果brew方式安装,conf配置文件需要修改。

(2)基于Session实现登陆

发送短信验证码
 @PostMapping("code")
 public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
     // 发送短信验证码并保存验证码
     return userService.sendCode(phone, session);
 }
 public Result sendCode(String phone, HttpSession session) {
     // 1.校验手机号
     if (RegexUtils.isPhoneInvalid(phone)) {
         // 2.如果不符合,返回错误信息
         return Result.fail("手机号格式错误!");
     }
     // 3.符合,生成验证码
     String code = RandomUtil.randomNumbers(6);
 ​
     // 4.保存验证码到 session
     session.setAttribute("code", code);
     // 4.保存验证码到 redis
     //stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
 ​
     // 5.发送验证码
     log.debug("发送短信验证码成功,验证码:{}", code);
     // 返回ok
     return Result.ok();
 }
短信验证码登陆

 /**
  * 登录功能
  * @param loginForm 登录参数:包含手机号、验证码;或者手机号、密码
  */
 @PostMapping("/login")
 public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
     // 实现登录功能
     return userService.login(loginForm, session);
 }
 public Result login(LoginFormDTO loginForm, HttpSession session) {
     // 1.校验手机号
     String phone = loginForm.getPhone();
     if (RegexUtils.isPhoneInvalid(phone)) {
         // 2.如果不符合,返回错误信息
         return Result.fail("手机号格式错误!");
     }
     // 3.从redis获取验证码并校验
     String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
     String code = loginForm.getCode();
     if (cacheCode == null || !cacheCode.equals(code)) {
         // 不一致,报错
         return Result.fail("验证码错误");
     }
 ​
     // 4.一致,根据手机号查询用户 select * from tb_user where phone = ?
     User user = query().eq("phone", phone).one();
 ​
     // 5.判断用户是否存在
     if (user == null) {
         // 6.不存在,创建新用户并保存
         user = createUserWithPhone(phone);
     }
 ​
     // 7.保存用户信息到 redis中
     // 7.1.随机生成token,作为登录令牌
     String token = UUID.randomUUID().toString(true);
     // 7.2.将User对象转为HashMap存储
     UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
     Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
             CopyOptions.create()
                     .setIgnoreNullValue(true)
                     .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
     // 7.3.存储
     String tokenKey = LOGIN_USER_KEY + token;
     stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
     // 7.4.设置token有效期
     stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
 ​
     // 8.返回token
     return Result.ok(token);
 }
 ​
 private User createUserWithPhone(String phone) {
     // 1.创建用户
     User user = new User();
     user.setPhone(phone);
     user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
     // 2.保存用户
     save(user);
     return user;
 }
登录验证功能

设置一个拦截器

 package com.hmdp.utils;
 ​
 import com.hmdp.dto.UserDTO;
 import org.springframework.web.servlet.HandlerInterceptor;
 ​
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import javax.servlet.http.HttpSession;
 ​
 public class LoginInterceptor2 implements HandlerInterceptor {
     /**
      * @description 前置拦截
      * @updateTime 2022/3/13 上午9:45
      */
     @Override
     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
         // 1,获取Session
         HttpSession session = request.getSession();
         // 2,获取Session中的用户
         Object user = session.getAttribute("user");
         // 3,判断用户是否存在
         if (user == null){
             // 4,不存在,拦截,返回401状态码
             response.setStatus(401);
             return false;
         }
         // 5,存在,保存用户信息到ThreadLocal  (ThreadLocal的实现原理可以自己下来学习下)
         UserHolder.saveUser((UserDTO) user);
         // 6,放行
         return true;
     }
 ​
     @Override
     public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
         // 移除用户
         UserHolder.removeUser();
     }
 }

配置拦截器(使其生效)

 import com.hmdp.utils.LoginInterceptor;
 import com.hmdp.utils.RefreshTokenInterceptor;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.data.redis.core.StringRedisTemplate;
 import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 ​
 import javax.annotation.Resource;
 ​
 @Configuration
 public class MvcConfig implements WebMvcConfigurer {
 ​
     @Resource
     private StringRedisTemplate stringRedisTemplate;
 ​
     @Override
     public void addInterceptors(InterceptorRegistry registry) {
         // 登录拦截器
         registry.addInterceptor(new LoginInterceptor())
                 .excludePathPatterns(
                         "/shop/**",
                         "/voucher/**",
                         "/shop-type/**",
                         "/upload/**",
                         "/blog/hot",
                         "/user/code",
                         "/user/login"
                 ).order(1);
         // token刷新的拦截器
         registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
     }
 }

验证通过后返回用户

 @GetMapping("/me")
 public Result me(){
     // 获取当前登录的用户并返回
     UserDTO user = UserHolder.getUser();
     return Result.ok(user);
 }

(3)集群的Session共享问题

(4)基于Redis实现共享session登陆

基于Redis代替session

 // 4.保存验证码到 Redis
 stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);

 # Redis代替session需要考虑的问题:
 ​
 选择合适的数据结构
 选择合适的key
 选择合适的存储粒度(并未存储用户完整信息,而是部分信息UserDTO)
登陆拦截器的优化(刷新 + 拦截)

原有拦截器基础上加一个新的拦截器:因为目前来说,如果用户一直访问的都是不需要拦截的请求,那么30分钟后用户的状态还是消失了。

新拦截器做刷新Token的动作,也即token续期。原拦截器才是实现拦截功能。

 public class LoginInterceptor implements HandlerInterceptor {
 ​
     public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {
         this.stringRedisTemplate = stringRedisTemplate;
     }
 ​
     @Override
     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
         // 1.判断是否需要拦截(ThreadLocal中是否有用户)
         if (UserHolder.getUser() == null) {
             // 没有,需要拦截,设置状态码
             response.setStatus(401);
             // 拦截
             return false;
         }
         // 有用户,则放行
         return true;
     }
 }
 package com.hmdp.utils;
 ​
 import cn.hutool.core.bean.BeanUtil;
 import cn.hutool.core.util.StrUtil;
 import com.hmdp.dto.UserDTO;
 import org.springframework.data.redis.core.StringRedisTemplate;
 import org.springframework.web.servlet.HandlerInterceptor;
 ​
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
 ​
 import static com.hmdp.utils.RedisConstants.LOGIN_USER_KEY;
 import static com.hmdp.utils.RedisConstants.LOGIN_USER_TTL;
 ​
 public class RefreshTokenInterceptor implements HandlerInterceptor {
 ​
     private StringRedisTemplate stringRedisTemplate;
 ​
     public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
         this.stringRedisTemplate = stringRedisTemplate;
     }
 ​
     @Override
     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
         // 1.获取请求头中的token
         String token = request.getHeader("authorization");
         if (StrUtil.isBlank(token)) {
             return true;
         }
         // 2.基于TOKEN获取redis中的用户
         String key  = LOGIN_USER_KEY + token;
         Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
         // 3.判断用户是否存在
         if (userMap.isEmpty()) {
             return true;
         }
         // 5.将查询到的hash数据转为UserDTO
         UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
         // 6.存在,保存用户信息到 ThreadLocal
         UserHolder.saveUser(userDTO);
         // 7.刷新token有效期
         stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
         // 8.放行
         return true;
     }
 ​
     @Override
     public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
         // 移除用户
         UserHolder.removeUser();
     }
 }
 package com.hmdp.config;
 ​
 import com.hmdp.utils.LoginInterceptor;
 import com.hmdp.utils.RefreshTokenInterceptor;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.data.redis.core.StringRedisTemplate;
 import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 ​
 import javax.annotation.Resource;
 ​
 @Configuration
 public class MvcConfig implements WebMvcConfigurer {
 ​
     @Resource
     private StringRedisTemplate stringRedisTemplate;
 ​
     @Override
     public void addInterceptors(InterceptorRegistry registry) {
         // 登录拦截器
         registry.addInterceptor(new LoginInterceptor())
                 .excludePathPatterns(
                         "/shop/**",
                         "/voucher/**",
                         "/shop-type/**",
                         "/upload/**",
                         "/blog/hot",
                         "/user/code",
                         "/user/login"
                 ).order(1);
         // token刷新的拦截器
         //设置order为0,优先级更高,先执行
         registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
     }
 }


4.2 商品查询缓存(缓存使用技巧、缓存雪崩和穿透问题)

借助商户查询业务,分析缓存使用的技巧,还有常见问题的解决方案。

什么是缓存

缓存就是数据交换的缓冲区(称作Cache [ kæʃ ] ),是存贮数据的临时地方,一般读写性能较高。

中小型企业,数据量不大的情况下,也可以不使用缓存。

添加Redis缓存

未命中,查询数据库后需要写缓存到Redis

public Result queryById2(Long id) {
    // 1.从Redis查询店铺缓存
    String key = CACHE_SHOP_KEY + id;
    String shopJson = stringRedisTemplate.opsForValue().get(key);
    // 2.判断是否存在
    if (StrUtil.isNotBlank(shopJson)){
        // 3.存在,直接返回
        Shop shop = JSONUtil.toBean(shopJson, Shop.class);
        return Result.ok(shop);
    }
    // 4.不存在,根据id查询数据库
    Shop shop = getById(id);

    // 5.不存在,返回错误
    if (shop == null){
        return Result.fail("店铺不存在!");
    }
    stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop));
    // 6.存在,存入Redis
    // 7.返回
    return Result.ok(shop);
}

应用:给店铺类型查询业务添加缓存(店铺类型在首页和其它多个页面都会用到),流程如上图。

缓存更新策略

数据一致性问题

业务场景: 低一致性需求:使用内存淘汰机制。例如店铺类型的查询缓存 高一致性需求:主动更新,并以超时剔除作为兜底方案。例如店铺详情查询的缓存

存在的线程安全问题(发生频率其实较高:查写缓存都很快,更新数据库较慢)

先删除缓存,后操作数据库

先操作数据库,再删除缓存(胜出)

案例:给查询商铺的缓存添加超时剔除和主动更新的策略

修改ShopController中的业务逻辑,满足下面的需求:

  • 根据id查询店铺时,如果缓存未命中,则查询数据库,将数据库结果写入缓存,并设置超时时间
  • 根据id修改店铺时,先修改数据库,再删除缓存


缓存穿透

缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。 常见的解决方案有2种:

  • 缓存空对象
  • 布隆过滤

缓存雪崩

缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

解决方案:

  • 给不同的Key的TTL添加随机值
  • 利用Redis集群提高服务的可用性
  • 给缓存业务添加降级限流策略
  • 给业务添加多级缓存

缓存击穿(热点Key问题)

缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

常见的解决方案有两种:

  • 互斥锁
  • 逻辑过期

案例:基于互斥锁方式解决缓存击穿问题 需求:修改根据id查询商铺的业务,基于互斥锁方式来解决缓存击穿问题

public <R, ID> R queryWithMutex(
        String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
    String key = keyPrefix + id;
    // 1.从redis查询商铺缓存
    String shopJson = stringRedisTemplate.opsForValue().get(key);
    // 2.判断是否存在
    if (StrUtil.isNotBlank(shopJson)) {
        // 3.存在,直接返回
        return JSONUtil.toBean(shopJson, type);
    }
    // 判断命中的是否是空值
    if (shopJson != null) {
        // 返回一个错误信息
        return null;
    }

    // 4.实现缓存重建
    // 4.1.获取互斥锁
    String lockKey = LOCK_SHOP_KEY + id;
    R r = null;
    try {
        boolean isLock = tryLock(lockKey);
        // 4.2.判断是否获取成功
        if (!isLock) {
            // 4.3.获取锁失败,休眠并重试
            Thread.sleep(50);
            return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);
        }
        // 4.4.获取锁成功,根据id查询数据库
        r = dbFallback.apply(id);
        // 5.不存在,返回错误
        if (r == null) {
            // 将空值写入redis
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            // 返回错误信息
            return null;
        }
        // 6.存在,写入redis
        this.set(key, r, time, unit);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }finally {
        // 7.释放锁
        unlock(lockKey);
    }
    // 8.返回
    return r;
}

基于逻辑过期方式解决缓存击穿问题 需求:修改根据id查询商铺的业务,基于逻辑过期方式来解决缓存击穿问题

public <R, ID> R queryWithLogicalExpire(
        String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
    String key = keyPrefix + id;
    // 1.从redis查询商铺缓存
    String json = stringRedisTemplate.opsForValue().get(key);
    // 2.判断是否存在
    if (StrUtil.isBlank(json)) {
        // 3.存在,直接返回
        return null;
    }
    // 4.命中,需要先把json反序列化为对象
    RedisData redisData = JSONUtil.toBean(json, RedisData.class);
    R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
    LocalDateTime expireTime = redisData.getExpireTime();
    // 5.判断是否过期
    if(expireTime.isAfter(LocalDateTime.now())) {
        // 5.1.未过期,直接返回店铺信息
        return r;
    }
    // 5.2.已过期,需要缓存重建
    // 6.缓存重建
    // 6.1.获取互斥锁
    String lockKey = LOCK_SHOP_KEY + id;
    boolean isLock = tryLock(lockKey);
    // 6.2.判断是否获取锁成功
    if (isLock){
        // 6.3.成功,开启独立线程,实现缓存重建
        CACHE_REBUILD_EXECUTOR.submit(() -> {
            try {
                // 查询数据库
                R newR = dbFallback.apply(id);
                // 重建缓存
                this.setWithLogicalExpire(key, newR, time, unit);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }finally {
                // 释放锁
                unlock(lockKey);
            }
        });
    }
    // 6.4.返回过期的商铺信息
    return r;
}

向Redis写入数据,并设置逻辑过期时间。

public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {
    // 设置逻辑过期
    RedisData redisData = new RedisData();
    redisData.setData(value);
    redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
    // 写入Redis
    stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
}
public <R, ID> R queryWithLogicalExpire(
        String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
    String key = keyPrefix + id;
    // 1.从redis查询商铺缓存
    String json = stringRedisTemplate.opsForValue().get(key);
    // 2.判断是否存在
    if (StrUtil.isBlank(json)) {
        // 3.存在,直接返回
        return null;
    }
    // 4.命中,需要先把json反序列化为对象
    RedisData redisData = JSONUtil.toBean(json, RedisData.class);
    R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
    LocalDateTime expireTime = redisData.getExpireTime();
    // 5.判断是否过期
    if(expireTime.isAfter(LocalDateTime.now())) {
        // 5.1.未过期,直接返回店铺信息
        return r;
    }
    // 5.2.已过期,需要缓存重建
    // 6.缓存重建
    // 6.1.获取互斥锁
    String lockKey = LOCK_SHOP_KEY + id;
    boolean isLock = tryLock(lockKey);
    // 6.2.判断是否获取锁成功
    if (isLock){
        // 6.3.成功,开启独立线程,实现缓存重建
        CACHE_REBUILD_EXECUTOR.submit(() -> {
            try {
                // 查询数据库
                R newR = dbFallback.apply(id);
                // 重建缓存
                this.setWithLogicalExpire(key, newR, time, unit);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }finally {
                // 释放锁
                unlock(lockKey);
            }
        });
    }
    // 6.4.返回过期的商铺信息
    return r;
}

缓存工具封装

基于StringRedisTemplate封装一个缓存工具类,满足下列需求:

方法1:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间 方法2:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题 方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题 方法4:根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题

private final StringRedisTemplate stringRedisTemplate;

// 线程池
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

/**
 * @description 基于构造函数进行注入
 * @author Lemonade
 * @param: stringRedisTemplate
 * @updateTime 2022/3/14 下午10:01
 */
public CacheClient(StringRedisTemplate stringRedisTemplate) {
    this.stringRedisTemplate = stringRedisTemplate;
}

// ----------------------缓存工具封装练习----------------------------
public void set(String key, Object value, Long time, TimeUnit unit) {
    // JSONUtil.toJsonStr(value)序列化为Json字符串
    stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
}

public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {
    // 设置逻辑过期
    RedisData redisData = new RedisData();
    redisData.setData(value);
    redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
    // 写入Redis
    stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
}

// 缓存穿透工具类
public <R,ID> R queryWithPassThrough(
        String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit){
    String key = keyPrefix + id;
    // 1.从redis查询商铺缓存
    String json = stringRedisTemplate.opsForValue().get(key);
    // 2.判断是否存在
    if (StrUtil.isNotBlank(json)) {
        // 3.存在,直接返回
        return JSONUtil.toBean(json, type);
    }
    // 判断命中的是否是空值
    if (json != null) {
        // 返回一个错误信息
        return null;
    }

    // 4.不存在,根据id查询数据库
    R r = dbFallback.apply(id);
    // 5.不存在,返回错误
    if (r == null) {
        // 将空值写入redis
        stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
        // 返回错误信息
        return null;
    }
    // 6.存在,写入redis
    this.set(key, r, time, unit);
    return r;
}

// 缓存击穿工具类
public <R, ID> R queryWithLogicalExpire(
        String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
    String key = keyPrefix + id;
    // 1.从redis查询商铺缓存
    String json = stringRedisTemplate.opsForValue().get(key);
    // 2.判断是否存在
    if (StrUtil.isBlank(json)) {
        // 3.存在,直接返回
        return null;
    }
    // 4.命中,需要先把json反序列化为对象
    RedisData redisData = JSONUtil.toBean(json, RedisData.class);
    R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
    LocalDateTime expireTime = redisData.getExpireTime();
    // 5.判断是否过期
    if(expireTime.isAfter(LocalDateTime.now())) {
        // 5.1.未过期,直接返回店铺信息
        return r;
    }
    // 5.2.已过期,需要缓存重建
    // 6.缓存重建
    // 6.1.获取互斥锁
    String lockKey = LOCK_SHOP_KEY + id;
    boolean isLock = tryLock(lockKey);
    // 6.2.判断是否获取锁成功
    if (isLock){
        // 6.3.成功,开启独立线程,实现缓存重建
        CACHE_REBUILD_EXECUTOR.submit(() -> {
            try {
                // 查询数据库
                R newR = dbFallback.apply(id);
                // 重建缓存
                this.setWithLogicalExpire(key, newR, time, unit);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }finally {
                // 释放锁
                unlock(lockKey);
            }
        });
    }
    // 6.4.返回过期的商铺信息
    return r;
}
// ----------------------缓存工具封装练习----------------------------

4.3 优惠券秒杀(含金量高)(计数器、lua脚本、分布式锁、3种消息队列)

全局唯一ID

全局ID生成器,是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足下列特性:

为了增加ID的安全性,我们可以不直接使用Redis自增的数值,而是拼接一些其它信息:

@Component
public class RedisIdWorker {
    /**
     * 开始时间戳
     */
    private static final long BEGIN_TIMESTAMP = 1640995200L;
    /**
     * 序列号的位数
     */
    private static final int COUNT_BITS = 32;

    private StringRedisTemplate stringRedisTemplate;

    public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    public long nextId(String keyPrefix) {
        // 1.生成时间戳
        LocalDateTime now = LocalDateTime.now();
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
        long timestamp = nowSecond - BEGIN_TIMESTAMP;

        // 2.生成序列号
        // 2.1.获取当前日期,精确到天
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        // 2.2.自增长
        long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);

        // 3.拼接并返回
        return timestamp << COUNT_BITS | count;
    }
}

全局唯一ID生成策略:

  • UUID
  • Redis自增
  • snowflake算法
  • 数据库自增

Redis自增ID策略:

  • 每天一个key,方便统计订单量
  • ID构造是 时间戳 + 计数器

实现优惠券秒杀下单

每个店铺都可以发布优惠券,分为平价券和特价券。平价券可以任意购买,而特价券需要秒杀抢购:

表关系: tb_voucher:优惠券的基本信息,优惠金额、使用规则等 tb_seckill_voucher:优惠券的库存、开始抢购时间,结束抢购时间。特价优惠券才需要填写这些信息

在VoucherController中提供了一个接口,可以添加秒杀优惠券:

@RestController
@RequestMapping("/voucher")
public class VoucherController {

    @Resource
    private IVoucherService voucherService;

    /**
     * 新增秒杀券
     * @param voucher 优惠券信息,包含秒杀信息
     * @return 优惠券id
     */
    @PostMapping("seckill")
    public Result addSeckillVoucher(@RequestBody Voucher voucher) {
        voucherService.addSeckillVoucher(voucher);
        return Result.ok(voucher.getId());
    }

下单时需要判断两点:

  • 秒杀是否开始或结束,如果尚未开始或已经结束则无法下单
  • 库存是否充足,不足则无法下单

public Result seckillVoucher(Long voucherId) {
    // 1.查询优惠券
    SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
    // 2.判断秒杀是否开始
    if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
        // 尚未开始
        return Result.fail("秒杀尚未开始!");
    }
    // 3.判断秒杀是否已经结束
    if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
        // 尚未开始
        return Result.fail("秒杀已经结束!");
    }
    // 4.判断库存是否充足(反向判断)
    if (voucher.getStock() < 1) {
        // 库存不足
        return Result.fail("库存不足!");
    }

    return createVoucherOrder(voucherId);
}

此处未完成,待补充!P51

超卖问题

一人一单

分布式锁

集群模式下,Synchronized 锁失效了 。Synchronized只能保证单个JVM内部的多个线程之间的互斥,而没有办法让集群环境下的多个JVM进程之间互斥。

要解决这个问题,必须使用分布式锁。

Synchronized其实就是利用JVM内部的锁监视器来控制线程,JVM内部只有一个锁监视器,所以只有一个线程获取锁,实现线程的互斥。
但是有多个JVM的时候,就有多个锁监视器,就有多个线程获取到锁。
我们必须让多个JVM使用同一个锁监视器,外部的。

分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。

分布式锁的核心是实现多进程之间互斥,而满足这一点的方式有很多,常见的有三种:

MySQLRedisZookeeper
互斥利用mysql本身的互斥锁机制利用setnx这样的互斥命令利用节点的唯一性和有序性实现互斥
高可用
高性能一般一般
安全性断开连接,自动释放锁利用锁超时时间,到期释放临时节点,断开连接自动释放
基于Redis的分布式锁
# 实现分布式锁时需要实现的两个基本方法:
## 获取锁
互斥:确保只能有一个线程获取锁
添加锁,利用setnx的互斥特性
setnx lock thread1

## 释放锁
手动释放,删除即可
del key

超时释放:获取锁时添加一个超时时间(避免服务宕机引起的死锁)
expire lock 10

保证原子性操作 
set lock thread1 ex 10 nx

非阻塞:尝试一次,成功返回true,失败返回false

基于Redis实现分布式锁初级版本

需求:定义一个类,实现下面接口,利用Redis实现分布式锁功能。

P61
释放锁时产生阻塞(Jvm垃圾回收full gc),如果阻塞时间足够长,就会触发超时释放锁,
这时其它线程就可以获取锁,开始执行业务,
这时如果线程1阻塞完成,恢复执行,开始释放锁,就把线程2 的锁给释放掉了(因为阻塞前已经判断过了,所以可以释放)
这就发生了误删

判断锁标识和释放是2个动作,之间产生阻塞,
所以,必须保证2个动作的原子性
# redis事务
可以保证原子性,但是无法保证一致性

推荐使用Redis的Lua脚本
Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言,它的基本语法大家可以参考网站:https://www.runoob.com/lua/lua-tutorial.html
# 释放锁的业务流程是这样的:
获取锁中的线程标示
判断是否与指定的标示(当前线程标示)一致
如果一致则释放锁(删除)
如果不一致则什么都不做
如果用Lua脚本来表示则是这样的:

需求:基于Lua脚本实现分布式锁的释放锁逻辑

提示:RedisTemplate调用Lua脚本的API如下:

Redisson

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。

它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。

官网地址: redisson.orgGitHub

地址: github.com/redisson/re…

Redis优化秒杀

Redis消息队列实现异步秒杀

4.4 达人探店(基于list的点赞列表、基于sortedset的点赞排行榜)

4.5 好友关注(基于set集合的关注、取关、共同关注、消息推送)

4.6 附近的商户(GeoHash)

4.7 用户签到(BitMap数据统计功能)

4.8 UV统计(HyperLogLog统计功能)


高级篇

原理篇

\