服务器对应安装插件和后端jar
192.168.1.7 | redis,rabbitmq,nginx |
192.168.1.8 | 后端jar包 |
192.168.1.9 | 后端jar包 |
docker安装redis
docker run -d -p 6379:6379 --name redis-node-1 -v /data/redis/share/redis-node-1:/data --privileged=true redis
docker安装rabbitmq
docker run -id --name myrabbit -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=123456 -p 5672:5672 -p 15672:15672 rabbitmq:3-management
访问
创建表结构
CREATE TABLE `book` (
`id` bigint NOT NULL AUTO_INCREMENT,
`wz_name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '文章名称',
`num` bigint DEFAULT '0' COMMENT '点赞次数',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=89 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='文章表';
CREATE TABLE `user_book` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_name` varchar(20) DEFAULT NULL COMMENT '用户名称',
`book_id` bigint DEFAULT NULL COMMENT '文章id',
`is_dz` int DEFAULT NULL COMMENT '是否点赞,0:未点赞/取消点赞,1:已点赞',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=34 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户点赞表';
插入一条测试数据
INSERT INTO `dmg`.`book` (`id`, `wz_name`, `num`) VALUES (88, '测试文章', 0);
docker安装nginx
安装nginx
docker run --name nginx -p 80:80 -d nginx:1.22.0
在服务器的home目录下创建bd-nginx文件夹
把容器中的配置拷贝到本地文件夹下
docker cp nginx:/etc/nginx/nginx.conf /home/bd-nginx/
docker cp nginx:/etc/nginx/conf.d/ /home/bd-nginx/conf/
docker cp nginx:/usr/share/nginx/html/ /home/bd-nginx/html/
docker cp nginx:/var/log/nginx/ /home/bd-nginx/logs/
停止并删除nginx
docker stop nginx
docker rm nginx
把本地文件挂载,启动nginx
docker run -p 80:80 \
-v /home/bd-nginx/nginx.conf:/etc/nginx/nginx.conf \
-v /home/bd-nginx/logs:/var/log/nginx \
-v /home/bd-nginx/html:/usr/share/nginx/html \
-v /home/bd-nginx/conf:/etc/nginx/conf.d \
-v /etc/localtime:/etc/localtime \
--name nginx \
--restart=always \
-d nginx:1.22.0
参数说明
-p 端口映射 服务器端口:docker容器端口
-v 挂载文件
--privileged=true 让docker容器中的root用户拥有权限
-- name 容器的名字
--restart=always docker自动重启
-d 指定要启动的镜像名称
先看下场景
使用apipost进行压测的时候
后台也报错了,数据库也插入了10条数据,这不是我们想要的,我们只想要1条
我们把并发数调成1000,然后在访问一个接口参数不同,这时候会发现服务器挂了
就是因为我们频繁的访问数据库导致的
那么这个时候,我们可以接入缓存
但是如果并发情况下,点赞次数还会出错
这个时候我们引入Redisson分布式锁
因为引入了分布式锁,那么我们的点赞业务就不能写的太复杂
这时候,我们就需要把操作数据库的数据放入mq,通过mq去接收数据来操作数据库
这样我们的redis就能和mysql做到了最终一致性
最终一致性就是过一段时间mysql和redis的数据一致
我们在获取文章的数据的时候另一个接口,如果进行了大量的请求,那么这个时候就
服务就挂掉了,我们可以采用nginx配置负载均衡 配置2台服务
修改nginx.conf,配置负载均衡
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
#定义负载均衡
upstream zhuanfa {
#转发到ip:端口号
server localhost:8080;
server localhost:8081;
}
server {
listen 80;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
root html;
index index.html index.htm;
#代理 转发
proxy_pass http://zhuanfa;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}
# another virtual host using mix of IP-, name-, and port-based configuration
#
#server {
# listen 8000;
# listen somename:8080;
# server_name somename alias another.alias;
# location / {
# root html;
# index index.html index.htm;
# }
#}
# HTTPS server
#
#server {
# listen 443 ssl;
# server_name localhost;
# ssl_certificate cert.pem;
# ssl_certificate_key cert.key;
# ssl_session_cache shared:SSL:1m;
# ssl_session_timeout 5m;
# ssl_ciphers HIGH:!aNULL:!MD5;
# ssl_prefer_server_ciphers on;
# location / {
# root html;
# index index.html index.htm;
# }
#}
}
主要改动这里,然后重启nginx
这个时候访问就是通过nginx访问
localhost:80/getInfo 获取文章数据
虽然做了负载均衡,但是还是会频繁的访问数据库,会造成数据库的压力过大
这时候我们引入缓存,先查询缓存是否存在,如果缓存不存在 才查询数据库
虽然引入了缓存,但有时候他故意查询了一些缓存不存在的数据,大量的请求打进来
还是会频繁的访问数据库,那么这个时候 我们引入了限流
创建后端springboot项目
引入pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!-- 排除spring boot默认日志logback 不然会报错 -->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--导入log4j2日志依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--RabbitMQ 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!-- mybatis-springboot-starter-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- SpringBoot集成Redis的依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- redisson 分布式锁 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.17.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!--在这里修改版本-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>2.4.3</version>
</plugin>
<!---->
</plugins>
</build>
</project>
配置application.properties
#端口号
server.port=8081
#数据库配置
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.url=jdbc:mysql://localhost:3306/dmg?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#整合mybatis
#接口的配置文件的位置
mybatis.mapper-locations=classpath:mappers/*.xml
#配置控制台日志
logging.config=classpath:log4j2.xml
#设置redis的配置信息
spring.redis.host=192.168.1.7
spring.redis.port=6379
#配置rabbitmq
spring.rabbitmq.host=192.168.1.7
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=123456
创建log4j2.xml
<?xml version="1.0" encoding="UTF-8"?>
<!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL -->
<!--Configuration后面的status,这个用于设置log4j2自身内部的信息输出,可以不设置,当设置成trace时,你会看到log4j2内部各种详细输出-->
<!--monitorInterval:Log4j能够自动检测修改配置 文件和重新配置本身,设置间隔秒数-->
<configuration status="WARN" monitorInterval="30">
<!--先定义所有的appender-->
<appenders>
<!--这个输出控制台的配置-->
<console name="Console" target="SYSTEM_OUT">
<!--输出日志的格式-->
<PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/>
</console>
<!--文件会打印出所有信息,这个log每次运行程序会自动清空,由append属性决定,这个也挺有用的,适合临时测试用-->
<File name="log" fileName="log/test.log" append="false">
<PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %class{36} %L %M - %msg%xEx%n"/>
</File>
<!-- 这个会打印出所有的info及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
<RollingFile name="RollingFileInfo" fileName="${sys:user.home}/logs/info.log"
filePattern="${sys:user.home}/logs/$${date:yyyy-MM}/info-%d{yyyy-MM-dd}-%i.log">
<!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
<ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/>
<Policies>
<TimeBasedTriggeringPolicy/>
<SizeBasedTriggeringPolicy size="100 MB"/>
</Policies>
</RollingFile>
<RollingFile name="RollingFileWarn" fileName="${sys:user.home}/logs/warn.log"
filePattern="${sys:user.home}/logs/$${date:yyyy-MM}/warn-%d{yyyy-MM-dd}-%i.log">
<ThresholdFilter level="warn" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/>
<Policies>
<TimeBasedTriggeringPolicy/>
<SizeBasedTriggeringPolicy size="100 MB"/>
</Policies>
<!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件,这里设置了20 -->
<DefaultRolloverStrategy max="20"/>
</RollingFile>
<RollingFile name="RollingFileError" fileName="${sys:user.home}/logs/error.log"
filePattern="${sys:user.home}/logs/$${date:yyyy-MM}/error-%d{yyyy-MM-dd}-%i.log">
<ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/>
<Policies>
<TimeBasedTriggeringPolicy/>
<SizeBasedTriggeringPolicy size="100 MB"/>
</Policies>
</RollingFile>
</appenders>
<!--然后定义logger,只有定义了logger并引入的appender,appender才会生效-->
<loggers>
<!--过滤掉spring和mybatis的一些无用的DEBUG信息-->
<logger name="org.springframework" level="INFO"></logger>
<logger name="org.mybatis" level="INFO"></logger>
<root level="all">
<appender-ref ref="Console"/>
<appender-ref ref="RollingFileInfo"/>
<appender-ref ref="RollingFileWarn"/>
<appender-ref ref="RollingFileError"/>
</root>
</loggers>
</configuration>
创建mappers\BookMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.dao.BookDao">
<!--修改点赞次数-->
<update id="updateNum">
UPDATE book set num=#{num}
WHERE id=88
</update>
<select id="getInfo" resultType="com.example.demo.entity.Book">
select `id`, `wz_name` as wzName, `num` from `book`
WHERE id=88
</select>
</mapper>
创建mappers\UserBookMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.dao.UserBookDao">
<select id="getInfo" resultType="Integer">
SELECT is_dz FROM
user_book
WHERE user_name=#{userName}
and book_id=88
</select>
<update id="updateIsDz">
update
user_book set is_dz=#{isDz}
WHERE user_name=#{userName}
and book_id=88
</update>
<insert id="add">
INSERT INTO `user_book` ( `user_name`, `book_id`, `is_dz` )
VALUES
( #{userName}, 88, 1 );
</insert>
</mapper>
创建mq配置
package com.example.demo.config;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* 队列配置类
*
* @param
* @return
* @throws Exception
*/
@Configuration
public class MqQueueConfig {
//普通交换机
public static final String DIANZAN_EXCHANGE="dianZan_exchange";
//普通队列
public static final String QUEUE_NAME="dianZan";
//普通路由key
public static final String DIANZAN_ROUTER="dianZan_router";
//声明普通交换机
@Bean
public DirectExchange dianZanExchange(){
return new DirectExchange(DIANZAN_EXCHANGE);
}
//声明普通队列
@Bean
public Queue queueDianZan(){
Map<String,Object> map=new HashMap<>();
//构建队列
return QueueBuilder.durable(QUEUE_NAME).withArguments(map).build();
}
//绑定普通队列和普通交换机 普通路由key
@Bean
public Binding queryBindA(@Qualifier("queueDianZan") Queue queueDianZan,
@Qualifier("dianZanExchange") DirectExchange dianZanExchange){
return BindingBuilder.bind(queueDianZan).to(dianZanExchange).with(DIANZAN_ROUTER);
}
}
Redisson配置
package com.example.demo.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient(){
// 配置对象
Config config = new Config();
//单节点 地址 redis://ip:6379
config.useSingleServer().setAddress("redis://192.168.1.7:6379");
//创建配置
return Redisson.create(config);
}
}
redis工具类
package com.example.demo.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
@Slf4j
@Component
public class RedisUtil {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
*
* 添加缓存
* @param
* @return
* @throws Exception
*/
public void set(String key,String value){
try {
//设置超时时间为1天
stringRedisTemplate.opsForValue().set(key,value,1, TimeUnit.DAYS);
}catch (Exception e){
log.error("缓存添加出错了:{}",e.getMessage());
e.printStackTrace();
}
}
/**
*
* 获取缓存
* @param
* @return
* @throws Exception
*/
public String get(String key){
String s =null;
try {
//设置超时时间为1天
s = stringRedisTemplate.opsForValue().get(key);
}catch (Exception e){
log.error("缓存获取出错了:{}",e.getMessage());
e.printStackTrace();
}
return s;
}
/**
*
* 次数累加
* @param
* @return
* @throws Exception
*/
public Long increment(String key){
Long res =null;
try {
res = stringRedisTemplate.opsForValue().increment(key);
}catch (Exception e){
log.error("缓存次数累加出错了:{}",e.getMessage());
e.printStackTrace();
}
return res;
}
/**
*
* 次数递减
* @param
* @return
* @throws Exception
*/
public Long decrement(String key){
Long res =null;
try {
res = stringRedisTemplate.opsForValue().decrement(key);
}catch (Exception e){
log.error("缓存次数递减出错了:{}",e.getMessage());
e.printStackTrace();
}
return res;
}
/**
*
* 加锁
* 如果返回true 表示添加成功
* 如果返回false 表示已经存在了
* @param
* @return
* @throws Exception
*/
public Boolean setIfAbsent(String key,String value){
Boolean res =null;
try {
//设置1天的过期时间
res = stringRedisTemplate.opsForValue().setIfAbsent(key,value,1,TimeUnit.DAYS);
}catch (Exception e){
log.error("缓存加锁出错了:{}",e.getMessage());
e.printStackTrace();
}
return res;
}
/**
*
* 删除缓存中的数据
* @param
* @return
* @throws Exception
*/
public Boolean delete(String key){
Boolean res =null;
try {
//设置1天的过期时间
res = stringRedisTemplate.delete(key);
}catch (Exception e){
log.error("缓存删除出错了:{}",e.getMessage());
e.printStackTrace();
}
return res;
}
}
控制层
package com.example.demo.controller;
import com.example.demo.entity.User;
import com.example.demo.service.BookService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
public class BookController {
@Autowired
private BookService bookService;
/**
* 点赞/取消点赞
* @param
* @return
* @throws Exception
*/
@PostMapping("/dianZan")
@ResponseBody
public String dianZan(@RequestBody User user){
log.info("进来了:{}",user.getUserName());
bookService.dianZan(user.getUserName());
return "操作成功";
}
/**
* 获取文章数据
* @param
* @return
* @throws Exception
*/
@PostMapping("/getInfo")
@ResponseBody
public String getInfo(){
String res=bookService.getInfo();
return res;
}
}
dao层
package com.example.demo.dao;
import com.example.demo.entity.Book;
import org.apache.ibatis.annotations.Param;
public interface BookDao {
/**
*
* 修改点赞次数
* @param
* @return
* @throws Exception
*/
int updateNum(@Param("num")Long num);
/**
*
* 获取文章数据
* @param
* @return
* @throws Exception
*/
Book getInfo();
}
package com.example.demo.dao;
import com.example.demo.entity.UserBook;
import org.apache.ibatis.annotations.Param;
public interface UserBookDao {
/**
*
* 获取用户是否点赞
* @param
* @return
* @throws Exception
*/
public Integer getInfo(@Param("userName")String userName);
/**
*
* 修改是否点赞
* @param
* @return
* @throws Exception
*/
public Integer updateIsDz(@Param("isDz")Integer isDz,
@Param("userName")String userName);
/**
*
* 添加点赞纪录
* @param
* @return
* @throws Exception
*/
public Integer add(@Param("userName")String userName);
}
实体类
package com.example.demo.entity;
import lombok.Data;
@Data
public class Book {
//主键
private Long id;
//文章名称
private String wzName;
//点赞次数
private Long num;
}
package com.example.demo.entity;
import lombok.Data;
@Data
public class User {
private String userName;
}
package com.example.demo.entity;
import lombok.Data;
@Data
public class UserBook {
//主键
private Long id;
//用户名称
private String userName;
//文章id
private Long bookId;
//是否点赞,0:未点赞/取消点赞,1:已点赞
private Integer isDz;
}
mq监听
package com.example.demo.listener;
import com.alibaba.fastjson.JSONObject;
import com.example.demo.config.MqQueueConfig;
import com.example.demo.config.RedisUtil;
import com.example.demo.dao.BookDao;
import com.example.demo.dao.UserBookDao;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
*
* 消费mq的消息
* @param
* @return
* @throws Exception
*/
@Slf4j
@Component
public class Consumer {
//全局缓存 key, 88是文章的id
private static String DIAN_ZAN="dianzan-88-";
@Autowired
private BookDao bookDao;
@Autowired
private UserBookDao userBookDao;
@Autowired
private RedisUtil redisUtil;
/**
*
* 监听队列
* @param
* @return
* @throws Exception
*/
@RabbitListener(queues = MqQueueConfig.QUEUE_NAME)
public void receive(Message message, Channel channel){
String msg=new String(message.getBody());
//转成json
JSONObject jsonObject=JSONObject.parseObject(msg);
String type = jsonObject.getString("type");
String userName=jsonObject.getString("userName");
//获取数据库是否存在
Integer dz = userBookDao.getInfo(userName);
if(dz==null){
//不存在 添加到数据库中
userBookDao.add(userName);
}else {
//存在 修改是否点赞
userBookDao.updateIsDz(Integer.parseInt(type),userName);
}
//获取点赞次数
//点赞次数key
String numKey=DIAN_ZAN+"num";
String num=redisUtil.get(numKey);
//修改文章点赞次数
bookDao.updateNum(Long.parseLong(num));
log.info("接收到消息,当前时间:{}"+new Date()+",内容:{}"+msg);
}
}
接口层
package com.example.demo.service;
import org.apache.ibatis.annotations.Param;
public interface BookService {
/**
*
* 点赞/取消点赞
* @param
* @return
* @throws Exception
*/
void dianZan(String userName);
/**
*
* 获取点赞数据
* @param
* @return
* @throws Exception
*/
String getInfo();
}
package com.example.demo.service.impl;
import com.alibaba.fastjson.JSONObject;
import com.example.demo.config.MqQueueConfig;
import com.example.demo.config.RedisUtil;
import com.example.demo.dao.BookDao;
import com.example.demo.dao.UserBookDao;
import com.example.demo.entity.Book;
import com.example.demo.entity.UserBook;
import com.example.demo.service.BookService;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.*;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import java.util.Objects;
@Slf4j
@Service
public class BookServiceImpl implements BookService {
//全局缓存 key, 88是文章的id
private static String DIAN_ZAN="dianzan-88-";
@Autowired
private BookDao bookDao;
@Autowired
private UserBookDao userBookDao;
@Autowired
private RedisUtil redisUtil;
@Autowired
private RedissonClient redissonClient;
@Autowired
private RabbitTemplate rabbitTemplate;
@Override
public void dianZan(String userName) {
//注意 这里一定要结合文章id去加锁,如果不加,别的文章进来的 你也给人家锁住了
String lockName=DIAN_ZAN;
RLock rLock=redissonClient.getLock(lockName);
//加锁 默认有自动续期的功能,默认时间30秒,当前时间达到3分之1的时候,自动续期
//底层就是继承了juc的Lock类,也实现了可重入锁
rLock.lock();
try {
JSONObject jsonObject=new JSONObject();
//用户key
String userKey=DIAN_ZAN+userName;
//点赞次数key
String numKey=DIAN_ZAN+"num";
//查询用户是否点赞
String user= redisUtil.get(userKey);
if(StringUtils.hasText(user)){
//如果缓存中的数据存在
//点赞次数递减
redisUtil.decrement(numKey);
//删除缓存的用户点赞数据
redisUtil.delete(userKey);
//通知mq 取消点赞纪录 和修改点赞次数
jsonObject.put("type","0");
}else {
//如果不存在 添加缓存数据
redisUtil.set(userKey,userName);
//点赞次数累加
redisUtil.increment(numKey);
//通知mq 添加到数据库点赞纪录 和修改点赞次数
jsonObject.put("type","1");
}
//用户
jsonObject.put("userName",userName);
//发送mq
sendMq(jsonObject.toJSONString());
}finally {
//释放锁 防止死锁
rLock.unlock();
}
}
/**
*
* 发送mq消息
* @param message 发送内容
* @return
* @throws Exception
*/
private void sendMq(String message){
//普通交换机
String exchange= MqQueueConfig.DIANZAN_EXCHANGE;
//普通路由key
String routingKey=MqQueueConfig.DIANZAN_ROUTER;
try {
rabbitTemplate.convertAndSend(exchange,routingKey,message);
log.info("发送消息成功");
}catch (Exception e){
log.error("发送消息失败:{}",e.getMessage());
e.printStackTrace();
}
}
/**
*
* 获取点赞数据
* @param
* @return
* @throws Exception
*/
@Override
public String getInfo() {
//通过令牌桶进行限流,如果桶内有令牌 那么可以往下走,如果桶内没有令牌 限流 不允许访问
//dianZanLimiter 随便定义令牌桶的名称
RRateLimiter dianZanLimiter = redissonClient.getRateLimiter("dianZanLimiter");
//设置速率 每1秒产生300个令牌 在这里要注意:
// TODO 如果修改了令牌的参数,一定要删除redis中dianZanLimiter的数据,否则不生效
dianZanLimiter.trySetRate(RateType.OVERALL, 300, 1, RateIntervalUnit.SECONDS);
if(dianZanLimiter.tryAcquire(1)){
//如果能够获取一个令牌 那么处理业务逻辑
//先从缓存中获取文章是否存在
String key=DIAN_ZAN+"info";
String value=redisUtil.get(key);
if(StringUtils.hasText(value)){
//如果缓存存在 直接返回
return value;
}
//如果不存在 查询数据库
Book book=bookDao.getInfo();
if(book==null){
return null;
}
value=book.toString();
//放入缓存
redisUtil.set(key,value);
return value;
}else {
//如果获取不到令牌 限流
log.info("当前访问人数过多,请稍后刷新界面");
return "当前访问人数过多,请稍后刷新界面";
}
}
}
启动类
package com.example.demo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@MapperScan("com.example.demo.dao")
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
压测
在apipost写好接口之后,点击一键压测
然后开启压测服务,点击开始压测
数据库点赞次数为4,因为是4个人点赞
纪录了4条点赞数据,没有出现重复
当令牌不足的时候,就会触发我们的限流
在缓存中存储点赞的数量和mysql一致,通过mq做到了最终一致性