高并发点赞与mysql和redis实现最终一致性

55 篇文章 0 订阅
29 篇文章 0 订阅

服务器对应安装插件和后端jar 

192.168.1.7redis,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

访问

http://192.168.1.7:15672/

创建表结构 

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做到了最终一致性

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值