├── .settings ├── org.eclipse.jdt.apt.core.prefs ├── org.springframework.ide.eclipse.prefs ├── org.eclipse.m2e.core.prefs ├── org.eclipse.core.resources.prefs ├── org.eclipse.wst.common.project.facet.core.xml ├── org.springframework.ide.eclipse.boot.properties.editor.prefs └── org.eclipse.jdt.core.prefs ├── src └── main │ ├── resources │ ├── META-INF │ │ └── spring.factories │ ├── application.yml │ ├── mybatis-config.xml │ └── logback-spring.xml │ ├── java │ └── cn │ │ └── com │ │ └── bluemoon │ │ ├── mybatis │ │ └── datasource │ │ │ ├── DynamicDataSourceGlobal.java │ │ │ ├── DynamicDataSourceHolder.java │ │ │ ├── DynamicDataSourceTransactionManager.java │ │ │ ├── DynamicDataSource.java │ │ │ └── DynamicPlugin.java │ │ ├── common │ │ ├── exception │ │ │ ├── ServiceExceptionEnum.java │ │ │ ├── IllegalReentrantException.java │ │ │ ├── WebException.java │ │ │ ├── AssertException.java │ │ │ ├── WebExceptionEnum.java │ │ │ └── GlobalExceptionHandler.java │ │ ├── response │ │ │ ├── StockNumResponse.java │ │ │ ├── BaseResponse.java │ │ │ ├── ResponseBean.java │ │ │ └── SeckillInfoResponse.java │ │ ├── config │ │ │ ├── WebConfig.java │ │ │ ├── SwaggerConfig.java │ │ │ └── DatasourceConfig.java │ │ ├── interceptor │ │ │ └── LimitInterceptor.java │ │ └── logs │ │ │ └── LogAspect.java │ │ ├── threads │ │ ├── UserRejectHandler.java │ │ ├── UserThreadFactory.java │ │ ├── CallableAndFuture.java │ │ ├── UserThreadPool.java │ │ ├── CallableAndFuture2.java │ │ └── Totp.java │ │ ├── kafka │ │ ├── KafkaSender.java │ │ └── KafkaConsumer.java │ │ ├── ServiceSeckillApplication.java │ │ ├── service │ │ ├── ISeckillService.java │ │ └── impl │ │ │ └── SeckillServiceImpl.java │ │ ├── redis │ │ ├── repository │ │ │ ├── RedisCacheConfig.java │ │ │ ├── RedissonProperties.java │ │ │ ├── RedissonAutoConfiguration.java │ │ │ └── RedisRepository.java │ │ └── lock │ │ │ ├── DistributedExclusiveRedisLock.java │ │ │ └── RedissonDistributedLocker.java │ │ ├── utils │ │ ├── AssertUtil.java │ │ ├── SerialNo.java │ │ └── DateUtil.java │ │ └── controller │ │ └── SeckillController.java │ └── webapp │ └── views │ └── index.html ├── .gitignore ├── README.md ├── pom.xml └── .factorypath /.settings/org.eclipse.jdt.apt.core.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | org.eclipse.jdt.apt.aptEnabled=true 3 | -------------------------------------------------------------------------------- /.settings/org.springframework.ide.eclipse.prefs: -------------------------------------------------------------------------------- 1 | boot.validation.initialized=true 2 | eclipse.preferences.version=1 3 | -------------------------------------------------------------------------------- /.settings/org.eclipse.m2e.core.prefs: -------------------------------------------------------------------------------- 1 | activeProfiles= 2 | eclipse.preferences.version=1 3 | resolveWorkspaceProjects=true 4 | version=1 5 | -------------------------------------------------------------------------------- /.settings/org.eclipse.core.resources.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | encoding//src/main/java=UTF-8 3 | encoding//src/main/resources=UTF-8 4 | encoding//src/test/java=UTF-8 5 | encoding/=UTF-8 6 | -------------------------------------------------------------------------------- /.settings/org.eclipse.wst.common.project.facet.core.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.settings/org.springframework.ide.eclipse.boot.properties.editor.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | spring.properties.editor.problem.YAML.project.prefs.enabled=true 3 | spring.properties.editor.problem.YAML_UNKNOWN_PROPERTY=IGNORE 4 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ 2 | cn.com.bluemoon1.redis.repository.RedisCacheConfig,cn.com.bluemoon1.redis.repository.RedissonProperties,cn.com.bluemoon1.redis.repository.RedissonAutoConfiguration -------------------------------------------------------------------------------- /src/main/java/cn/com/bluemoon/mybatis/datasource/DynamicDataSourceGlobal.java: -------------------------------------------------------------------------------- 1 | package cn.com.bluemoon.mybatis.datasource; 2 | 3 | public enum DynamicDataSourceGlobal { 4 | /** 5 | * 读数据源 6 | */ 7 | READ, 8 | /** 9 | * 写数据源 10 | */ 11 | WRITE 12 | } -------------------------------------------------------------------------------- /.settings/org.eclipse.jdt.core.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 3 | org.eclipse.jdt.core.compiler.compliance=1.8 4 | org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning 5 | org.eclipse.jdt.core.compiler.processAnnotations=enabled 6 | org.eclipse.jdt.core.compiler.source=1.8 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # maven ignore 2 | target/ 3 | 4 | # eclipse ignore 5 | .settings/ 6 | .project 7 | .classpath 8 | 9 | # idea ignore 10 | .idea/ 11 | *.ipr 12 | *.iml 13 | *.iws 14 | 15 | # temp ignore 16 | *.log 17 | *.cache 18 | *.diff 19 | *.patch 20 | *.tmp 21 | 22 | # system ignore 23 | .DS_Store 24 | Thumbs.db 25 | 26 | # project ignore 27 | **/tmp 28 | pom.xml.versionsBackup 29 | -------------------------------------------------------------------------------- /src/main/java/cn/com/bluemoon/common/exception/ServiceExceptionEnum.java: -------------------------------------------------------------------------------- 1 | package cn.com.bluemoon.common.exception; 2 | 3 | /** 4 | * 抽象接口 5 | * 6 | * @author fengshuonan 7 | * @date 2017-12-28-下午10:27 8 | */ 9 | public interface ServiceExceptionEnum { 10 | 11 | /** 12 | * 请求是否成功 13 | */ 14 | Boolean getIsSuccess(); 15 | 16 | /** 17 | * 获取返回的code 18 | */ 19 | Integer getResponseCode(); 20 | 21 | /** 22 | * 获取返回的message 23 | */ 24 | String getResponseMsg(); 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/cn/com/bluemoon/threads/UserRejectHandler.java: -------------------------------------------------------------------------------- 1 | package cn.com.bluemoon.threads; 2 | 3 | import java.util.concurrent.RejectedExecutionHandler; 4 | import java.util.concurrent.ThreadPoolExecutor; 5 | 6 | /** 7 | * 拒绝策略应该考虑到业务场景,返回响应的提示或者友好的跳转 8 | * 如下示例为简单实例,在实际应用中应该根据业务场景进行调整 9 | * @author Guoqing.Lee 10 | * @date 2019年5月24日 上午11:54:09 11 | * 12 | */ 13 | public class UserRejectHandler implements RejectedExecutionHandler { 14 | 15 | @Override 16 | public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { 17 | System.out.println("task rejected. " + executor.toString()); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/cn/com/bluemoon/common/response/StockNumResponse.java: -------------------------------------------------------------------------------- 1 | package cn.com.bluemoon.common.response; 2 | 3 | /** 4 | * 5 | * 活动库存 6 | * @author Guoqing 7 | * 8 | */ 9 | public class StockNumResponse extends BaseResponse { 10 | 11 | private Long stockNum; 12 | 13 | private Long realStockNum; 14 | 15 | public Long getStockNum() { 16 | return stockNum; 17 | } 18 | 19 | public void setStockNum(Long stockNum) { 20 | this.stockNum = stockNum; 21 | } 22 | 23 | public Long getRealStockNum() { 24 | return realStockNum; 25 | } 26 | 27 | public void setRealStockNum(Long realStockNum) { 28 | this.realStockNum = realStockNum; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/cn/com/bluemoon/kafka/KafkaSender.java: -------------------------------------------------------------------------------- 1 | package cn.com.bluemoon.kafka; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.kafka.core.KafkaTemplate; 5 | import org.springframework.stereotype.Component; 6 | /** 7 | * 生产者 8 | * @author Guoqing 9 | * @date 2018/11/16 15:09 10 | */ 11 | @Component 12 | public class KafkaSender { 13 | 14 | @Autowired 15 | private KafkaTemplate kafkaTemplate; 16 | 17 | /** 18 | * 发送消息到kafka 19 | */ 20 | public void sendChannelMess(String channel, String message){ 21 | kafkaTemplate.send(channel,message); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/cn/com/bluemoon/common/config/WebConfig.java: -------------------------------------------------------------------------------- 1 | package cn.com.bluemoon.common.config; 2 | 3 | import org.springframework.stereotype.Component; 4 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 5 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; 6 | 7 | import cn.com.bluemoon.common.interceptor.LimitInterceptor; 8 | 9 | @Component 10 | public class WebConfig extends WebMvcConfigurerAdapter { 11 | 12 | public void addInterceptors(InterceptorRegistry registry) { 13 | //多个拦截器组成一个拦截器链 14 | registry.addInterceptor(new LimitInterceptor(1000, LimitInterceptor.LimitType.DROP)).addPathPatterns("/**"); 15 | super.addInterceptors(registry); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/cn/com/bluemoon/common/exception/IllegalReentrantException.java: -------------------------------------------------------------------------------- 1 | package cn.com.bluemoon.common.exception; 2 | 3 | /** 4 | * 对于不可重入的锁将抛出此异常 5 | * 6 | * Created by Guoqing on 16/8/25. 7 | */ 8 | public class IllegalReentrantException extends RuntimeException { 9 | 10 | private static final long serialVersionUID = 1L; 11 | 12 | public IllegalReentrantException(Throwable cause) { 13 | super(cause); 14 | } 15 | 16 | public IllegalReentrantException(String message, Throwable cause) { 17 | super(message, cause); 18 | } 19 | 20 | public IllegalReentrantException(String message) { 21 | super(message); 22 | } 23 | 24 | public IllegalReentrantException() { 25 | super(); 26 | } 27 | } -------------------------------------------------------------------------------- /src/main/java/cn/com/bluemoon/threads/UserThreadFactory.java: -------------------------------------------------------------------------------- 1 | package cn.com.bluemoon.threads; 2 | 3 | import java.util.concurrent.ThreadFactory; 4 | import java.util.concurrent.atomic.AtomicInteger; 5 | 6 | /** 7 | * 线程工厂 8 | * @author Guoqing.Lee 9 | * @date 2019年5月24日 上午11:20:52 10 | * 11 | */ 12 | public class UserThreadFactory implements ThreadFactory{ 13 | 14 | private final String namePrefix; 15 | private final AtomicInteger nextId = new AtomicInteger(1); 16 | 17 | public UserThreadFactory(String whatFeatureOfGroup) { 18 | this.namePrefix = "UserThreadFactory's " + whatFeatureOfGroup + "-Worker-"; 19 | } 20 | 21 | @Override 22 | public Thread newThread(Runnable r) { 23 | String name = namePrefix + nextId.getAndIncrement(); 24 | Thread thread = new Thread(null, r, name, 0); 25 | System.out.println(thread.getName()); 26 | return thread; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/cn/com/bluemoon/ServiceSeckillApplication.java: -------------------------------------------------------------------------------- 1 | package cn.com.bluemoon; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.transaction.annotation.EnableTransactionManagement; 7 | import org.springframework.web.bind.annotation.RestController; 8 | 9 | /** 10 | *

Title: ServiceSeckillApplication

11 | *

Description: seckill service服务启动类

12 | * @author Guoqing 13 | * @date 2018年7月2日 14 | */ 15 | @Configuration 16 | @RestController 17 | @SpringBootApplication 18 | @EnableTransactionManagement //启用事务 19 | public class ServiceSeckillApplication { 20 | 21 | 22 | public static void main( String[] args ){ 23 | SpringApplication.run(ServiceSeckillApplication.class, args); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/cn/com/bluemoon/mybatis/datasource/DynamicDataSourceHolder.java: -------------------------------------------------------------------------------- 1 | package cn.com.bluemoon.mybatis.datasource; 2 | 3 | /** 4 | * 动态数据源持有者 5 | * 6 | * @author mij 7 | */ 8 | public final class DynamicDataSourceHolder { 9 | 10 | /** 11 | * 动态数据源存储 12 | */ 13 | private static final ThreadLocal DYNAMIC_DATA_SOURCE_GLOBAL_THREAD_LOCAL = new ThreadLocal<>(); 14 | 15 | private DynamicDataSourceHolder() { 16 | // 17 | } 18 | 19 | /** 20 | * 存放数据源 21 | * 22 | * @param dataSource 数据源 23 | */ 24 | public static void putDataSource(DynamicDataSourceGlobal dataSource) { 25 | DYNAMIC_DATA_SOURCE_GLOBAL_THREAD_LOCAL.set(dataSource); 26 | } 27 | 28 | /** 29 | * 获取数据源 30 | * 31 | * @return the data source 32 | */ 33 | public static DynamicDataSourceGlobal getDataSource() { 34 | return DYNAMIC_DATA_SOURCE_GLOBAL_THREAD_LOCAL.get(); 35 | } 36 | 37 | /** 38 | * 清除数据源 39 | */ 40 | public static void clearDataSource() { 41 | DYNAMIC_DATA_SOURCE_GLOBAL_THREAD_LOCAL.remove(); 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /src/main/java/cn/com/bluemoon/common/response/BaseResponse.java: -------------------------------------------------------------------------------- 1 | package cn.com.bluemoon.common.response; 2 | 3 | /** 4 | * 通用响应model 5 | * @author Guoqing 6 | * @version 1.0 7 | */ 8 | public class BaseResponse{ 9 | 10 | //请求是否成功 11 | private Boolean isSuccess = true; 12 | //请求响应码,成功时为0 13 | private int responseCode = 0; 14 | //请求响应码对应描述 15 | private String responseMsg = "请求成功"; 16 | 17 | public BaseResponse(){} 18 | 19 | public BaseResponse(Boolean isSuccess, int responseCode, 20 | String responseMsg) { 21 | this.isSuccess = isSuccess; 22 | this.responseCode = responseCode; 23 | this.responseMsg = responseMsg; 24 | } 25 | 26 | public Boolean getIsSuccess() { 27 | return isSuccess; 28 | } 29 | public void setIsSuccess(Boolean isSuccess) { 30 | this.isSuccess = isSuccess; 31 | } 32 | public int getResponseCode() { 33 | return responseCode; 34 | } 35 | public void setResponseCode(int responseCode) { 36 | this.responseCode = responseCode; 37 | } 38 | public String getResponseMsg() { 39 | return responseMsg; 40 | } 41 | public void setResponseMsg(String responseMsg) { 42 | this.responseMsg = responseMsg; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/cn/com/bluemoon/service/ISeckillService.java: -------------------------------------------------------------------------------- 1 | /** 2 | *

Title: ISeckillService.java

3 | *

Description:

4 | *

Copyright: Copyright (c) 2018

5 | *

Company: www.bluemoon.com

6 | * @author Guoqing 7 | * @date 2018年8月10日 8 | */ 9 | package cn.com.bluemoon.service; 10 | 11 | import cn.com.bluemoon.common.response.SeckillInfoResponse; 12 | 13 | /** 14 | *

Title: ISeckillService

15 | *

Description: 秒杀相关方法

16 | * @author Guoqing 17 | * @date 2018年8月10日 18 | */ 19 | public interface ISeckillService { 20 | 21 | /** 22 | * 秒杀处理主要逻辑 23 | *

Title: startSeckill

24 | *

Description:

25 | * @param stallActivityId 26 | * @param purchaseNum 27 | * @param openId 28 | * @param formId 29 | * @param addressId 30 | * @return 31 | */ 32 | public SeckillInfoResponse startSeckill(int stallActivityId, int purchaseNum, String openId, String formId, 33 | long addressId, String shareCode, String shareSource, String userCode ); 34 | 35 | /** 36 | * 判断秒杀活动是否已经开始 37 | * @param stallActivityId 38 | * @return 39 | */ 40 | public boolean checkStartSeckill(int stallActivityId); 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/cn/com/bluemoon/common/exception/WebException.java: -------------------------------------------------------------------------------- 1 | package cn.com.bluemoon.common.exception; 2 | 3 | /** 4 | * 封装异常 5 | * 6 | * @author Guoqing 7 | * @Date 2018/06/28 下午10:32 8 | */ 9 | public class WebException extends RuntimeException { 10 | 11 | private Boolean isSuccess; 12 | 13 | private Integer responseCode; 14 | 15 | private String responseMsg; 16 | 17 | public WebException(ServiceExceptionEnum serviceExceptionEnum) { 18 | this.isSuccess = serviceExceptionEnum.getIsSuccess(); 19 | this.responseCode = serviceExceptionEnum.getResponseCode(); 20 | this.responseMsg = serviceExceptionEnum.getResponseMsg(); 21 | } 22 | 23 | public Boolean getIsSuccess() { 24 | return isSuccess; 25 | } 26 | 27 | public void setIsSuccess(Boolean isSuccess) { 28 | this.isSuccess = isSuccess; 29 | } 30 | 31 | public Integer getResponseCode() { 32 | return responseCode; 33 | } 34 | 35 | public void setResponseCode(Integer responseCode) { 36 | this.responseCode = responseCode; 37 | } 38 | 39 | public String getResponseMsg() { 40 | return responseMsg; 41 | } 42 | 43 | public void setResponseMsg(String responseMsg) { 44 | this.responseMsg = responseMsg; 45 | } 46 | 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/cn/com/bluemoon/common/exception/AssertException.java: -------------------------------------------------------------------------------- 1 | package cn.com.bluemoon.common.exception; 2 | 3 | 4 | import java.util.logging.Level; 5 | 6 | /** 7 | * 断言异常类 8 | * 9 | * @author Guoqing 10 | */ 11 | public class AssertException extends RuntimeException { 12 | /** */ 13 | private static final long serialVersionUID = 1L; 14 | private int code = 1; 15 | private Level level; 16 | 17 | public AssertException(int code, String message, Throwable cause) { 18 | super(message, cause); 19 | this.code = code; 20 | } 21 | 22 | public AssertException(String message) { 23 | super(message); 24 | } 25 | 26 | public AssertException(Level level, String message) { 27 | super(message); 28 | this.level = level; 29 | } 30 | 31 | public AssertException(Throwable cause) { 32 | super(cause); 33 | } 34 | 35 | public AssertException(int code, String message) { 36 | super(message); 37 | this.code = code; 38 | } 39 | 40 | /** 41 | * Getter method for property code. 42 | * 43 | * @return property value of code 44 | */ 45 | public int getCode() { 46 | return code; 47 | } 48 | 49 | /** 50 | * Getter method for property level. 51 | * 52 | * @return property value of level 53 | */ 54 | public final Level getLevel() { 55 | return level; 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/cn/com/bluemoon/threads/CallableAndFuture.java: -------------------------------------------------------------------------------- 1 | package cn.com.bluemoon.threads; 2 | 3 | import java.util.concurrent.Callable; 4 | import java.util.concurrent.CompletionService; 5 | import java.util.concurrent.ExecutorCompletionService; 6 | import java.util.concurrent.ExecutorService; 7 | import java.util.concurrent.Executors; 8 | 9 | /** 10 | * 执行多个带返回值的任务,并取得多个返回值 11 | * 异步非阻塞获取并行任务执行结果 12 | * @author Guoqing.Lee 13 | * @date 2019年1月9日 下午3:56:46 14 | * 15 | */ 16 | public class CallableAndFuture { 17 | 18 | public static void main(String[] args) { 19 | ExecutorService threadPool = Executors.newCachedThreadPool(); 20 | CompletionService cs = new ExecutorCompletionService(threadPool); 21 | for (int i = 0; i < 5; i++) { 22 | final int taskId = i; 23 | cs.submit(new Callable() { 24 | 25 | @Override 26 | public Integer call() throws Exception { 27 | //taskId为3的时候等待3s,最后输出的结果永远是3,证明获取结果是非阻塞 28 | if( taskId == 3 ) { 29 | Thread.sleep(3000); 30 | } 31 | return taskId; 32 | } 33 | }); 34 | } 35 | 36 | threadPool.shutdown(); 37 | for (int i = 0; i < 5; i++) { 38 | try { 39 | System.out.println(cs.take().get()); 40 | } catch (Exception e) { 41 | e.printStackTrace(); 42 | } 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/cn/com/bluemoon/mybatis/datasource/DynamicDataSourceTransactionManager.java: -------------------------------------------------------------------------------- 1 | package cn.com.bluemoon.mybatis.datasource; 2 | 3 | import org.springframework.jdbc.datasource.DataSourceTransactionManager; 4 | import org.springframework.transaction.TransactionDefinition; 5 | 6 | import javax.sql.DataSource; 7 | 8 | /** 9 | * 动态数据源事务管理器 10 | * 11 | * @author mij 12 | */ 13 | public class DynamicDataSourceTransactionManager extends DataSourceTransactionManager { 14 | 15 | public DynamicDataSourceTransactionManager(DataSource dataSource) { 16 | super(dataSource); 17 | } 18 | 19 | @Override 20 | protected void doBegin(Object transaction, TransactionDefinition definition) { 21 | 22 | //设置数据源 23 | boolean readOnly = definition.isReadOnly(); 24 | //只读事务到读库,读写事务到写库 25 | if (readOnly) { 26 | DynamicDataSourceHolder.putDataSource(DynamicDataSourceGlobal.READ); 27 | } else { 28 | DynamicDataSourceHolder.putDataSource(DynamicDataSourceGlobal.WRITE); 29 | } 30 | super.doBegin(transaction, definition); 31 | } 32 | 33 | @Override 34 | protected void doCleanupAfterCompletion(Object transaction) { 35 | super.doCleanupAfterCompletion(transaction); 36 | //清理本地线程的数据源 37 | DynamicDataSourceHolder.clearDataSource(); 38 | } 39 | } -------------------------------------------------------------------------------- /src/main/java/cn/com/bluemoon/common/config/SwaggerConfig.java: -------------------------------------------------------------------------------- 1 | package cn.com.bluemoon.common.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | import springfox.documentation.builders.ApiInfoBuilder; 8 | import springfox.documentation.builders.PathSelectors; 9 | import springfox.documentation.builders.RequestHandlerSelectors; 10 | import springfox.documentation.service.ApiInfo; 11 | import springfox.documentation.service.Contact; 12 | import springfox.documentation.spi.DocumentationType; 13 | import springfox.documentation.spring.web.plugins.Docket; 14 | import springfox.documentation.swagger2.annotations.EnableSwagger2; 15 | 16 | @Configuration 17 | @EnableSwagger2 18 | public class SwaggerConfig { 19 | 20 | @Bean 21 | public Docket userApi() { 22 | return new Docket(DocumentationType.SWAGGER_2).groupName("秒杀案例").apiInfo(apiInfo()).select() 23 | .apis(RequestHandlerSelectors.basePackage("cn.com.bluemoon.controller")).paths(PathSelectors.any()).build(); 24 | } 25 | 26 | // 预览地址:swagger-ui.html 27 | private ApiInfo apiInfo() { 28 | return new ApiInfoBuilder().title("SpringBoot 中使用Swagger2构建文档").termsOfServiceUrl("https://blog.52itstyle.com") 29 | .contact(new Contact("Guoqing ", "http://www.cnblogs.com/ocean-sky/", "514471352@qq.com")).version("1.0").build(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/cn/com/bluemoon/common/response/ResponseBean.java: -------------------------------------------------------------------------------- 1 | package cn.com.bluemoon.common.response; 2 | 3 | 4 | /** 5 | * 返回对象的基础bean 6 | * @author Guoqing 7 | * 8 | */ 9 | public class ResponseBean { 10 | 11 | /** 12 | * 请求是否成功 13 | */ 14 | private boolean isSuccess; 15 | 16 | /** 17 | * 请求响应码,成功时为0 18 | */ 19 | private int responseCode; 20 | 21 | /** 22 | * 请求响应码对应描述 23 | */ 24 | private String responseMsg; 25 | 26 | /** 27 | * 请求响应的数据对象 28 | */ 29 | private Object data; 30 | 31 | public ResponseBean(boolean isSuccess, int responseCode, String responseMsg, Object data) { 32 | super(); 33 | this.isSuccess = isSuccess; 34 | this.responseCode = responseCode; 35 | this.responseMsg = responseMsg; 36 | this.data = data; 37 | } 38 | 39 | public boolean getIsSuccess() { 40 | return isSuccess; 41 | } 42 | 43 | public void setIsSuccess(boolean isSuccess) { 44 | this.isSuccess = isSuccess; 45 | } 46 | 47 | public int getResponseCode() { 48 | return responseCode; 49 | } 50 | 51 | public void setResponseCode(int responseCode) { 52 | this.responseCode = responseCode; 53 | } 54 | 55 | public String getResponseMsg() { 56 | return responseMsg; 57 | } 58 | 59 | public void setResponseMsg(String responseMsg) { 60 | this.responseMsg = responseMsg; 61 | } 62 | 63 | public Object getData() { 64 | return data; 65 | } 66 | 67 | public void setData(Object data) { 68 | this.data = data; 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/cn/com/bluemoon/threads/UserThreadPool.java: -------------------------------------------------------------------------------- 1 | package cn.com.bluemoon.threads; 2 | 3 | import java.util.concurrent.BlockingQueue; 4 | import java.util.concurrent.LinkedBlockingQueue; 5 | import java.util.concurrent.ThreadPoolExecutor; 6 | import java.util.concurrent.TimeUnit; 7 | import java.util.concurrent.atomic.AtomicLong; 8 | 9 | /** 10 | * 多线程编程 11 | * @author Guoqing.Lee 12 | * @date 2019年5月24日 下午5:19:35 13 | * 14 | */ 15 | public class UserThreadPool { 16 | 17 | public static void main(String[] args) { 18 | //缓存队列设置固定长度为2,为了快速出发rejectHandler 19 | BlockingQueue queue = new LinkedBlockingQueue(2); 20 | //假设外部任务线程的来源由机房1和机房2的混合调用 21 | UserThreadFactory f1 = new UserThreadFactory("第1机房"); 22 | UserThreadFactory f2 = new UserThreadFactory("第2机房"); 23 | 24 | UserRejectHandler handler = new UserRejectHandler(); 25 | 26 | //核心线程为1,最大线程为2,为了保证触发rejectHandler 27 | ThreadPoolExecutor threadPoolFirst = new ThreadPoolExecutor(1, 2, 60, TimeUnit.SECONDS, queue, f1, handler); 28 | //利用第二个线程工厂实例创建第二个线程池 29 | ThreadPoolExecutor threadPoolSecond = new ThreadPoolExecutor(1, 2, 60, TimeUnit.SECONDS, queue, f2, handler); 30 | 31 | //创建400个任务线程 32 | Runnable task = new Task(); 33 | for(int i = 0; i < 200; i++ ) { 34 | threadPoolFirst.execute(task); 35 | threadPoolSecond.execute(task); 36 | } 37 | } 38 | 39 | static class Task implements Runnable{ 40 | private final AtomicLong count = new AtomicLong(0L); 41 | 42 | @Override 43 | public void run() { 44 | System.out.println("running_" + count.getAndIncrement()); 45 | } 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/cn/com/bluemoon/common/exception/WebExceptionEnum.java: -------------------------------------------------------------------------------- 1 | package cn.com.bluemoon.common.exception; 2 | 3 | /** 4 | * 异常枚举 5 | * 6 | * @author Guoqing 7 | * @Date 2018/06/28 下午10:33 8 | */ 9 | public enum WebExceptionEnum implements ServiceExceptionEnum{ 10 | 11 | /** 12 | * 其他 13 | */ 14 | WRITE_ERROR(false, 500, "渲染界面错误"), 15 | 16 | /** 17 | * 文件上传 18 | */ 19 | FILE_READING_ERROR(false, 400, "FILE_READING_ERROR!"), 20 | FILE_NOT_FOUND(false, 400, "FILE_NOT_FOUND!"), 21 | 22 | /** 23 | * 错误的请求 24 | */ 25 | REQUEST_NULL(false, 400, "请求有错误"), 26 | REQUEST_LIMIT(false, 400, "请求已达上限"), 27 | SERVER_ERROR(false, 500, "服务器异常"), 28 | TOKEN_NOT_FUND(false, 401, "未授权"), 29 | TOKEN_ERROR(false, 700, "token验证失败"); 30 | 31 | private WebExceptionEnum(Boolean isSuccess, Integer responseCode, String responseMsg) { 32 | this.isSuccess = isSuccess; 33 | this.responseCode = responseCode; 34 | this.responseMsg = responseMsg; 35 | } 36 | 37 | private Boolean isSuccess; 38 | 39 | private Integer responseCode; 40 | 41 | private String responseMsg; 42 | 43 | public Boolean getIsSuccess() { 44 | return isSuccess; 45 | } 46 | 47 | public void setIsSuccess(Boolean isSuccess) { 48 | this.isSuccess = isSuccess; 49 | } 50 | 51 | public Integer getResponseCode() { 52 | return responseCode; 53 | } 54 | 55 | public void setResponseCode(Integer responseCode) { 56 | this.responseCode = responseCode; 57 | } 58 | 59 | public String getResponseMsg() { 60 | return responseMsg; 61 | } 62 | 63 | public void setResponseMsg(String responseMsg) { 64 | this.responseMsg = responseMsg; 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/cn/com/bluemoon/common/response/SeckillInfoResponse.java: -------------------------------------------------------------------------------- 1 | /** 2 | *

Title: SeckillResponse.java

3 | *

Description:

4 | *

Copyright: Copyright (c) 2018

5 | *

Company: www.bluemoon.com

6 | * @author Guoqing 7 | * @date 2018年8月10日 8 | */ 9 | package cn.com.bluemoon.common.response; 10 | 11 | import cn.com.bluemoon.common.response.BaseResponse; 12 | 13 | /** 14 | *

Title: SeckillResponse

15 | *

Description:

16 | * @author Guoqing 17 | * @date 2018年8月10日 18 | */ 19 | public class SeckillInfoResponse extends BaseResponse { 20 | 21 | private int refreshTime; //下一次请求刷新时间 22 | private long orderId; //订单ID 23 | private String orderCode; //订单编码 24 | private String orderQualificationCode; //下单资格码 25 | /** 26 | * @return the refreshTime 27 | */ 28 | public int getRefreshTime() { 29 | return refreshTime; 30 | } 31 | /** 32 | * @param refreshTime the refreshTime to set 33 | */ 34 | public void setRefreshTime(int refreshTime) { 35 | this.refreshTime = refreshTime; 36 | } 37 | /** 38 | * @return the orderId 39 | */ 40 | public long getOrderId() { 41 | return orderId; 42 | } 43 | /** 44 | * @param orderId the orderId to set 45 | */ 46 | public void setOrderId(long orderId) { 47 | this.orderId = orderId; 48 | } 49 | /** 50 | * @return the orderCode 51 | */ 52 | public String getOrderCode() { 53 | return orderCode; 54 | } 55 | /** 56 | * @param orderCode the orderCode to set 57 | */ 58 | public void setOrderCode(String orderCode) { 59 | this.orderCode = orderCode; 60 | } 61 | public String getOrderQualificationCode() { 62 | return orderQualificationCode; 63 | } 64 | public void setOrderQualificationCode(String orderQualificationCode) { 65 | this.orderQualificationCode = orderQualificationCode; 66 | } 67 | 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/cn/com/bluemoon/redis/repository/RedisCacheConfig.java: -------------------------------------------------------------------------------- 1 | package cn.com.bluemoon.redis.repository; 2 | 3 | import org.springframework.cache.CacheManager; 4 | import org.springframework.cache.annotation.EnableCaching; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.data.redis.cache.RedisCacheManager; 8 | import org.springframework.data.redis.connection.RedisConnectionFactory; 9 | import org.springframework.data.redis.core.RedisTemplate; 10 | import org.springframework.data.redis.serializer.RedisSerializer; 11 | import org.springframework.data.redis.serializer.StringRedisSerializer; 12 | 13 | @Configuration 14 | @EnableCaching 15 | public class RedisCacheConfig { 16 | 17 | @Bean 18 | public CacheManager cacheManager(RedisTemplate redisTemplate){ 19 | CacheManager cacheManager = new RedisCacheManager(redisTemplate); 20 | return cacheManager; 21 | } 22 | @Bean 23 | public RedisTemplate redisTemplate(RedisConnectionFactory factory){ 24 | RedisTemplate redisTemplate = new RedisTemplate(); 25 | redisTemplate.setConnectionFactory(factory); 26 | // key序列化方式;(不然会出现乱码;),但是如果方法上有Long等非String类型的话,会报类型转换错误; 27 | // 所以在没有自己定义key生成策略的时候,以下这个代码建议不要这么写,可以不配置或者自己实现ObjectRedisSerializer 28 | // 或者JdkSerializationRedisSerializer序列化方式; 29 | RedisSerializer redisSerializer = new StringRedisSerializer();// Long类型不可以会出现异常信息; 30 | redisTemplate.setKeySerializer(redisSerializer); 31 | redisTemplate.setHashKeySerializer(redisSerializer); 32 | return redisTemplate; 33 | } 34 | 35 | @Bean 36 | public RedisRepository redisRepository(RedisTemplate redisTemplate){ 37 | return new RedisRepository(redisTemplate); 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /src/main/java/cn/com/bluemoon/utils/AssertUtil.java: -------------------------------------------------------------------------------- 1 | package cn.com.bluemoon.utils; 2 | 3 | import java.util.Collection; 4 | 5 | import org.springframework.util.Assert; 6 | import org.springframework.util.CollectionUtils; 7 | import org.springframework.util.StringUtils; 8 | 9 | import cn.com.bluemoon.common.exception.AssertException; 10 | 11 | /** 12 | * 断言工具 13 | * Created by Guoqing on 2016/10/22. 14 | */ 15 | public class AssertUtil extends Assert { 16 | 17 | /** 18 | * 判布尔型 19 | * @param expression 20 | * @param code 21 | * @param message 22 | */ 23 | public static void isTrue(boolean expression, int code, String message) { 24 | if (!expression) { 25 | throw new AssertException(code,message); 26 | } 27 | } 28 | 29 | /** 30 | * 判对象为空 31 | * @param object 32 | * @param code 33 | * @param message 34 | */ 35 | public static void isNull(Object object, int code, String message) { 36 | if (object != null) { 37 | throw new AssertException(code,message); 38 | } 39 | } 40 | 41 | /** 42 | * 判对象非空 43 | * @param object 44 | * @param code 45 | * @param message 46 | */ 47 | public static void notNull(Object object, int code, String message) { 48 | if (object == null) { 49 | throw new AssertException(code,message); 50 | } 51 | } 52 | 53 | /** 54 | * 判字符串是否有值 55 | * @param text 56 | * @param code 57 | * @param message 58 | */ 59 | public static void hasLength(String text, int code, String message) { 60 | if (!StringUtils.hasLength(text)) { 61 | throw new AssertException(code,message); 62 | } 63 | } 64 | 65 | /** 66 | * 判集合是否为空 67 | * @param collection 68 | * @param code 69 | * @param message 70 | */ 71 | public static void notEmpty(Collection collection, int code, String message) { 72 | if (CollectionUtils.isEmpty(collection)) { 73 | throw new AssertException(code,message); 74 | } 75 | } 76 | 77 | } -------------------------------------------------------------------------------- /src/main/java/cn/com/bluemoon/mybatis/datasource/DynamicDataSource.java: -------------------------------------------------------------------------------- 1 | package cn.com.bluemoon.mybatis.datasource; 2 | 3 | import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | /** 9 | * 动态数据源实现读写分离 10 | * 11 | * @author mij 12 | */ 13 | public class DynamicDataSource extends AbstractRoutingDataSource { 14 | 15 | /** 16 | * 写数据源 17 | */ 18 | private Object writeDataSource; 19 | /** 20 | * 读数据源 21 | */ 22 | private Object readDataSource; 23 | 24 | @Override 25 | public void afterPropertiesSet() { 26 | if (this.writeDataSource == null) { 27 | throw new IllegalArgumentException("Property 'writeDataSource' is required"); 28 | } 29 | setDefaultTargetDataSource(writeDataSource); 30 | Map targetDataSources = new HashMap<>(); 31 | targetDataSources.put(DynamicDataSourceGlobal.WRITE.name(), writeDataSource); 32 | if (readDataSource != null) { 33 | targetDataSources.put(DynamicDataSourceGlobal.READ.name(), readDataSource); 34 | } 35 | setTargetDataSources(targetDataSources); 36 | super.afterPropertiesSet(); 37 | } 38 | 39 | @Override 40 | protected Object determineCurrentLookupKey() { 41 | 42 | DynamicDataSourceGlobal dynamicDataSourceGlobal = DynamicDataSourceHolder.getDataSource(); 43 | 44 | if (dynamicDataSourceGlobal == null 45 | || dynamicDataSourceGlobal == DynamicDataSourceGlobal.WRITE) { 46 | return DynamicDataSourceGlobal.WRITE.name(); 47 | } 48 | 49 | return DynamicDataSourceGlobal.READ.name(); 50 | } 51 | 52 | public void setWriteDataSource(Object writeDataSource) { 53 | this.writeDataSource = writeDataSource; 54 | } 55 | 56 | public Object getWriteDataSource() { 57 | return writeDataSource; 58 | } 59 | 60 | public Object getReadDataSource() { 61 | return readDataSource; 62 | } 63 | 64 | public void setReadDataSource(Object readDataSource) { 65 | this.readDataSource = readDataSource; 66 | } 67 | } -------------------------------------------------------------------------------- /src/main/java/cn/com/bluemoon/threads/CallableAndFuture2.java: -------------------------------------------------------------------------------- 1 | package cn.com.bluemoon.threads; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.concurrent.Callable; 6 | import java.util.concurrent.ExecutionException; 7 | import java.util.concurrent.ExecutorService; 8 | import java.util.concurrent.Executors; 9 | import java.util.concurrent.Future; 10 | 11 | /** 12 | * 使用ExecutorService、Callable、Future实现有返回结果的多线程 13 | * @author Guoqing.Lee 14 | * @date 2019年1月9日 下午4:37:32 15 | * 16 | */ 17 | public class CallableAndFuture2 { 18 | 19 | @SuppressWarnings({ "rawtypes", "unchecked" }) 20 | public static void main(String[] args) throws InterruptedException, ExecutionException { 21 | System.out.println("----程序开始运行----"); 22 | long time1 = System.currentTimeMillis(); 23 | 24 | int taskSize = 5; 25 | //创建一个线程池 26 | ExecutorService threadPool = Executors.newFixedThreadPool(taskSize); 27 | //创建多个有返回值的任务 28 | List list = new ArrayList(); 29 | for (int i = 0; i < taskSize; i++) { 30 | Callable callable = new MyCallable(i + ""); 31 | //执行任务并获取Future对象 32 | Future future = threadPool.submit(callable); 33 | list.add(future); 34 | } 35 | 36 | //关闭线程池 37 | threadPool.shutdown(); 38 | 39 | //获取所有并发任务的运行结果 40 | for( Future future : list ) { 41 | //从Future对象上获取任务的返回值,并输出到控制台 42 | System.out.println(">>>" + future.get().toString() ); 43 | } 44 | long time2 = System.currentTimeMillis(); 45 | System.out.println("----程序运行结束----,程序运行时间【" + (time2 - time1) + "毫秒】"); 46 | } 47 | 48 | static class MyCallable implements Callable { 49 | 50 | private String taskNum; 51 | 52 | public MyCallable(String taskNum) { 53 | this.taskNum = taskNum; 54 | } 55 | 56 | @Override 57 | public Object call() throws Exception { 58 | System.out.println(">>>" + taskNum + "任务启动"); 59 | long startTime = System.currentTimeMillis(); 60 | if("1".equals(taskNum)) { 61 | Thread.sleep(1500); 62 | }else { 63 | Thread.sleep(1000); 64 | } 65 | long endTime = System.currentTimeMillis(); 66 | System.out.println(">>>" + taskNum + "任务终止"); 67 | return taskNum + "任务返回运行结果,当前任务运行时间【" + (endTime - startTime) + "毫秒】"; 68 | } 69 | 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/cn/com/bluemoon/common/interceptor/LimitInterceptor.java: -------------------------------------------------------------------------------- 1 | package cn.com.bluemoon.common.interceptor; 2 | 3 | import java.util.concurrent.TimeUnit; 4 | 5 | import javax.servlet.http.HttpServletRequest; 6 | import javax.servlet.http.HttpServletResponse; 7 | 8 | import org.springframework.web.servlet.ModelAndView; 9 | import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; 10 | 11 | import com.google.common.util.concurrent.RateLimiter; 12 | 13 | import cn.com.bluemoon.common.exception.WebException; 14 | import cn.com.bluemoon.common.exception.WebExceptionEnum; 15 | 16 | /** 17 | * Guava 限流器 18 | * @author Guoqing 19 | * 20 | */ 21 | public class LimitInterceptor extends HandlerInterceptorAdapter { 22 | 23 | public enum LimitType { 24 | DROP, //丢弃 25 | WAIT //等待 26 | } 27 | 28 | /** 29 | * 限流器 30 | */ 31 | private RateLimiter limiter; 32 | 33 | /** 34 | * 限流方式 35 | */ 36 | private LimitType limitType = LimitType.DROP; 37 | 38 | public LimitInterceptor() { 39 | this.limiter = RateLimiter.create(1); 40 | } 41 | 42 | /** 43 | * @param tps 限流(每秒处理量) 44 | * @param limitType 45 | */ 46 | public LimitInterceptor(int tps, LimitInterceptor.LimitType limitType) { 47 | this.limiter = RateLimiter.create(tps); 48 | this.limitType = limitType; 49 | } 50 | 51 | /** 52 | * @param permitsPerSecond 每秒新增的令牌数 53 | * @param limitType 限流类型 54 | */ 55 | public LimitInterceptor(double permitsPerSecond, LimitInterceptor.LimitType limitType) { 56 | this.limiter = RateLimiter.create(permitsPerSecond, 1000, TimeUnit.MILLISECONDS); 57 | this.limitType = limitType; 58 | } 59 | 60 | @Override 61 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 62 | if (limitType.equals(LimitType.DROP)) { 63 | if (limiter.tryAcquire()) { 64 | return super.preHandle(request, response, handler); 65 | } 66 | } else { 67 | limiter.acquire(); 68 | return super.preHandle(request, response, handler); 69 | } 70 | throw new WebException(WebExceptionEnum.REQUEST_LIMIT);//达到限流后,往页面提示的错误信息。 71 | } 72 | 73 | @Override 74 | public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { 75 | super.postHandle(request, response, handler, modelAndView); 76 | } 77 | 78 | @Override 79 | public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { 80 | super.afterCompletion(request, response, handler, ex); 81 | } 82 | 83 | public RateLimiter getLimiter() { 84 | return limiter; 85 | } 86 | 87 | public void setLimiter(RateLimiter limiter) { 88 | this.limiter = limiter; 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/cn/com/bluemoon/common/exception/GlobalExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package cn.com.bluemoon.common.exception; 2 | 3 | import javax.servlet.http.HttpServletRequest; 4 | 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.web.bind.annotation.ControllerAdvice; 8 | import org.springframework.web.bind.annotation.ExceptionHandler; 9 | import org.springframework.web.bind.annotation.ResponseBody; 10 | 11 | import com.alibaba.fastjson.JSONException; 12 | import com.fasterxml.jackson.core.JsonParseException; 13 | 14 | import cn.com.bluemoon.common.response.ResponseBean; 15 | 16 | 17 | /** 18 | * 全局异常处理 19 | * @author Guoqing 20 | * @version 1.0 21 | */ 22 | @ControllerAdvice 23 | public class GlobalExceptionHandler { 24 | 25 | private final Logger logger = LoggerFactory.getLogger(getClass()); 26 | 27 | @ExceptionHandler(value = AssertException.class) 28 | @ResponseBody 29 | public ResponseBean BootExceptionHandler(HttpServletRequest req, AssertException e) { 30 | ResponseBean response = new ResponseBean(false, e.getCode(), e.getMessage(), null); 31 | logger.error(e.getCode()+"",e); 32 | logger.warn("ExceptionHandler:"+response.toString()); 33 | return response; 34 | } 35 | 36 | @ExceptionHandler(value = JSONException.class) 37 | @ResponseBody 38 | public ResponseBean JSONExceptionHandler(HttpServletRequest req, JSONException e) { 39 | ResponseBean response = new ResponseBean(false, 1102, "请求参数格式异常", null); 40 | logger.error("1102",e); 41 | logger.error("ExceptionHandler"+response.toString()); 42 | return response; 43 | } 44 | 45 | @ExceptionHandler(value = JsonParseException.class) 46 | @ResponseBody 47 | public ResponseBean JsonParseExceptionHandler(HttpServletRequest req, JsonParseException e) { 48 | ResponseBean response = new ResponseBean(false, 1102, "请求参数格式异常", null); 49 | logger.error("1102",e); 50 | logger.error("ExceptionHandler"+response.toString()); 51 | return response; 52 | } 53 | 54 | @ExceptionHandler(value = WebException.class) 55 | @ResponseBody 56 | public ResponseBean ExceptionHandler(HttpServletRequest req, WebException e) { 57 | ResponseBean response = new ResponseBean(e.getIsSuccess(), e.getResponseCode(), e.getResponseMsg(), null); 58 | logger.error(e.getResponseCode()+"",e); 59 | logger.error("ExceptionHandler"+response.toString()); 60 | return response; 61 | } 62 | 63 | @ExceptionHandler(value = Exception.class) 64 | @ResponseBody 65 | public ResponseBean ExceptionHandler(HttpServletRequest req, Exception e) { 66 | ResponseBean response = new ResponseBean(false, 1000, "服务器正在繁忙,请稍后再试哦~", null); 67 | logger.error("1000",e); 68 | logger.error("ExceptionHandler"+response.toString()); 69 | return response; 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8080 3 | tomcat: 4 | max-threads: 1000 5 | accept-count: 1000 6 | max-connections: 2000 7 | 8 | zookeeper: 9 | address: 192.168.240.15\:2181,192.168.240.15\:2182,192.168.240.15\:2183 10 | 11 | spring: 12 | application: 13 | name: distributed-seckill 14 | session: 15 | store-type: none 16 | kafka: 17 | bootstrap-servers: 18 | - 192.168.240.42:9092 19 | - 192.168.240.43:9092 20 | - 192.168.240.44:9092 21 | consumer: 22 | group-id: 0 23 | key-deserializer: org.apache.kafka.common.serialization.StringDeserializer 24 | value-deserializer: org.apache.kafka.common.serialization.StringDeserializer 25 | producer: 26 | key-serializer: org.apache.kafka.common.serialization.StringSerializer 27 | value-serializer: org.apache.kafka.common.serialization.StringSerializer 28 | batch-size: 65536 29 | buffer-memory: 524288 30 | redis: 31 | database: 0 32 | port: 56379 33 | # password: dXzMHN2MaHUX 34 | password: Mon56BuEcXzZ 35 | timeout: 3000 36 | host: 192.168.234.33 37 | # cluster: 38 | # nodes: 192.168.234.18:6579,192.168.234.28:6579,192.168.234.29:6579,192.168.234.30:6579,192.168.234.6:6579,192.168.234.43:6579 39 | pool: 40 | max-active: 8 41 | max-wait: 3000 42 | max-idle: 8 43 | min-idle: 0 44 | datasource: 45 | write: 46 | url: jdbc:mysql://127.0.0.1:8096/bm_market?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true 47 | username: BlueMoon 48 | password: bm.mall.123 49 | driver-class-name: com.mysql.jdbc.Driver 50 | max-active: 20 51 | initial-size: 1 52 | min-idle: 3 53 | max-wait: 60000 54 | time-between-eviction-runs-millis: 60000 55 | min-evictable-idle-time-millis: 300000 56 | validation-query: SELECT 'x' FROM DUAL 57 | test-while-idle: true 58 | test-on-borrow: false 59 | test-on-return: false 60 | connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=3000 61 | read: 62 | url: jdbc:mysql://127.0.0.1:8096/bm_market?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true 63 | username: BlueMoon 64 | password: bm.mall.123 65 | driver-class-name: com.mysql.jdbc.Driver 66 | max-active: 20 67 | initial-size: 1 68 | min-idle: 3 69 | max-wait: 60000 70 | time-between-eviction-runs-millis: 60000 71 | min-evictable-idle-time-millis: 300000 72 | validation-query: SELECT 'x' FROM DUAL 73 | test-while-idle: true 74 | test-on-borrow: false 75 | test-on-return: false 76 | connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=3000 77 | mvc: 78 | view: 79 | prefix: /views/ 80 | suffix: .html 81 | 82 | mybatis: 83 | config-location: classpath:mybatis-config.xml 84 | mapper-locations: classpath:mapper/*.xml 85 | 86 | 87 | -------------------------------------------------------------------------------- /src/main/java/cn/com/bluemoon/common/logs/LogAspect.java: -------------------------------------------------------------------------------- 1 | package cn.com.bluemoon.common.logs; 2 | 3 | import java.util.Arrays; 4 | 5 | import javax.servlet.http.HttpServletRequest; 6 | 7 | import org.aspectj.lang.JoinPoint; 8 | import org.aspectj.lang.annotation.After; 9 | import org.aspectj.lang.annotation.AfterReturning; 10 | import org.aspectj.lang.annotation.Aspect; 11 | import org.aspectj.lang.annotation.Before; 12 | import org.aspectj.lang.annotation.Pointcut; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | import org.springframework.core.annotation.Order; 16 | import org.springframework.stereotype.Component; 17 | import org.springframework.web.context.request.RequestContextHolder; 18 | import org.springframework.web.context.request.ServletRequestAttributes; 19 | 20 | /** 21 | * 统一日志处理 22 | * @author Guoqing.Lee 23 | * @date 2019年1月3日 上午11:29:13 24 | * 25 | */ 26 | @Aspect 27 | @Component 28 | @Order(1) 29 | public class LogAspect { 30 | 31 | private Logger logger = LoggerFactory.getLogger(LogAspect.class); 32 | 33 | ThreadLocal startTime = new ThreadLocal(); 34 | 35 | @Pointcut("execution(public * cn.com.bluemoon.controller.*.*(..))") 36 | public void logPointCut() {} 37 | 38 | /** 39 | * 在切点前执行 40 | * @param joinPoint 41 | */ 42 | @Before("logPointCut()") 43 | public void doBefore(JoinPoint joinPoint) { 44 | startTime.set(System.currentTimeMillis()); 45 | ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); 46 | HttpServletRequest request = requestAttributes.getRequest(); 47 | String url = request.getRequestURL().toString(); 48 | String httpMethod = request.getMethod(); 49 | String ip = getIpAddr(request); 50 | String classMethod = joinPoint.getSignature().getDeclaringTypeName() + "."+ joinPoint.getSignature().getName(); 51 | String parameters = Arrays.toString(joinPoint.getArgs()); 52 | logger.info("REQUEST URL:" + url + " | HTTP METHOD: " + httpMethod + " | IP: " + ip + " | CLASS_METHOD: " + classMethod 53 | + " | ARGS:" + parameters); 54 | } 55 | 56 | /** 57 | * 在切点后,return前执行 58 | * @param joinPoint 59 | */ 60 | @After("logPointCut()") 61 | public void doAfter(JoinPoint joinPoint) {} 62 | 63 | /** 64 | * 在切入点,return后执行,如果相对某些方法的返回参数进行处理,可以在此处执行 65 | * @param object 66 | */ 67 | @AfterReturning(returning = "object",pointcut = "logPointCut()") 68 | public void doAfterReturning(Object object){ 69 | logger.info("RESPONSE TIME: "+ (System.currentTimeMillis() - startTime.get()) + "ms"); 70 | logger.info("RESPONSE BODY: "+ object); 71 | } 72 | 73 | /** 74 | * 获取真实的IP地址 75 | * @param request 76 | * @return 77 | */ 78 | private String getIpAddr(HttpServletRequest request) { 79 | String ip = request.getHeader("x-forwarded-for"); 80 | if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 81 | ip = request.getHeader("Proxy-Client-IP"); 82 | } 83 | if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 84 | ip = request.getHeader("WL-Proxy-Client-IP"); 85 | } 86 | if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { 87 | ip = request.getRemoteAddr(); 88 | } 89 | return ip; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/resources/mybatis-config.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /src/main/java/cn/com/bluemoon/redis/repository/RedissonProperties.java: -------------------------------------------------------------------------------- 1 | package cn.com.bluemoon.redis.repository; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | import org.springframework.boot.context.properties.ConfigurationProperties; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | /** 10 | * redisson配置类 11 | * @author Guoqing.Lee 12 | * @date 2019年1月23日 下午3:01:39 13 | * 14 | */ 15 | /** 16 | * @author Guoqing.Lee 17 | * @date 2019年1月24日 上午11:33:48 18 | * 19 | */ 20 | @Configuration 21 | @ConfigurationProperties(prefix="spring.redis") 22 | public class RedissonProperties { 23 | 24 | private int timeout; 25 | 26 | private String host; 27 | 28 | private String port; 29 | 30 | private String password; 31 | 32 | private int database = 0; 33 | 34 | private int connectionPoolSize = 64; 35 | 36 | private int connectionMinimumIdleSize=10; 37 | 38 | private int slaveConnectionPoolSize = 250; 39 | 40 | private int masterConnectionPoolSize = 250; 41 | 42 | private String[] sentinelAddresses; 43 | 44 | private String masterName; 45 | 46 | private Map cluster = new HashMap<>(); 47 | 48 | public int getTimeout() { 49 | return timeout; 50 | } 51 | 52 | public void setTimeout(int timeout) { 53 | this.timeout = timeout; 54 | } 55 | 56 | public String getHost() { 57 | return host; 58 | } 59 | 60 | public void setHost(String host) { 61 | this.host = host; 62 | } 63 | 64 | public String getPort() { 65 | return port; 66 | } 67 | 68 | public void setPort(String port) { 69 | this.port = port; 70 | } 71 | 72 | public String getPassword() { 73 | return password; 74 | } 75 | 76 | public void setPassword(String password) { 77 | this.password = password; 78 | } 79 | 80 | public int getDatabase() { 81 | return database; 82 | } 83 | 84 | public void setDatabase(int database) { 85 | this.database = database; 86 | } 87 | 88 | public int getConnectionPoolSize() { 89 | return connectionPoolSize; 90 | } 91 | 92 | public void setConnectionPoolSize(int connectionPoolSize) { 93 | this.connectionPoolSize = connectionPoolSize; 94 | } 95 | 96 | public int getConnectionMinimumIdleSize() { 97 | return connectionMinimumIdleSize; 98 | } 99 | 100 | public void setConnectionMinimumIdleSize(int connectionMinimumIdleSize) { 101 | this.connectionMinimumIdleSize = connectionMinimumIdleSize; 102 | } 103 | 104 | public int getSlaveConnectionPoolSize() { 105 | return slaveConnectionPoolSize; 106 | } 107 | 108 | public void setSlaveConnectionPoolSize(int slaveConnectionPoolSize) { 109 | this.slaveConnectionPoolSize = slaveConnectionPoolSize; 110 | } 111 | 112 | public int getMasterConnectionPoolSize() { 113 | return masterConnectionPoolSize; 114 | } 115 | 116 | public void setMasterConnectionPoolSize(int masterConnectionPoolSize) { 117 | this.masterConnectionPoolSize = masterConnectionPoolSize; 118 | } 119 | 120 | public String[] getSentinelAddresses() { 121 | return sentinelAddresses; 122 | } 123 | 124 | public void setSentinelAddresses(String sentinelAddresses) { 125 | this.sentinelAddresses = sentinelAddresses.split(","); 126 | } 127 | 128 | public String getMasterName() { 129 | return masterName; 130 | } 131 | 132 | public void setMasterName(String masterName) { 133 | this.masterName = masterName; 134 | } 135 | 136 | public Map getCluster() { 137 | return cluster; 138 | } 139 | 140 | public void setCluster(Map cluster) { 141 | this.cluster = cluster; 142 | } 143 | 144 | } 145 | -------------------------------------------------------------------------------- /src/main/java/cn/com/bluemoon/common/config/DatasourceConfig.java: -------------------------------------------------------------------------------- 1 | package cn.com.bluemoon.common.config; 2 | 3 | import com.alibaba.druid.pool.DruidDataSource; 4 | 5 | import cn.com.bluemoon.common.exception.WebException; 6 | import cn.com.bluemoon.common.exception.WebExceptionEnum; 7 | import cn.com.bluemoon.mybatis.datasource.DynamicDataSource; 8 | import cn.com.bluemoon.mybatis.datasource.DynamicDataSourceTransactionManager; 9 | 10 | import org.apache.ibatis.session.SqlSessionFactory; 11 | import org.mybatis.spring.SqlSessionFactoryBean; 12 | import org.mybatis.spring.boot.autoconfigure.MybatisProperties; 13 | import org.springframework.beans.factory.annotation.Qualifier; 14 | import org.springframework.boot.context.properties.ConfigurationProperties; 15 | import org.springframework.context.annotation.Bean; 16 | import org.springframework.context.annotation.Configuration; 17 | import org.springframework.context.annotation.Primary; 18 | import org.springframework.core.io.DefaultResourceLoader; 19 | import org.springframework.jdbc.datasource.DataSourceTransactionManager; 20 | 21 | import javax.sql.DataSource; 22 | 23 | /** 24 | * 数据源配置 25 | * 26 | * @author Guoqing 27 | */ 28 | @Configuration 29 | public class DatasourceConfig { 30 | 31 | /** 32 | * Write data source druid data source. 33 | * 34 | * @return the druid data source 35 | */ 36 | @Primary 37 | @Bean(name = "writeDataSource") 38 | @ConfigurationProperties("spring.datasource.write") 39 | public DruidDataSource writeDataSource() { 40 | return new DruidDataSource(); 41 | } 42 | 43 | /** 44 | * Read data source druid data source. 45 | * 46 | * @return the druid data source 47 | */ 48 | @Bean(name = "readDataSource") 49 | @ConfigurationProperties("spring.datasource.read") 50 | public DruidDataSource readDataSource() { 51 | return new DruidDataSource(); 52 | } 53 | 54 | /** 55 | * Dynamic data source data source. 56 | * 57 | * @return the data source 58 | */ 59 | @Bean(name = "dynamicDataSource") 60 | public DataSource dynamicDataSource() { 61 | DynamicDataSource dynamicDataSource = new DynamicDataSource(); 62 | dynamicDataSource.setWriteDataSource(writeDataSource()); 63 | dynamicDataSource.setReadDataSource(readDataSource()); 64 | 65 | return dynamicDataSource; 66 | } 67 | 68 | /** 69 | * Dynamic transaction manager data source transaction manager. 70 | * 71 | * @param dynamicDataSource the dynamic data source 72 | * @return the data source transaction manager 73 | */ 74 | @Bean(name = "dynamicTransactionManager") 75 | public DataSourceTransactionManager dynamicTransactionManager(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) { 76 | return new DynamicDataSourceTransactionManager(dynamicDataSource); 77 | } 78 | 79 | /** 80 | * Dynamic sql session factory sql session factory. 81 | * 82 | * @param dynamicDataSource the dynamic data source 83 | * @param properties the properties 84 | * @return the sql session factory 85 | */ 86 | @Bean 87 | @ConfigurationProperties(prefix = MybatisProperties.MYBATIS_PREFIX) 88 | public SqlSessionFactory dynamicSqlSessionFactory( 89 | @Qualifier("dynamicDataSource") DataSource dynamicDataSource, 90 | MybatisProperties properties) { 91 | final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean(); 92 | sessionFactory.setDataSource(dynamicDataSource); 93 | sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(properties.getConfigLocation())); 94 | sessionFactory.setMapperLocations(properties.resolveMapperLocations()); 95 | try { 96 | return sessionFactory.getObject(); 97 | } catch (Exception e) { 98 | throw new WebException(WebExceptionEnum.SERVER_ERROR); 99 | } 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 分析,在做秒杀系统的设计之初,一直在思考如何去设计这个秒杀系统,使之在现有的技术基础和认知范围内,能够做到最好;同时也能充分的利用公司现有的中间件来完成系统的实现。 2 | 3 | 我们都知道,正常去实现一个WEB端的秒杀系统,前端的处理和后端的处理一样重要;前端一般会做CDN,后端一般会做分布式部署,限流,性能优化等等一系列的操作,并完成一些网络的优化,比如IDC多线路(电信、联通、移动)的接入,带宽的升级等等。而由于目前系统前端是基于微信小程序,所以关于前端部分的优化就尽可能都是在代码中完成,CDN这一步就可以免了; 4 | 5 | ## 关于秒杀的更多思考,在原有的秒杀架构的基础上新增了新的实现方案 6 | #### 原有方案: 7 | 通过分布式锁的方式控制最终库存不超卖,并控制最终能够进入到下单环节的订单,入到队列中慢慢去消费下单 8 | #### 新增方案“ 9 | 请求进来之后,通过活动开始判断和重复秒杀判断之后,即进入到消息队列,然后在消息的消费端去做库存判断等操作,通过消息队列达到削峰的操作 10 | 11 | 其实,我觉得两种方案都是可以的,只是具体用在什么样的场景;原有方案更适合流量相对较小的平台,而且整个流程也会更加简单;而新增方案则是许多超大型平台采用的方案,通过消息队列达到削峰的目的;而这两种方案都加了真实能进入的请求限制,通过redis的原子自增来记录请求数,当请求量达到库存的n倍时,后面再进入的请求,则直接返回活动太火爆的提示; 12 | 13 | 1、架构介绍 14 | 后端项目是基于SpringCloud+SpringBoot搭建的微服务框架架构 15 | 16 | 前端在微信小程序商城上 17 | 18 | ### 核心支撑组件 19 | - 服务网关 Zuul 20 | - 服务注册发现 Eureka+Ribbon 21 | - 认证授权中心 Spring Security OAuth2、JWTToken 22 | - 服务框架 Spring MVC/Boot 23 | - 服务容错 Hystrix 24 | - 分布式锁 Redis 25 | - 服务调用 Feign 26 | - 消息队列 Kafka 27 | - 文件服务 私有云盘 28 | - 富文本组件 UEditor 29 | - 定时任务 xxl-job 30 | - 配置中心 apollo 31 | 32 | 2、关于秒杀的场景特点分析 33 | #### 秒杀系统的场景特点 34 | - 秒杀时大量用户会在同一时间同时进行抢购,网站瞬时访问流量激增; 35 | - 秒杀一般是访问请求量远远大于库存数量,只有少部分用户能够秒杀成功; 36 | - 秒杀业务流程比较简单,一般就是下订单操作; 37 | 38 | 39 | 40 | #### 秒杀架构设计理念 41 | - 限流:鉴于只有少部分用户能够秒杀成功,所以要限制大部分流量,只允许少部分流量进入服务后端(暂未处理); 42 | - 削峰:对于秒杀系统瞬时的大量用户涌入,所以在抢购开始会有很高的瞬时峰值。实现削峰的常用方法有利用缓存或者消息中间件等技术; 43 | - 异步处理:对于高并发系统,采用异步处理模式可以极大地提高系统并发量,异步处理就是削峰的一种实现方式; 44 | - 内存缓存:秒杀系统最大的瓶颈最终都可能会是数据库的读写,主要体现在的磁盘的I/O,性能会很低,如果能把大部分的业务逻辑都搬到缓存来处理,效率会有极大的提升; 45 | - 可拓展:如果需要支持更多的用户或者更大的并发,将系统设计为弹性可拓展的,如果流量来了,拓展机器就好; 46 | ![](https://images.gitbook.cn/000e5870-f837-11e8-93b4-2733009a7ae5) 47 | ![](https://images.gitbook.cn/086b0040-f837-11e8-93b4-2733009a7ae5) 48 | 49 | 50 | #### 秒杀设计思路 51 | - 由于前端是属于小程序端,所以不存在前端部分的访问压力,所以前端的访问压力就无从谈起; 52 | - 1、秒杀相关的活动页面相关的接口,所有查询能加缓存的,全部添加redis的缓存; 53 | - 2、活动相关真实库存、锁定库存、限购、下单处理状态等全放redis; 54 | - 3、当有请求进来时,首先通过redis原子自增的方式记录当前请求数,当请求超过一定量,比如说库存的10倍之后,后面进入的请求则直接返回活动太火爆的响应;而能进入抢购的请求,则首先进入活动ID为粒度的分布式锁,第一步进行用户购买的重复性校验,满足条件进入下一步,否则返回已下单的提示; 55 | - 4、第二步,判断当前可锁定的库存是否大于购买的数量,满足条件进入下一步,否则返回已售罄的提示; 56 | - 5、第三步,锁定当前请求的购买库存,从锁定库存中减除,并将下单的请求放入kafka消息队列; 57 | - 6、第四步,在redis中标记一个polling的key(用于轮询的请求接口判断用户是否下订单成功),在kafka消费端消费完成创建订单之后需要删除该key,并且维护一个活动id+用户id的key,防止重复购买; 58 | - 7、第五步,消息队列消费,创建订单,创建订单成功则扣减redis中的真实库存,并且删除polling的key。如果下单过程出现异常,则删除限购的key,返还锁定库存,提示用户下单失败; 59 | - 8、第六步,提供一个轮询接口,给前端在完成抢购动作后,检查最终下订单操作是否成功,主要判断依据是redis中的polling的key的状态; 60 | - 9、整个流程会将所有到后端的请求拦截的在redis的缓存层面,除了最终能下订单的库存限制订单会与数据库存在交互外,基本上无其他的交互,将数据库I/O压力降到了最低; 61 | 62 | 63 | 64 | #### 关于限流 65 | 66 | SpringCloud zuul的层面有很好的限流策略,可以防止同一用户的恶意请求行为 67 | ``` 68 | 1 zuul: 69 | 2 ratelimit: 70 | 3 key-prefix: your-prefix #对应用来标识请求的key的前缀 71 | 4 enabled: true 72 | 5 repository: REDIS #对应存储类型(用来存储统计信息) 73 | 6 behind-proxy: true #代理之后 74 | 7 default-policy: #可选 - 针对所有的路由配置的策略,除非特别配置了policies 75 | 8 limit: 10 #可选 - 每个刷新时间窗口对应的请求数量限制 76 | 9 quota: 1000 #可选- 每个刷新时间窗口对应的请求时间限制(秒) 77 | 10 refresh-interval: 60 # 刷新时间窗口的时间,默认值 (秒) 78 | 11 type: #可选 限流方式 79 | 12 - user 80 | 13 - origin 81 | 14 - url 82 | 15 policies: 83 | 16 myServiceId: #特定的路由 84 | 17 limit: 10 #可选- 每个刷新时间窗口对应的请求数量限制 85 | 18 quota: 1000 #可选- 每个刷新时间窗口对应的请求时间限制(秒) 86 | 19 refresh-interval: 60 # 刷新时间窗口的时间,默认值 (秒) 87 | 20 type: #可选 限流方式 88 | 21 - user 89 | 22 - origin 90 | 23 - url 91 | ``` 92 | 93 | #### 关于负载与分流 94 | 95 | 当一个活动的访问量级特别大的时候,可能从域名分发进来的nginx就算是做了高可用,但实际上最终还是单机在线,始终敌不过超大流量的压力时,我们可以考虑域名的多IP映射。也就是说同一个域名下面映射多个外网的IP,再映射到DMZ的多组高可用的nginx服务上,nginx再配置可用的应用服务集群来减缓压力; 96 | 97 | 这里也顺带介绍redis可以采用redis cluster的分布式实现方案,同时springcloud hystrix 也能有服务容错的效果; 98 | 99 | 而关于nginx、springboot的tomcat、zuul等一系列参数优化操作对于性能的访问提升也是至关重要; 100 | 101 | 补充说明一点,即使前端是基于小程序实现,但是活动相关的图片资源都放在自己的云盘服务上,所以活动前活动相关的图片资源上传CDN也是至关重要,否则哪怕是你IDC有1G的流量带宽,也会分分钟被吃完; 102 | -------------------------------------------------------------------------------- /src/main/java/cn/com/bluemoon/redis/repository/RedissonAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | package cn.com.bluemoon.redis.repository; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | import org.redisson.Redisson; 5 | import org.redisson.api.RedissonClient; 6 | import org.redisson.config.ClusterServersConfig; 7 | import org.redisson.config.Config; 8 | import org.redisson.config.SentinelServersConfig; 9 | import org.redisson.config.SingleServerConfig; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 12 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 13 | import org.springframework.context.annotation.Bean; 14 | import org.springframework.context.annotation.Configuration; 15 | 16 | /** 17 | * redisson装配各种模式 18 | * @author Guoqing.Lee 19 | * @date 2019年1月23日 下午3:14:07 20 | * 21 | */ 22 | @Configuration 23 | @ConditionalOnClass(Config.class) 24 | public class RedissonAutoConfiguration { 25 | 26 | @Autowired 27 | private RedissonProperties redssionProperties; 28 | 29 | /** 30 | * 哨兵模式自动装配 31 | * @return 32 | */ 33 | @Bean 34 | @ConditionalOnProperty(name="spring.redis.master-name") 35 | RedissonClient redissonSentinel() { 36 | String[] nodes = redssionProperties.getSentinelAddresses(); 37 | for(int i=0;i 2 | 3 | 4 | distributed-seckill 5 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | log/distributed-seckill-info.log 14 | 15 | 16 | 17 | log/distributed-seckill-info-%d{yyyy-MM-dd}.%i.log 18 | 19 | 128MB 20 | 21 | 22 | 30 23 | 24 | true 25 | 26 | ${CONSOLE_LOG_PATTERN} 27 | utf-8 28 | 29 | 30 | info 31 | ACCEPT 32 | DENY 33 | 34 | 35 | 36 | 37 | log/distributed-seckill-error.log 38 | 39 | 40 | 41 | log/distributed-seckill-error-%d{yyyy-MM-dd}.%i.log 42 | 43 | 2MB 44 | 45 | 46 | 180 47 | 48 | true 49 | 50 | 51 | ${CONSOLE_LOG_PATTERN} 52 | utf-8 53 | 54 | 55 | 56 | 57 | ERROR 58 | 59 | ACCEPT 60 | 61 | DENY 62 | 63 | 64 | 65 | 66 | 67 | 68 | ${CONSOLE_LOG_PATTERN} 69 | utf-8 70 | 71 | 72 | 73 | INFO 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /src/main/java/cn/com/bluemoon/utils/SerialNo.java: -------------------------------------------------------------------------------- 1 | package cn.com.bluemoon.utils; 2 | 3 | import java.text.NumberFormat; 4 | import java.text.SimpleDateFormat; 5 | 6 | /** 7 | * @author Administrator 8 | * 9 | * 取序列号类 10 | */ 11 | public class SerialNo { 12 | private static long sequence; 13 | private static String compareTime; 14 | private static NumberFormat numberFormat; 15 | 16 | static { 17 | numberFormat = NumberFormat.getInstance(); 18 | numberFormat.setGroupingUsed(false); 19 | numberFormat.setMinimumIntegerDigits(5); 20 | numberFormat.setMaximumIntegerDigits(5); 21 | } 22 | 23 | /** 24 | * 生成唯一序列号 25 | *

26 | * 根据当前时间加五位序号,一共20位 27 | * 28 | * @return 序列号 29 | */ 30 | public static synchronized String getUNID() { 31 | // System.out.println(sequence); 32 | String currentTime = DateUtil.getCurrentDateString("yyMMddHHmmssSSS"); 33 | if (compareTime == null || compareTime.compareTo(currentTime) != 0) { 34 | compareTime = currentTime; 35 | sequence = 1; 36 | } else 37 | sequence++; 38 | // System.out.println(sequence); 39 | int i = (int) (Math.random() * 9000 + 1000); 40 | // System.out.println(numberFormat.format(sequence)); 41 | // System.out.println(currentTime + i+sequence); 42 | return currentTime + i + sequence; 43 | } 44 | 45 | /** 46 | * 生成唯一序列号 47 | *

48 | * 根据当前时间生成,用于非批量数据记录生成时,一共15位(如果存在批量插入时,可能出现重复) 49 | * 50 | * @return 序列号 51 | */ 52 | public static String getSerialforDB() { 53 | return DateUtil.getCurrentDateString("yyMMddHHmmssSSS"); 54 | } 55 | 56 | /** 57 | * 生成短序列号 58 | *

59 | * 根据当前时间生成,用于少量数据记录时。(可能出现重复,一般用于记录较少且变动不频繁的静态表的记录生成) 60 | * 61 | * @return 序列号 62 | */ 63 | public static String getShortSerial() { 64 | return DateUtil.getCurrentDateString("mmssSSS"); 65 | } 66 | 67 | /** 68 | * @return 形如 yyyyMMddHHmmssSSS-Z0000019558195832297 的(38位)保证唯一的递增的序列号字符串, 69 | * 主要用于数据库的主键,方便基于时间点的跨数据库的异步数据同步。 70 | * 前半部分是currentTimeMillis,后半部分是nanoTime(正数)补齐20位的字符串, 71 | * 如果通过System.nanoTime()获取的是负数,则通过nanoTime = 72 | * nanoTime+Long.MAX_VALUE+1; 转化为正数或零。 73 | */ 74 | public static String getTimeMillisSequence() { 75 | long nanoTime = System.nanoTime(); 76 | String preFix = ""; 77 | if (nanoTime < 0) { 78 | preFix = "A";// 负数补位A保证负数排在正数Z前面,解决正负临界值(如A9223372036854775807至Z0000000000000000000)问题。 79 | nanoTime = nanoTime + Long.MAX_VALUE + 1; 80 | } else { 81 | preFix = "Z"; 82 | } 83 | String nanoTimeStr = String.valueOf(nanoTime); 84 | 85 | int difBit = String.valueOf(Long.MAX_VALUE).length() - nanoTimeStr.length(); 86 | for (int i = 0; i < difBit; i++) { 87 | preFix = preFix + "0"; 88 | } 89 | nanoTimeStr = preFix + nanoTimeStr; 90 | SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS"); // 24小时制 91 | // String 92 | // timeMillisSequence=sdf.format(System.currentTimeMillis())+"-"+nanoTimeStr; 93 | return nanoTimeStr; 94 | } 95 | 96 | public static void main(String[] args) { 97 | // System.out.println(getTimeMillisSequence()); 98 | String s1 = "1111"; 99 | int count = 0; 100 | long stat = System.currentTimeMillis(); 101 | for (int i = 0; i < 100000; i++) { 102 | String s2 = getUNID(); 103 | System.out.println(s2); 104 | // if(s1.equals(s2)){ 105 | // count++; 106 | // } 107 | // s1=s2; 108 | } 109 | System.out.println(System.currentTimeMillis() - stat); 110 | System.out.println(count); 111 | // System.out.println(getUNID()); 112 | // System.out.println(getShortSerial()); 113 | // UUID uuid = UUID.randomUUID(); 114 | // System.out.println(uuid.toString()); 115 | } 116 | 117 | public static synchronized String getUNID18() { 118 | // System.out.println(sequence); 119 | String currentTime = DateUtil.getCurrentDateString("yyMMddHHmmssSSS"); 120 | if (compareTime == null || compareTime.compareTo(currentTime) != 0) { 121 | compareTime = currentTime; 122 | sequence = 1; 123 | } else 124 | sequence++; 125 | // System.out.println(sequence); 126 | int i = (int) (Math.random() * 90 + 10); 127 | // System.out.println(numberFormat.format(sequence)); 128 | // System.out.println(currentTime + i+sequence); 129 | return currentTime + i + sequence; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/main/java/cn/com/bluemoon/mybatis/datasource/DynamicPlugin.java: -------------------------------------------------------------------------------- 1 | package cn.com.bluemoon.mybatis.datasource; 2 | 3 | import org.apache.ibatis.executor.Executor; 4 | import org.apache.ibatis.executor.keygen.SelectKeyGenerator; 5 | import org.apache.ibatis.mapping.BoundSql; 6 | import org.apache.ibatis.mapping.MappedStatement; 7 | import org.apache.ibatis.mapping.SqlCommandType; 8 | import org.apache.ibatis.plugin.Interceptor; 9 | import org.apache.ibatis.plugin.Intercepts; 10 | import org.apache.ibatis.plugin.Invocation; 11 | import org.apache.ibatis.plugin.Plugin; 12 | import org.apache.ibatis.plugin.Signature; 13 | import org.apache.ibatis.session.ResultHandler; 14 | import org.apache.ibatis.session.RowBounds; 15 | import org.slf4j.Logger; 16 | import org.slf4j.LoggerFactory; 17 | import org.springframework.transaction.support.TransactionSynchronizationManager; 18 | 19 | import java.util.Locale; 20 | import java.util.Map; 21 | import java.util.Properties; 22 | import java.util.concurrent.ConcurrentHashMap; 23 | 24 | /** 25 | * 动态数据源Mybatis拦截器插件 26 | * 27 | * @author mij 28 | */ 29 | @Intercepts({ 30 | @Signature(type = Executor.class, method = "update", args = { 31 | MappedStatement.class, Object.class}), 32 | @Signature(type = Executor.class, method = "query", args = { 33 | MappedStatement.class, Object.class, RowBounds.class, 34 | ResultHandler.class})}) 35 | public class DynamicPlugin implements Interceptor { 36 | 37 | /** 38 | * Logger 39 | */ 40 | protected static final Logger LOGGER = LoggerFactory.getLogger(DynamicPlugin.class); 41 | 42 | /** 43 | * 拦截SQL表达式 44 | */ 45 | private static final String REGEX = ".*insert\\u0020.*|.*delete\\u0020.*|.*update\\u0020.*"; 46 | 47 | /** 48 | * 动态数据源缓存 49 | */ 50 | private static final Map CACHE_MAP = new ConcurrentHashMap<>(); 51 | 52 | @Override 53 | public Object intercept(Invocation invocation) throws Throwable { 54 | 55 | boolean synchronizationActive = TransactionSynchronizationManager.isSynchronizationActive(); 56 | //如果没有事务 57 | if (!synchronizationActive) { 58 | Object[] objects = invocation.getArgs(); 59 | MappedStatement ms = (MappedStatement) objects[0]; 60 | 61 | DynamicDataSourceGlobal dynamicDataSourceGlobal; 62 | 63 | if ((dynamicDataSourceGlobal = CACHE_MAP.get(ms.getId())) == null) { 64 | dynamicDataSourceGlobal = getDynamicDataSource(ms, objects[1]); 65 | LOGGER.warn("设置方法[{}] use [{}] Strategy, SqlCommandType [{}]..", ms.getId(), dynamicDataSourceGlobal.name(), ms.getSqlCommandType().name()); 66 | CACHE_MAP.put(ms.getId(), dynamicDataSourceGlobal); 67 | } 68 | DynamicDataSourceHolder.putDataSource(dynamicDataSourceGlobal); 69 | } 70 | 71 | return invocation.proceed(); 72 | } 73 | 74 | /** 75 | * 获得动态数据源 76 | * 77 | * @param ms MappedStatement 78 | * @param parameterObject Parameter 79 | * @return DynamicDataSourceGlobal 80 | */ 81 | private DynamicDataSourceGlobal getDynamicDataSource(MappedStatement ms, Object parameterObject) { 82 | DynamicDataSourceGlobal dynamicDataSourceGlobal; 83 | //读方法 84 | if (ms.getSqlCommandType().equals(SqlCommandType.SELECT)) { 85 | //!selectKey 为自增id查询主键(SELECT LAST_INSERT_ID() )方法,使用主库 86 | if (ms.getId().contains(SelectKeyGenerator.SELECT_KEY_SUFFIX)) { 87 | dynamicDataSourceGlobal = DynamicDataSourceGlobal.WRITE; 88 | } else { 89 | BoundSql boundSql = ms.getSqlSource().getBoundSql(parameterObject); 90 | String sql = boundSql.getSql().toLowerCase(Locale.CHINA).replaceAll("[\\t\\n\\r]", " "); 91 | if (sql.matches(REGEX)) { 92 | dynamicDataSourceGlobal = DynamicDataSourceGlobal.WRITE; 93 | } else { 94 | dynamicDataSourceGlobal = DynamicDataSourceGlobal.READ; 95 | } 96 | } 97 | } else { 98 | dynamicDataSourceGlobal = DynamicDataSourceGlobal.WRITE; 99 | } 100 | return dynamicDataSourceGlobal; 101 | } 102 | 103 | @Override 104 | public Object plugin(Object target) { 105 | if (target instanceof Executor) { 106 | return Plugin.wrap(target, this); 107 | } else { 108 | return target; 109 | } 110 | } 111 | 112 | @Override 113 | public void setProperties(Properties properties) { 114 | // 115 | } 116 | } -------------------------------------------------------------------------------- /src/main/java/cn/com/bluemoon/redis/lock/DistributedExclusiveRedisLock.java: -------------------------------------------------------------------------------- 1 | package cn.com.bluemoon.redis.lock; 2 | 3 | import org.springframework.dao.DataAccessException; 4 | import org.springframework.data.redis.connection.RedisConnection; 5 | import org.springframework.data.redis.core.RedisCallback; 6 | import org.springframework.data.redis.core.RedisTemplate; 7 | 8 | import cn.com.bluemoon.common.exception.IllegalReentrantException; 9 | import redis.clients.jedis.Jedis; 10 | 11 | import java.io.Serializable; 12 | import java.util.Collections; 13 | import java.util.UUID; 14 | import java.util.concurrent.TimeUnit; 15 | import java.util.concurrent.locks.Condition; 16 | import java.util.concurrent.locks.Lock; 17 | 18 | /** 19 | * 不可重入分布式锁,基于redis实现 20 | *

21 | * Created by Guoqing on 18/11/22. 22 | * 变更了加锁与释放锁的过程,通过jedis的操作实现,防止出现死锁等问题 23 | */ 24 | public class DistributedExclusiveRedisLock implements Lock, Serializable { 25 | private static final long serialVersionUID = -7118885188373628439L; 26 | 27 | private RedisTemplate redisTemplate; 28 | 29 | private Jedis jedis; 30 | 31 | /** 32 | * 控制锁颗粒度的参数 33 | *

34 | * 不建议使用全局锁,具体应用中推荐指定对应的Key,把锁的颗粒度减小,利于性能 35 | */ 36 | private String lockKey = "distributed_global_lock"; 37 | 38 | private static final String LOCK_SUCCESS = "OK"; 39 | private static final String SET_IF_NOT_EXIST = "NX"; 40 | private static final String SET_WITH_EXPIRE_TIME = "EX"; 41 | private static final Long RELEASE_SUCCESS = 1L; 42 | 43 | private String uuid; 44 | 45 | private boolean isOccupy; 46 | 47 | // 单位 默认10秒 48 | private long expires = 30L; 49 | 50 | public DistributedExclusiveRedisLock(RedisTemplate template, Jedis jedis) { 51 | this.redisTemplate = template; 52 | this.jedis = jedis; 53 | } 54 | 55 | public DistributedExclusiveRedisLock(RedisTemplate template, String lockKey, Jedis jedis) { 56 | this.lockKey = lockKey; 57 | this.redisTemplate = template; 58 | this.jedis = jedis; 59 | } 60 | 61 | /** 62 | * 获取锁方法,获取不到会被阻塞 63 | */ 64 | @SuppressWarnings("unchecked") 65 | @Override 66 | public void lock() { 67 | if (isOccupy) { 68 | throw new IllegalReentrantException("锁不可重入,请检查代码"); 69 | } 70 | isOccupy = true; 71 | uuid = UUID.randomUUID().toString(); 72 | boolean isAcquired; 73 | for (; ; ) { 74 | isAcquired = (Boolean) redisTemplate.execute(new RedisCallback() { 75 | @Override 76 | public Boolean doInRedis(RedisConnection connection) throws DataAccessException { 77 | /*return connection.setNX(lockKey.getBytes(), uuid.getBytes()) 78 | && connection.expire(lockKey.getBytes(), expires);*/ 79 | /** 80 | * 更新了1.0版本中,setnx成功之后程序崩溃导致的死锁的问题 81 | */ 82 | String result = jedis.set(lockKey, uuid, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expires); 83 | if (LOCK_SUCCESS.equals(result)) { 84 | return true; 85 | } 86 | return false; 87 | } 88 | }); 89 | if (isAcquired) 90 | return; 91 | } 92 | } 93 | 94 | @Override 95 | public void lockInterruptibly() throws InterruptedException { 96 | 97 | } 98 | 99 | @Override 100 | public boolean tryLock() { 101 | return false; 102 | } 103 | 104 | /** 105 | * 尝试获取锁,支持超时中断 (暂未实现) 106 | * 107 | * @param time 108 | * @param unit 109 | * @return 110 | * @throws InterruptedException 111 | */ 112 | @Override 113 | public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { 114 | return false; 115 | } 116 | 117 | /** 118 | * 解锁并释放资源 119 | *

120 | * 超时后的资源被释放掉,避免误删,这里务必校验uuid 121 | */ 122 | @Override 123 | public void unlock() { 124 | if (!isOccupy) 125 | return; 126 | String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; 127 | Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(uuid)); 128 | 129 | if (RELEASE_SUCCESS.equals(result)) { 130 | isOccupy = false; 131 | } 132 | } 133 | 134 | @Override 135 | public Condition newCondition() { 136 | return null; 137 | } 138 | 139 | public long getExpires() { 140 | return expires; 141 | } 142 | 143 | public void setExpires(long expires) { 144 | this.expires = expires; 145 | } 146 | 147 | public String getLockKey() { 148 | return lockKey; 149 | } 150 | 151 | public void setLockKey(String lockKey) { 152 | this.lockKey = lockKey; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/main/java/cn/com/bluemoon/kafka/KafkaConsumer.java: -------------------------------------------------------------------------------- 1 | package cn.com.bluemoon.kafka; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.kafka.annotation.KafkaListener; 7 | import org.springframework.stereotype.Component; 8 | 9 | import com.alibaba.fastjson.JSONObject; 10 | 11 | import cn.com.bluemoon.common.response.SeckillInfoResponse; 12 | import cn.com.bluemoon.redis.lock.RedissonDistributedLocker; 13 | import cn.com.bluemoon.redis.repository.RedisRepository; 14 | import cn.com.bluemoon.utils.SerialNo; 15 | 16 | /** 17 | * 消费者 spring-kafka 2.0 + 依赖JDK8 18 | * @author Guoqing 19 | */ 20 | @Component 21 | public class KafkaConsumer { 22 | 23 | private Logger logger = LoggerFactory.getLogger(KafkaConsumer.class); 24 | @Autowired 25 | private RedisRepository redisRepository; 26 | @Autowired 27 | private RedissonDistributedLocker redissonDistributedLocker; 28 | 29 | /** 30 | * 监听seckill主题,有消息就读取 31 | * 主要消费秒杀进入到下订单操作的队列数据,此处的数据已经过滤了绝大部分请求,只有真正得到下单机会的用户才会进入到这一环节 32 | * @param message 33 | */ 34 | @KafkaListener(topics = {"demo_seckill"}) 35 | public void receiveMessage(String message){ 36 | try { 37 | //收到通道的消息之后执行秒杀操作 38 | logger.info(message); 39 | JSONObject json = JSONObject.parseObject(message); 40 | long stallActivityId = json.getLong("stallActivityId"); 41 | int purchaseNum = json.getInteger("purchaseNum"); 42 | String openId = json.getString("openId"); 43 | // long addressId = json.getLong("addressId"); 44 | // String formId = json.getString("formId"); 45 | // String shareCode = json.getString("shareCode"); 46 | // String shareSource = json.getString("shareSource"); 47 | // String userCode = json.getString("userCode"); 48 | //生成订单,模拟生成订单编码 49 | String orderCode = "J"+SerialNo.getUNID(); 50 | //删除redis中的key,让轮询接口发现,该订单已经处理完成 51 | redisRepository.del("BM_MARKET_LOCK_POLLING_" + stallActivityId + "_" + openId); 52 | //并将orderId_orderCode放入缓存,有效时间10分钟(因为支付有效时间为10分钟) 53 | redisRepository.setExpire("BM_MARKET_SECKILL_ORDERID_" + stallActivityId + "_" + openId, orderCode, 600); 54 | String lockKey = "marketOrder"+stallActivityId; //控制锁的颗粒度(摊位活动ID) 55 | boolean isGetLock = redissonDistributedLocker.tryLock(lockKey, 1L, 1L); //最多等待1S,每次操作预计的超时时间1S 56 | if(isGetLock) { 57 | try { 58 | //扣减真实库存 59 | redisRepository.decrBy("BM_MARKET_SECKILL_REAL_STOCKNUM_" + stallActivityId, purchaseNum); 60 | }finally{ 61 | redissonDistributedLocker.unlock(lockKey); 62 | } 63 | } 64 | } catch (NumberFormatException e) { 65 | e.printStackTrace(); 66 | } 67 | } 68 | 69 | /** 70 | * 与上述方法不同,该方法还包含库存校验等逻辑操作 71 | */ 72 | @KafkaListener(topics = {"demo_seckill_queue"}) 73 | public void receiveMessage2(String message) { 74 | JSONObject json = JSONObject.parseObject(message); 75 | long stallActivityId = json.getLong("stallActivityId"); 76 | int purchaseNum = json.getInteger("purchaseNum"); 77 | String openId = json.getString("openId"); 78 | // long addressId = json.getLong("addressId"); 79 | // String formId = json.getString("formId"); 80 | // String shareCode = json.getString("shareCode"); 81 | // String shareSource = json.getString("shareSource"); 82 | // String userCode = json.getString("userCode"); 83 | String lockKey = "marketOrder"+stallActivityId;//控制锁的颗粒度(摊位活动ID) 84 | redissonDistributedLocker.lock(lockKey, 1L); 85 | try{ 86 | JSONObject result = new JSONObject(); 87 | SeckillInfoResponse response = new SeckillInfoResponse(); 88 | String redisStock = redisRepository.get("BM_MARKET_SECKILL_STOCKNUM_" + stallActivityId); 89 | int surplusStock = Integer.parseInt(redisStock == null ? "0" : redisStock); //剩余库存 90 | //如果剩余库存大于购买数量,则获得下单资格,并生成唯一下单资格码 91 | if( surplusStock >= purchaseNum ) { 92 | response.setIsSuccess(true); 93 | response.setResponseCode(0); 94 | response.setResponseMsg("您已获得下单资格,请尽快下单"); 95 | response.setRefreshTime(0); 96 | String code = SerialNo.getUNID(); 97 | response.setOrderQualificationCode(code); 98 | //将下单资格码维护到redis中,用于下单时候的检验;有效时间10分钟; 99 | redisRepository.setExpire("BM_MARKET_SECKILL_QUALIFICATION_CODE_" + stallActivityId + "_" + openId, code, 10*60); 100 | //维护一个key,防止获得下单资格用户重新抢购,当支付过期之后应该维护删除该标志 101 | redisRepository.setExpire("BM_MARKET_SECKILL_LIMIT_" + stallActivityId + "_" + openId, "true", 3600*24*7); 102 | //扣减锁定库存 103 | redisRepository.decrBy("BM_MARKET_SECKILL_STOCKNUM_" + stallActivityId, purchaseNum); 104 | }else { 105 | response.setIsSuccess(false); 106 | response.setResponseCode(6102); 107 | response.setResponseMsg("秒杀失败,商品已经售罄"); 108 | response.setRefreshTime(0); 109 | } 110 | result.put("response", response); 111 | //将信息维护到redis中 112 | redisRepository.setExpire("BM_MARKET_SECKILL_QUEUE_"+stallActivityId+"_"+openId, result.toJSONString(), 3600*24*7); 113 | }finally{ 114 | redissonDistributedLocker.unlock(lockKey); 115 | } 116 | } 117 | 118 | } -------------------------------------------------------------------------------- /src/main/java/cn/com/bluemoon/redis/lock/RedissonDistributedLocker.java: -------------------------------------------------------------------------------- 1 | package cn.com.bluemoon.redis.lock; 2 | 3 | import java.util.concurrent.TimeUnit; 4 | 5 | import org.redisson.api.RLock; 6 | import org.redisson.api.RedissonClient; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.stereotype.Component; 9 | 10 | /** 11 | * 基于Redisson的分布式锁实现 12 | * @author Guoqing.Lee 13 | * @date 2019年1月23日 下午4:04:57 14 | * 15 | */ 16 | @Component 17 | public class RedissonDistributedLocker { 18 | 19 | @Autowired 20 | private RedissonClient redissonClient; 21 | 22 | 23 | /** 24 | * 加锁 25 | * 26 | * 假如线程A和线程B使用同一个锁LOCK,此时线程A首先获取到锁LOCK.lock(),并且始终持有不释放。如果此时B要去获取锁 27 | * 在等待获取锁的过程中休眠并禁止一切线程调度,直到获取到锁; 28 | * @param lockKey 29 | * @return 30 | */ 31 | public RLock lock(String lockKey) { 32 | RLock lock = redissonClient.getLock(lockKey); 33 | lock.lock(); 34 | return lock; 35 | } 36 | 37 | /** 38 | * 加锁,过期自动释放 39 | * 40 | * 假如线程A和线程B使用同一个锁LOCK,此时线程A首先获取到锁LOCK.lock(),并且始终持有不释放。如果此时B要去获取锁 41 | * 在等待获取锁的过程中休眠并禁止一切线程调度,直到获取到锁; 42 | * 如果已经获取到锁,则一直会持有锁直到调用unlock方法,或者直到leaseTime的时间到了 43 | * @param lockKey 44 | * @param leaseTime 自动释放锁时间 45 | * @return 46 | */ 47 | public RLock lock(String lockKey, long leaseTime) { 48 | RLock lock = redissonClient.getLock(lockKey); 49 | lock.lock(leaseTime, TimeUnit.SECONDS); 50 | return lock; 51 | } 52 | 53 | /** 54 | * 加锁,过期自动释放,时间单位传入 55 | * 56 | * 假如线程A和线程B使用同一个锁LOCK,此时线程A首先获取到锁LOCK.lock(),并且始终持有不释放。如果此时B要去获取锁 57 | * 在等待获取锁的过程中休眠并禁止一切线程调度,直到获取到锁; 58 | * 如果已经获取到锁,则一直会持有锁直到调用unlock方法,或者直到leaseTime的时间到了 59 | * @param lockKey 60 | * @param unit 时间单位 61 | * @param leaseTime 上锁后自动释放时间 62 | * @return 63 | */ 64 | public RLock lock(String lockKey, TimeUnit unit, long leaseTime) { 65 | RLock lock = redissonClient.getLock(lockKey); 66 | lock.lock(leaseTime, unit); 67 | return lock; 68 | } 69 | 70 | /** 71 | * 尝试获取锁 72 | * 73 | * 假如线程A和线程B使用同一个锁LOCK,此时线程A首先获取到锁LOCK.lock(),并且始终持有不释放。如果此时B要去获取锁 74 | * 该获取锁的方法不会等待,如果获取到锁则返回true,获取不到锁并直接返回false,去执行下面的; 75 | * 如果获取到锁,则会一直持有锁直到调用unlock方法,或者leaseTime时间到 76 | * 但当调用B.interrupt()会被中断等待,并抛出InterruptedException。 77 | * 78 | * @param lockKey 79 | * @param unit 时间单位 80 | * @param waitTime 最多等待时间 81 | * @param leaseTime 上锁后自动释放时间 82 | * @return 83 | */ 84 | public boolean tryLock(String lockKey, TimeUnit unit, long waitTime, long leaseTime) { 85 | RLock lock = redissonClient.getLock(lockKey); 86 | try { 87 | return lock.tryLock(waitTime, leaseTime, unit); 88 | } catch (InterruptedException e) { 89 | return false; 90 | } 91 | } 92 | 93 | /** 94 | * 尝试获取锁 95 | * 96 | * 假如线程A和线程B使用同一个锁LOCK,此时线程A首先获取到锁LOCK.lock(),并且始终持有不释放。如果此时B要去获取锁 97 | * 该获取锁的方法不会等待,如果获取到锁则返回true,获取不到锁并直接返回false,去执行下面的; 98 | * 如果获取到锁,则会一直持有锁直到调用unlock方法,或者leaseTime时间到 99 | * 但当调用B.interrupt()会被中断等待,并抛出InterruptedException。 100 | * 101 | * @param lockKey 102 | * @param waitTime 最多等待时间 103 | * @param leaseTime 上锁后自动释放锁时间 104 | * @return 105 | */ 106 | public boolean tryLock(String lockKey, long waitTime, long leaseTime) { 107 | RLock lock = redissonClient.getLock(lockKey); 108 | try { 109 | return lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS); 110 | } catch (InterruptedException e) { 111 | return false; 112 | } 113 | } 114 | 115 | /** 116 | * 释放锁 117 | * @param lockKey 118 | */ 119 | public void unlock(String lockKey) { 120 | RLock lock = redissonClient.getLock(lockKey); 121 | lock.unlock(); 122 | } 123 | 124 | /** 125 | * 释放锁 126 | * @param lock 127 | */ 128 | public void unlock(RLock lock) { 129 | lock.unlock(); 130 | } 131 | 132 | /** 133 | * 检查此锁是否被任何线程锁定 134 | * @param lockKey 135 | * @return 136 | */ 137 | public boolean isLocked(String lockKey) { 138 | RLock lock = redissonClient.getLock(lockKey); 139 | return lock.isLocked(); 140 | } 141 | 142 | /** 143 | * 获取锁,可被中断 144 | * 145 | * 假如线程A和线程B使用同一个锁LOCK,此时线程A首先获取到锁LOCK.lock(),并且始终持有不释放。如果此时B要去获取锁 146 | * 此方式会等待,但当调用B.interrupt()会被中断等待,并抛出InterruptedException异常,否则会与lock()一样始终处于等待中,直到线程A释放锁。 147 | * @param lockKey 148 | * @param leaseTime 149 | * @param unit 150 | * @return 151 | * @throws InterruptedException 152 | */ 153 | public RLock lockInterruptibly(String lockKey, long leaseTime, TimeUnit unit) throws InterruptedException { 154 | RLock lock = redissonClient.getLock(lockKey); 155 | lock.lockInterruptibly(leaseTime, unit); 156 | return lock; 157 | } 158 | 159 | /** 160 | * 获取锁,可被中断 161 | * 162 | * 假如线程A和线程B使用同一个锁LOCK,此时线程A首先获取到锁LOCK.lock(),并且始终持有不释放。如果此时B要去获取锁 163 | * 此方式会等待,但当调用B.interrupt()会被中断等待,并抛出InterruptedException异常,否则会与lock()一样始终处于等待中,直到线程A释放锁。 164 | * @param lockKey 165 | * @param leaseTime 166 | * @return 167 | * @throws InterruptedException 168 | */ 169 | public RLock lockInterruptibly(String lockKey, long leaseTime) throws InterruptedException { 170 | RLock lock = redissonClient.getLock(lockKey); 171 | lock.lockInterruptibly(leaseTime, TimeUnit.SECONDS); 172 | return lock; 173 | } 174 | 175 | 176 | } 177 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | cn.com.bluemoon 6 | distributed-seckill 7 | 0.0.1-SNAPSHOT 8 | distributed-seckill 9 | 10 | 11 | org.springframework.boot 12 | spring-boot-starter-parent 13 | 1.5.6.RELEASE 14 | 15 | 16 | 17 | 18 | UTF-8 19 | 20 | 21 | 22 | 23 | com.alibaba 24 | fastjson 25 | 1.2.28 26 | 27 | 28 | 29 | 30 | io.springfox 31 | springfox-swagger2 32 | 2.7.0 33 | 34 | 35 | io.springfox 36 | springfox-swagger-ui 37 | 2.7.0 38 | 39 | 40 | 41 | 42 | org.springframework.kafka 43 | spring-kafka 44 | 1.3.5.RELEASE 45 | 46 | 47 | 48 | javax.servlet 49 | javax.servlet-api 50 | 51 | 52 | 53 | velocity 54 | org.apache.velocity 55 | 1.7 56 | 57 | 58 | 59 | commons-fileupload 60 | commons-fileupload 61 | 1.3.1 62 | 63 | 64 | commons-io 65 | commons-io 66 | 67 | 68 | 69 | 70 | 71 | org.springframework.session 72 | spring-session-data-redis 73 | 74 | 75 | org.springframework.boot 76 | spring-boot-starter-data-redis 77 | 78 | 79 | org.springframework.boot 80 | spring-boot-starter-jdbc 81 | 82 | 83 | mysql 84 | mysql-connector-java 85 | 86 | 87 | 88 | com.alibaba 89 | druid 90 | 1.0.11 91 | 92 | 93 | 94 | 95 | com.github.pagehelper 96 | pagehelper 97 | 4.1.6 98 | 99 | 100 | 101 | org.mybatis.spring.boot 102 | mybatis-spring-boot-starter 103 | 1.1.1 104 | 105 | 106 | 107 | 108 | org.springframework.boot 109 | spring-boot-starter-web 110 | 111 | 112 | org.springframework.boot 113 | spring-boot-configuration-processor 114 | true 115 | 116 | 117 | org.springframework 118 | spring-test 119 | test 120 | 121 | 122 | org.springframework.boot 123 | spring-boot-test 124 | test 125 | 126 | 127 | 128 | 129 | org.springframework.boot 130 | spring-boot-starter-aop 131 | 132 | 133 | 134 | 135 | org.redisson 136 | redisson 137 | 3.10.1 138 | 139 | 140 | 141 | org.j-im 142 | jim-server 143 | 2.3.0.v20180830-RELEASE 144 | 145 | 146 | 147 | cn.hutool 148 | hutool-all 149 | 4.5.0 150 | 151 | 152 | 153 | 154 | 155 | distributed-seckill 156 | 157 | 158 | 159 | org.springframework.boot 160 | spring-boot-maven-plugin 161 | 162 | 163 | 164 | org.springframework 165 | springloaded 166 | 1.2.7.RELEASE 167 | 168 | 169 | 170 | 171 | org.apache.maven.plugins 172 | maven-javadoc-plugin 173 | 174 | 175 | 176 | 177 | -------------------------------------------------------------------------------- /src/main/java/cn/com/bluemoon/service/impl/SeckillServiceImpl.java: -------------------------------------------------------------------------------- 1 | /** 2 | *

Title: SeckillServiceImpl.java

3 | *

Description:

4 | *

Copyright: Copyright (c) 2018

5 | *

Company: www.bluemoon.com

6 | * @author Guoqing 7 | * @date 2018年8月10日 8 | */ 9 | package cn.com.bluemoon.service.impl; 10 | 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.stereotype.Service; 15 | import org.springframework.transaction.annotation.Transactional; 16 | 17 | import com.alibaba.fastjson.JSONObject; 18 | 19 | import cn.com.bluemoon.common.response.SeckillInfoResponse; 20 | import cn.com.bluemoon.kafka.KafkaSender; 21 | import cn.com.bluemoon.redis.lock.RedissonDistributedLocker; 22 | import cn.com.bluemoon.redis.repository.RedisRepository; 23 | import cn.com.bluemoon.service.ISeckillService; 24 | 25 | /** 26 | *

Title: SeckillServiceImpl

27 | *

Description:

28 | * @author Guoqing 29 | * @date 2018年8月10日 30 | */ 31 | @Service 32 | public class SeckillServiceImpl implements ISeckillService { 33 | 34 | @Autowired 35 | private RedisRepository redisRepository; 36 | @Autowired 37 | private KafkaSender kafkaSender; 38 | @Autowired 39 | private RedissonDistributedLocker redissonDistributedLocker; 40 | 41 | private Logger logger = LoggerFactory.getLogger(SeckillServiceImpl.class); 42 | 43 | @Override 44 | @Transactional 45 | public SeckillInfoResponse startSeckill(int stallActivityId, int purchaseNum, String openId, String formId, long addressId, 46 | String shareCode, String shareSource, String userCode) { 47 | SeckillInfoResponse response = new SeckillInfoResponse(); 48 | //判断秒杀活动是否开始 49 | if( !checkStartSeckill(stallActivityId) ) { 50 | response.setIsSuccess(false); 51 | response.setResponseCode(6205); 52 | response.setResponseMsg("秒杀活动尚未开始,请稍等!"); 53 | response.setRefreshTime(0); 54 | return response; 55 | } 56 | logger.info("开始获取锁资源..."); 57 | String lockKey = "BM_MARKET_SECKILL_" + stallActivityId; 58 | try { 59 | redissonDistributedLocker.lock(lockKey, 2L); 60 | logger.info("获取到锁资源..."); 61 | //做用户重复购买校验 62 | if( redisRepository.exists("BM_MARKET_SECKILL_LIMIT_" + stallActivityId + "_" + openId) ) { 63 | logger.info("已经检测到用户重复购买..."); 64 | response.setIsSuccess(false); 65 | response.setResponseCode(6105); 66 | response.setResponseMsg("您正在参与该活动,不能重复购买"); 67 | response.setRefreshTime(0); 68 | } else { 69 | String redisStock = redisRepository.get("BM_MARKET_SECKILL_STOCKNUM_" + stallActivityId); 70 | int surplusStock = Integer.parseInt(redisStock == null ? "0" : redisStock); //剩余库存 71 | //如果剩余库存大于购买数量,则进入消费队列 72 | if( surplusStock >= purchaseNum ) { 73 | try { 74 | //锁定库存,并将请求放入消费队列 75 | redisRepository.decrBy("BM_MARKET_SECKILL_STOCKNUM_" + stallActivityId, purchaseNum); 76 | JSONObject jsonStr = new JSONObject(); 77 | jsonStr.put("stallActivityId", stallActivityId); 78 | jsonStr.put("purchaseNum", purchaseNum); 79 | jsonStr.put("openId", openId); 80 | jsonStr.put("addressId", addressId); 81 | jsonStr.put("formId", formId); 82 | jsonStr.put("shareCode", shareCode); 83 | jsonStr.put("shareSource", shareSource); 84 | jsonStr.put("userCode", userCode); 85 | //放入kafka消息队列 86 | kafkaSender.sendChannelMess("demo_seckill", jsonStr.toString()); 87 | // messageQueueService.sendMessage("bm_market_seckill", jsonStr.toString(), true); 88 | //此处还应该标记一个seckillId和openId的唯一标志来给轮询接口判断请求是否已经处理完成,需要在下单完成之后去维护删除该标志,并且创建一个新的标志,并存放orderId 89 | redisRepository.set("BM_MARKET_LOCK_POLLING_" + stallActivityId + "_" + openId, "true"); 90 | //维护一个key,防止用户在该活动重复购买,当支付过期之后应该维护删除该标志 91 | redisRepository.setExpire("BM_MARKET_SECKILL_LIMIT_" + stallActivityId + "_" + openId, "true", 3600*24*7); 92 | 93 | response.setIsSuccess(true); 94 | response.setResponseCode(6101); 95 | response.setResponseMsg("排队中,请稍后"); 96 | response.setRefreshTime(1000); 97 | } catch (Exception e) { 98 | e.printStackTrace(); 99 | response.setIsSuccess(false); 100 | response.setResponseCode(6102); 101 | response.setResponseMsg("秒杀失败,商品已经售罄"); 102 | response.setRefreshTime(0); 103 | } 104 | }else { 105 | //需要在消费端维护一个真实的库存损耗值,用来显示是否还有未完成支付的用户 106 | String redisRealStock = redisRepository.get("BM_MARKET_SECKILL_REAL_STOCKNUM_" + stallActivityId); 107 | int realStock = Integer.parseInt(redisRealStock == null ? "0" : redisRealStock); //剩余的真实库存 108 | if( realStock > 0 ) { 109 | response.setIsSuccess(false); 110 | response.setResponseCode(6103); 111 | response.setResponseMsg("秒杀失败,还有部分订单未完成支付,超时将返还库存"); 112 | response.setRefreshTime(0); 113 | } else { 114 | response.setIsSuccess(false); 115 | response.setResponseCode(6102); 116 | response.setResponseMsg("秒杀失败,商品已经售罄"); 117 | response.setRefreshTime(0); 118 | } 119 | } 120 | } 121 | } catch (Exception e) { 122 | e.printStackTrace(); 123 | response.setIsSuccess(false); 124 | response.setResponseCode(6102); 125 | response.setResponseMsg("秒杀失败,商品已经售罄"); 126 | response.setRefreshTime(0); 127 | } finally { 128 | logger.info("开始释放锁资源..."); 129 | redissonDistributedLocker.unlock(lockKey); //释放锁 130 | } 131 | return response; 132 | } 133 | 134 | /** 135 | * 判断秒杀活动是否已经开始 136 | *

Title: checkStartSeckill

137 | *

Description:

138 | * @param stallActivityId 139 | * @return 140 | */ 141 | @Override 142 | public boolean checkStartSeckill(int stallActivityId) { 143 | //此处已经省略了业务代码,良好的操作时应该将秒杀活动的开始时间在新增/编辑主数据的是维护到redis中,并维护好key值,此处取出,然后做出判断 144 | //默认为开始了 145 | return true; 146 | } 147 | 148 | } 149 | -------------------------------------------------------------------------------- /src/main/webapp/views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 45 | 46 | 47 |
48 | 49 | 分布式秒杀测试 50 | 51 | 当前活动编码:{{stallActivityId}} 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 设置库存 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 获取库存信息 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 开始秒杀 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 |
102 | 103 | 104 | 105 | 106 | 107 | 108 | 239 | -------------------------------------------------------------------------------- /src/main/java/cn/com/bluemoon/threads/Totp.java: -------------------------------------------------------------------------------- 1 | package cn.com.bluemoon.threads; 2 | 3 | /** 4 | Copyright (c) 2011 IETF Trust and the persons identified as 5 | authors of the code. All rights reserved. 6 | Redistribution and use in source and binary forms, with or without 7 | modification, is permitted pursuant to, and subject to the license 8 | terms contained in, the Simplified BSD License set forth in Section 9 | 4.c of the IETF Trust's Legal Provisions Relating to IETF Documents 10 | (http://trustee.ietf.org/license-info). 11 | */ 12 | 13 | import java.lang.reflect.UndeclaredThrowableException; 14 | import java.security.GeneralSecurityException; 15 | import java.util.Date; 16 | import javax.crypto.Mac; 17 | import javax.crypto.spec.SecretKeySpec; 18 | import java.math.BigInteger; 19 | 20 | /** 21 | * TOTP算法(Time-based One-time Password algorithm)是一种从共享密钥和当前时间计算一次性密码的算法。 22 | * 它已被采纳为Internet工程任务组标准RFC 6238,是Initiative for Open Authentication(OATH)的基石,并被用于许多双因素身份验证系统。 23 | * 24 | * TOTP是基于散列的消息认证码(HMAC)的示例。 它使用加密哈希函数将密钥与当前时间戳组合在一起以生成一次性密码。 25 | * 由于网络延迟和不同步时钟可能导致密码接收者必须尝试一系列可能的时间来进行身份验证,因此时间戳通常以30秒的间隔增加,从而减少了潜在的搜索空间。 26 | * 27 | * 该算法是网银动态口令实现的基石,如中国银行的动态口令就是基于此算法实现 28 | * @author Guoqing.Lee 29 | * @date 2019年5月24日 下午5:23:07 30 | * 31 | */ 32 | public class Totp { 33 | 34 | private Totp() { 35 | } 36 | 37 | /** 38 | * This method uses the JCE to provide the crypto algorithm. HMAC computes a 39 | * Hashed Message Authentication Code with the crypto hash algorithm as a 40 | * parameter. 41 | * 42 | * @param crypto 43 | * : the crypto algorithm (HmacSHA1, HmacSHA256, HmacSHA512) 44 | * @param keyBytes 45 | * : the bytes to use for the HMAC key 46 | * @param text 47 | * : the message or text to be authenticated 48 | */ 49 | private static byte[] hmac_sha(String crypto, byte[] keyBytes, byte[] text) { 50 | try { 51 | Mac hmac; 52 | hmac = Mac.getInstance(crypto); 53 | SecretKeySpec macKey = new SecretKeySpec(keyBytes, "RAW"); 54 | hmac.init(macKey); 55 | return hmac.doFinal(text); 56 | } catch (GeneralSecurityException gse) { 57 | throw new UndeclaredThrowableException(gse); 58 | } 59 | } 60 | 61 | /** 62 | * This method converts a HEX string to Byte[] 63 | * 64 | * @param hex 65 | * : the HEX string 66 | * 67 | * @return: a byte array 68 | */ 69 | 70 | private static byte[] hexStr2Bytes(String hex) { 71 | // Adding one byte to get the right conversion 72 | // Values starting with "0" can be converted 73 | byte[] bArray = new BigInteger("10" + hex, 16).toByteArray(); 74 | 75 | // Copy all the REAL bytes, not the "first" 76 | byte[] ret = new byte[bArray.length - 1]; 77 | for (int i = 0; i < ret.length; i++) 78 | ret[i] = bArray[i + 1]; 79 | return ret; 80 | } 81 | 82 | private static final int[] DIGITS_POWER 83 | // 0 1 2 3 4 5 6 7 8 84 | = { 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000 }; 85 | 86 | /** 87 | * This method generates a TOTP value for the given set of parameters. 88 | * 89 | * @param key 90 | * : the shared secret, HEX encoded 91 | * @param time 92 | * : a value that reflects a time 93 | * @param returnDigits 94 | * : number of digits to return 95 | * 96 | * @return: a numeric String in base 10 that includes 97 | * {@link truncationDigits} digits 98 | */ 99 | 100 | public static String generateTOTP(String key, String time, 101 | String returnDigits) { 102 | return generateTOTP(key, time, returnDigits, "HmacSHA1"); 103 | } 104 | 105 | /** 106 | * This method generates a TOTP value for the given set of parameters. 107 | * 108 | * @param key 109 | * : the shared secret, HEX encoded 110 | * @param time 111 | * : a value that reflects a time 112 | * @param returnDigits 113 | * : number of digits to return 114 | * 115 | * @return: a numeric String in base 10 that includes 116 | * {@link truncationDigits} digits 117 | */ 118 | 119 | public static String generateTOTP256(String key, String time, 120 | String returnDigits) { 121 | return generateTOTP(key, time, returnDigits, "HmacSHA256"); 122 | } 123 | 124 | /** 125 | * This method generates a TOTP value for the given set of parameters. 126 | * 127 | * @param key 128 | * : the shared secret, HEX encoded 129 | * @param time 130 | * : a value that reflects a time 131 | * @param returnDigits 132 | * : number of digits to return 133 | * 134 | * @return: a numeric String in base 10 that includes 135 | * {@link truncationDigits} digits 136 | */ 137 | 138 | public static String generateTOTP512(String key, String time, 139 | String returnDigits) { 140 | return generateTOTP(key, time, returnDigits, "HmacSHA512"); 141 | } 142 | 143 | /** 144 | * This method generates a TOTP value for the given set of parameters. 145 | * 146 | * @param key 147 | * : the shared secret, HEX encoded 148 | * @param time 149 | * : a value that reflects a time 150 | * @param returnDigits 151 | * : number of digits to return 152 | * @param crypto 153 | * : the crypto function to use 154 | * 155 | * @return: a numeric String in base 10 that includes 156 | * {@link truncationDigits} digits 157 | */ 158 | 159 | public static String generateTOTP(String key, String time, 160 | String returnDigits, String crypto) { 161 | int codeDigits = Integer.decode(returnDigits).intValue(); 162 | String result = null; 163 | 164 | // Using the counter 165 | // First 8 bytes are for the movingFactor 166 | // Compliant with base RFC 4226 (HOTP) 167 | while (time.length() < 16) 168 | time = "0" + time; 169 | 170 | // Get the HEX in a Byte[] 171 | byte[] msg = hexStr2Bytes(time); 172 | byte[] k = hexStr2Bytes(key); 173 | byte[] hash = hmac_sha(crypto, k, msg); 174 | 175 | // put selected bytes into result int 176 | int offset = hash[hash.length - 1] & 0xf; 177 | 178 | int binary = ((hash[offset] & 0x7f) << 24) 179 | | ((hash[offset + 1] & 0xff) << 16) 180 | | ((hash[offset + 2] & 0xff) << 8) | (hash[offset + 3] & 0xff); 181 | 182 | int otp = binary % DIGITS_POWER[codeDigits]; 183 | 184 | result = Integer.toString(otp); 185 | while (result.length() < codeDigits) { 186 | result = "0" + result; 187 | } 188 | return result; 189 | } 190 | 191 | /*public static void main(String[] args) { 192 | // Seed for HMAC-SHA1 - 20 bytes 193 | String seed = "3132333435363738393031323334353637383930"; 194 | // Seed for HMAC-SHA256 - 32 bytes 195 | String seed32 = "3132333435363738393031323334353637383930" 196 | + "313233343536373839303132"; 197 | // Seed for HMAC-SHA512 - 64 bytes 198 | String seed64 = "3132333435363738393031323334353637383930" 199 | + "3132333435363738393031323334353637383930" 200 | + "3132333435363738393031323334353637383930" + "31323334"; 201 | long T0 = 0; 202 | long X = 30; 203 | long testTime[] = { 59L, 1111111109L, 1111111111L, 1234567890L, 204 | 2000000000L, 20000000000L }; 205 | 206 | String steps = "0"; 207 | DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 208 | df.setTimeZone(TimeZone.getTimeZone("UTC")); 209 | try { 210 | System.out.println("+---------------+-----------------------+" 211 | + "------------------+--------+--------+"); 212 | System.out.println("| Time(sec) | Time (UTC format) " 213 | + "| Value of T(Hex) | TOTP | Mode |"); 214 | System.out.println("+---------------+-----------------------+" 215 | + "------------------+--------+--------+"); 216 | 217 | for (int i = 0; i < testTime.length; i++) { 218 | long T = (testTime[i] - T0) / X; 219 | steps = Long.toHexString(T).toUpperCase(); 220 | while (steps.length() < 16) 221 | steps = "0" + steps; 222 | String fmtTime = String.format("%1$-11s", testTime[i]); 223 | String utcTime = df.format(new Date(testTime[i] * 1000)); 224 | System.out.print("| " + fmtTime + " | " + utcTime + " | " 225 | + steps + " |"); 226 | System.out.println(generateTOTP(seed, steps, "8", "HmacSHA1") 227 | + "| SHA1 |"); 228 | System.out.print("| " + fmtTime + " | " + utcTime + " | " 229 | + steps + " |"); 230 | System.out.println(generateTOTP(seed32, steps, "8", 231 | "HmacSHA256") + "| SHA256 |"); 232 | System.out.print("| " + fmtTime + " | " + utcTime + " | " 233 | + steps + " |"); 234 | System.out.println(generateTOTP(seed64, steps, "8", 235 | "HmacSHA512") + "| SHA512 |"); 236 | 237 | System.out.println("+---------------+-----------------------+" 238 | + "------------------+--------+--------+"); 239 | } 240 | } catch (final Exception e) { 241 | System.out.println("Error : " + e); 242 | } 243 | }*/ 244 | 245 | 246 | public static void main(String[] args) { 247 | try { 248 | 249 | for (int j = 0; j < 10; j++) { 250 | long now = new Date().getTime(); 251 | System.out.println(now); 252 | String totp = generateTOTP("123456", Long.toString(now), "8", "HmacSHA256"); 253 | System.out.println(String.format("加密后: %s", totp)); 254 | //Thread.sleep(1000); 255 | } 256 | 257 | } catch (final Exception e) { 258 | e.printStackTrace(); 259 | } 260 | } 261 | } -------------------------------------------------------------------------------- /src/main/java/cn/com/bluemoon/controller/SeckillController.java: -------------------------------------------------------------------------------- 1 | /** 2 | *

Title: SeckillApiController.java

3 | *

Description:

4 | *

Copyright: Copyright (c) 2018

5 | *

Company: www.bluemoon.com

6 | * @author Guoqing 7 | * @date 2018年8月10日 8 | */ 9 | package cn.com.bluemoon.controller; 10 | 11 | import org.apache.commons.lang3.StringUtils; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.web.bind.annotation.CrossOrigin; 16 | import org.springframework.web.bind.annotation.GetMapping; 17 | import org.springframework.web.bind.annotation.RequestBody; 18 | import org.springframework.web.bind.annotation.RequestMapping; 19 | import org.springframework.web.bind.annotation.RequestMethod; 20 | import org.springframework.web.bind.annotation.RestController; 21 | 22 | import com.alibaba.fastjson.JSONObject; 23 | 24 | import cn.com.bluemoon.common.response.BaseResponse; 25 | import cn.com.bluemoon.common.response.SeckillInfoResponse; 26 | import cn.com.bluemoon.common.response.StockNumResponse; 27 | import cn.com.bluemoon.kafka.KafkaSender; 28 | import cn.com.bluemoon.redis.lock.RedissonDistributedLocker; 29 | import cn.com.bluemoon.redis.repository.RedisRepository; 30 | import cn.com.bluemoon.service.ISeckillService; 31 | import cn.com.bluemoon.utils.AssertUtil; 32 | import cn.com.bluemoon.utils.StringUtil; 33 | import cn.hutool.system.SystemUtil; 34 | import io.swagger.annotations.Api; 35 | import io.swagger.annotations.ApiOperation; 36 | 37 | /** 38 | *

Title: SeckillApiController

39 | *

Description: 秒杀相关接口

40 | * @author Guoqing 41 | * @date 2018年8月10日 42 | */ 43 | @Api(tags="分布式秒杀") 44 | @RestController 45 | @CrossOrigin 46 | @RequestMapping(value = "/api/seckill") 47 | public class SeckillController { 48 | 49 | @Autowired 50 | private RedisRepository redisRepository; 51 | @Autowired 52 | private ISeckillService seckillService; 53 | @Autowired 54 | private KafkaSender kafkaSender; 55 | @Autowired 56 | private RedissonDistributedLocker redissonDistributedLocker; 57 | 58 | Logger logger = LoggerFactory.getLogger(SeckillController.class); 59 | 60 | /** 61 | * 设置活动库存 62 | * @param jsonObject 63 | * @return 64 | */ 65 | @ApiOperation(value="设置活动库存",nickname="Guoqing") 66 | @RequestMapping(value="/setStockNum", method=RequestMethod.POST) 67 | public BaseResponse setStockNum(@RequestBody JSONObject jsonObject) { 68 | 69 | int stockNum = jsonObject.containsKey("stockNum")?jsonObject.getInteger("stockNum"):0; 70 | int stallActivityId = jsonObject.containsKey("stallActivityId") ? jsonObject.getInteger("stallActivityId") : -1; 71 | AssertUtil.isTrue(stallActivityId != -1, "非法参数"); 72 | redisRepository.incrBy("BM_MARKET_SECKILL_STOCKNUM_" + stallActivityId, stockNum); 73 | redisRepository.incrBy("BM_MARKET_SECKILL_REAL_STOCKNUM_" + stallActivityId, stockNum); 74 | 75 | return new BaseResponse(); 76 | } 77 | 78 | /** 79 | * 查看活动库存情况 80 | * @param jsonObject 81 | * @return 82 | */ 83 | @ApiOperation(value="查看活动库存",nickname="Guoqing") 84 | @RequestMapping(value="/getStockNum", method=RequestMethod.POST) 85 | public StockNumResponse getStockNum(@RequestBody JSONObject jsonObject) { 86 | StockNumResponse response = new StockNumResponse(); 87 | int stallActivityId = jsonObject.containsKey("stallActivityId") ? jsonObject.getInteger("stallActivityId") : -1; 88 | AssertUtil.isTrue(stallActivityId != -1, "非法参数"); 89 | String stockNum = redisRepository.get("BM_MARKET_SECKILL_STOCKNUM_" + stallActivityId); 90 | String realStockNum = redisRepository.get("BM_MARKET_SECKILL_REAL_STOCKNUM_" + stallActivityId); 91 | response.setStockNum(Long.parseLong(stockNum)); 92 | response.setRealStockNum(Long.parseLong(realStockNum)); 93 | return response; 94 | } 95 | 96 | /** 97 | * 06.04-去秒杀,创建秒杀订单 98 | * 通过分布式锁的方式控制,控制库存不超卖 99 | *

Title: testSeckill

100 | *

Description: 秒杀下单

101 | * @param jsonObject 102 | * @return 103 | */ 104 | @ApiOperation(value="去秒杀--先分布式锁模式",nickname="Guoqing") 105 | @RequestMapping(value="/goSeckill", method=RequestMethod.POST) 106 | public SeckillInfoResponse goSeckill(@RequestBody JSONObject jsonObject) { 107 | int stallActivityId = jsonObject.containsKey("stallActivityId") ? jsonObject.getInteger("stallActivityId") : -1; //活动Id 108 | AssertUtil.isTrue(stallActivityId != -1, "非法參數"); 109 | int purchaseNum = jsonObject.containsKey("purchaseNum") ? jsonObject.getInteger("purchaseNum") : 1; //购买数量 110 | AssertUtil.isTrue(purchaseNum != -1, "非法參數"); 111 | String openId = jsonObject.containsKey("openId") ? jsonObject.getString("openId") : null; 112 | AssertUtil.isTrue(!StringUtil.isEmpty(openId), 1101, "非法參數"); 113 | String formId = jsonObject.containsKey("formId") ? jsonObject.getString("formId") : null; 114 | AssertUtil.isTrue(!StringUtil.isEmpty(formId), 1101, "非法參數"); 115 | long addressId = jsonObject.containsKey("addressId") ? jsonObject.getLong("addressId") : -1; 116 | AssertUtil.isTrue(addressId != -1, "非法參數"); 117 | //通过分享入口进来的参数 118 | String shareCode = jsonObject.getString("shareCode"); 119 | String shareSource = jsonObject.getString("shareSource"); 120 | String userCode = jsonObject.getString("userId"); 121 | 122 | //这里拒绝多余的请求,比如库存100,那么超过500或者1000的请求都可以拒绝掉,利用redis的原子自增 123 | long count = redisRepository.incr("BM_MARKET_SECKILL_COUNT_" + stallActivityId); 124 | if( count > 1000 ) { 125 | SeckillInfoResponse response = new SeckillInfoResponse(); 126 | response.setIsSuccess(false); 127 | response.setResponseCode(6405); 128 | response.setResponseMsg( "活动太火爆,已经售罄啦!"); 129 | return response; 130 | } 131 | logger.info("第" + count + "个请求进入到了消息队列"); 132 | 133 | return seckillService.startSeckill(stallActivityId, purchaseNum, openId, formId, addressId, shareCode, shareSource, userCode); 134 | } 135 | 136 | /** 137 | * 秒杀接口,先将请求放入队列模式 138 | * @param jsonObject 139 | * @return 140 | */ 141 | @ApiOperation(value="去秒杀--先队列模式",nickname="Guoqing") 142 | @RequestMapping(value="/goSeckillByQueue", method=RequestMethod.POST) 143 | public BaseResponse goSeckillByQueue(@RequestBody JSONObject jsonObject) { 144 | int stallActivityId = jsonObject.containsKey("stallActivityId") ? jsonObject.getInteger("stallActivityId") : -1; //活动Id 145 | AssertUtil.isTrue(stallActivityId != -1, "非法參數"); 146 | int purchaseNum = jsonObject.containsKey("purchaseNum") ? jsonObject.getInteger("purchaseNum") : 1; //购买数量 147 | AssertUtil.isTrue(purchaseNum != -1, "非法參數"); 148 | String openId = jsonObject.containsKey("openId") ? jsonObject.getString("openId") : null; 149 | AssertUtil.isTrue(!StringUtil.isEmpty(openId), 1101, "非法參數"); 150 | String formId = jsonObject.containsKey("formId") ? jsonObject.getString("formId") : null; 151 | AssertUtil.isTrue(!StringUtil.isEmpty(formId), 1101, "非法參數"); 152 | long addressId = jsonObject.containsKey("addressId") ? jsonObject.getLong("addressId") : -1; 153 | AssertUtil.isTrue(addressId != -1, "非法參數"); 154 | //通过分享入口进来的参数 155 | String shareCode = jsonObject.getString("shareCode"); 156 | String shareSource = jsonObject.getString("shareSource"); 157 | String userCode = jsonObject.getString("userId"); 158 | 159 | JSONObject jsonStr = new JSONObject(); 160 | jsonStr.put("stallActivityId", stallActivityId); 161 | jsonStr.put("purchaseNum", purchaseNum); 162 | jsonStr.put("openId", openId); 163 | jsonStr.put("addressId", addressId); 164 | jsonStr.put("formId", formId); 165 | jsonStr.put("shareCode", shareCode); 166 | jsonStr.put("shareSource", shareSource); 167 | jsonStr.put("userCode", userCode); 168 | //判断秒杀活动是否开始 169 | if( !seckillService.checkStartSeckill(stallActivityId) ) { 170 | return new BaseResponse(false, 6205, "秒杀活动尚未开始,请稍等!"); 171 | } 172 | //这里拒绝多余的请求,比如库存100,那么超过500或者1000的请求都可以拒绝掉,利用redis的原子自增操作 173 | long count = redisRepository.incr("BM_MARKET_SECKILL_COUNT_" + stallActivityId); 174 | if( count > 500 ) { 175 | return new BaseResponse(false, 6405, "活动太火爆,已经售罄啦!"); 176 | } 177 | logger.info("第" + count + "个请求进入到了消息队列"); 178 | //做用户重复购买校验 179 | if( redisRepository.exists("BM_MARKET_SECKILL_LIMIT_" + stallActivityId + "_" + openId) ) { 180 | return new BaseResponse(false, 6105, "您正在参与该活动,不能重复购买!"); 181 | } 182 | //放入kafka消息队列 183 | kafkaSender.sendChannelMess("demo_seckill_queue", jsonStr.toString()); 184 | return new BaseResponse(); 185 | } 186 | 187 | /** 188 | * 06.05-轮询请求当前用户是否秒杀下单成功 189 | *

Title: seckillPolling

190 | *

Description:

191 | * @param jsonObject 192 | * @return 193 | */ 194 | @ApiOperation(value="轮询接口--先分布式锁模式",nickname="Guoqing") 195 | @RequestMapping(value="/seckillPolling", method=RequestMethod.POST) 196 | public SeckillInfoResponse seckillPolling(@RequestBody JSONObject jsonObject) { 197 | int stallActivityId = jsonObject.containsKey("stallActivityId") ? jsonObject.getInteger("stallActivityId") : -1; //活动Id 198 | AssertUtil.isTrue(stallActivityId != -1, "非法參數"); 199 | String openId = jsonObject.containsKey("openId") ? jsonObject.getString("openId") : null; 200 | AssertUtil.isTrue(!StringUtil.isEmpty(openId), 1101, "非法參數"); 201 | 202 | SeckillInfoResponse response = new SeckillInfoResponse(); 203 | if( redisRepository.exists("BM_MARKET_LOCK_POLLING_" + stallActivityId + "_" + openId) ) { 204 | //如果缓存中存在锁定秒杀和用户ID的key,则证明该订单尚未处理完成,需要继续等待 205 | response.setIsSuccess(true); 206 | response.setResponseCode(6103); 207 | response.setResponseMsg("排队中,请稍后"); 208 | response.setRefreshTime(1000); 209 | } else { 210 | //如果缓存中该key已经不存在,则表明该订单已经下单成功,可以进入支付操作,并取出orderId返回 211 | String redisOrderInfo = redisRepository.get("BM_MARKET_SECKILL_ORDERID_" + stallActivityId + "_" + openId); 212 | if( redisOrderInfo == null ) { 213 | response.setIsSuccess(false); 214 | response.setResponseCode(6106); 215 | response.setResponseMsg("秒杀失败,下单出现异常,请重试!"); 216 | response.setOrderCode(null); 217 | response.setRefreshTime(0); 218 | }else { 219 | response.setIsSuccess(true); 220 | response.setResponseCode(6104); 221 | response.setResponseMsg("秒杀成功"); 222 | response.setOrderCode(redisOrderInfo); 223 | response.setRefreshTime(0); 224 | } 225 | } 226 | return response; 227 | } 228 | 229 | /** 230 | * 轮询请求 判断是否获得下单资格 231 | * @param jsonObject 232 | * @return 233 | */ 234 | @ApiOperation(value="轮询接口--先队列模式",nickname="Guoqing") 235 | @RequestMapping(value="/seckillPollingQueue", method=RequestMethod.POST) 236 | public SeckillInfoResponse seckillPollingQueue(@RequestBody JSONObject jsonObject) { 237 | int stallActivityId = jsonObject.containsKey("stallActivityId") ? jsonObject.getInteger("stallActivityId") : -1; //活动Id 238 | AssertUtil.isTrue(stallActivityId != -1, "非法參數"); 239 | String openId = jsonObject.containsKey("openId") ? jsonObject.getString("openId") : null; 240 | AssertUtil.isTrue(!StringUtil.isEmpty(openId), 1101, "非法參數"); 241 | 242 | SeckillInfoResponse response = new SeckillInfoResponse(); 243 | //是否存在下单资格码的key 244 | if( redisRepository.exists("BM_MARKET_SECKILL_QUEUE_"+stallActivityId+"_"+openId) ){ 245 | String result = redisRepository.get("BM_MARKET_SECKILL_QUEUE_"+stallActivityId+"_"+openId); 246 | response = JSONObject.parseObject(JSONObject.parseObject(result).getJSONObject("response").toJSONString(), SeckillInfoResponse.class); 247 | } else { 248 | response.setIsSuccess(true); 249 | response.setResponseCode(0); 250 | response.setResponseMsg("活动太火爆,排队中..."); 251 | response.setRefreshTime(0); 252 | } 253 | return response; 254 | } 255 | 256 | /** 257 | * 根据获取到的下单资格码创建订单 258 | * @param jsonObject 259 | * @return 260 | */ 261 | @ApiOperation(value="先队列模式--下单接口",nickname="Guoqing") 262 | @RequestMapping(value="/createOrder", method=RequestMethod.POST) 263 | public BaseResponse createOrder(@RequestBody JSONObject jsonObject) { 264 | int stallActivityId = jsonObject.containsKey("stallActivityId") ? jsonObject.getInteger("stallActivityId") : -1; //活动Id 265 | AssertUtil.isTrue(stallActivityId != -1, "非法參數"); 266 | String openId = jsonObject.containsKey("openId") ? jsonObject.getString("openId") : null; 267 | AssertUtil.isTrue(!StringUtil.isEmpty(openId), 1101, "非法參數"); 268 | String orderQualificationCode = jsonObject.containsKey("orderQualificationCode") ? jsonObject.getString("orderQualificationCode") : null; 269 | AssertUtil.isTrue(!StringUtil.isEmpty(orderQualificationCode), 1101, "非法參數"); 270 | 271 | //校验下单资格码 272 | String redisQualificationCode = redisRepository.get("BM_MARKET_SECKILL_QUALIFICATION_CODE_" + stallActivityId + "_" + openId); 273 | if(StringUtils.isEmpty(redisQualificationCode) || !orderQualificationCode.equals(redisQualificationCode) ) { 274 | return new BaseResponse(false, 6305, "您的资格码已经过期!"); 275 | }else { 276 | //走后续的下单流程,并校验真实库存;该接口的流量已经是与真实库存几乎相匹配的流量值,按理不应该存在超高并发 277 | return new BaseResponse(); 278 | } 279 | } 280 | 281 | @ApiOperation(value="test",nickname="Guoqing") 282 | @GetMapping(value="/test") 283 | public void test() throws InterruptedException { 284 | final int[] counter = {0}; 285 | 286 | for (int i= 0; i < 300; i++){ 287 | 288 | new Thread(new Runnable() { 289 | 290 | @Override 291 | 292 | public void run() { 293 | boolean isGetLock = redissonDistributedLocker.tryLock("test0001", 3L, 1L); 294 | logger.info(isGetLock + ""); 295 | if(isGetLock) { 296 | try { 297 | int a = counter[0]; 298 | counter[0] = a + 1; 299 | logger.info(a + ""); 300 | } finally { 301 | redissonDistributedLocker.unlock("test0001"); 302 | } 303 | } 304 | } 305 | }).start(); 306 | 307 | } 308 | 309 | // 主线程休眠,等待结果 310 | Thread.sleep(10000); 311 | logger.info(counter[0] + ""); 312 | } 313 | 314 | @ApiOperation(value="test1",nickname="Guoqing") 315 | @GetMapping(value="/test1") 316 | public void test1() throws InterruptedException { 317 | final int[] counter = {0}; 318 | 319 | for (int i= 0; i < 100; i++){ 320 | 321 | new Thread(new Runnable() { 322 | 323 | @Override 324 | 325 | public void run() { 326 | try { 327 | redissonDistributedLocker.lock("test0002", 1L); 328 | logger.info(redissonDistributedLocker.isLocked("test0002") + ""); 329 | int a = counter[0]; 330 | counter[0] = a + 1; 331 | logger.info(a + ""); 332 | } finally { 333 | redissonDistributedLocker.unlock("test0002"); 334 | } 335 | } 336 | }).start(); 337 | 338 | } 339 | 340 | // 主线程休眠,等待结果 341 | Thread.sleep(10000); 342 | logger.info(counter[0] + ""); 343 | } 344 | 345 | @ApiOperation(value="test2",nickname="Guoqing") 346 | @GetMapping(value="/test2") 347 | public void test2() throws InterruptedException { 348 | logger.info(SystemUtil.getJavaRuntimeInfo().toString()); 349 | logger.info(SystemUtil.getJavaInfo().toString()); 350 | logger.info(SystemUtil.getJvmInfo().toString()); 351 | logger.info(SystemUtil.getJavaSpecInfo().toString()); 352 | logger.info(SystemUtil.getRuntimeInfo().toString()); 353 | } 354 | } 355 | -------------------------------------------------------------------------------- /src/main/java/cn/com/bluemoon/redis/repository/RedisRepository.java: -------------------------------------------------------------------------------- 1 | package cn.com.bluemoon.redis.repository; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.data.redis.connection.RedisServerCommands; 7 | import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; 8 | import org.springframework.data.redis.core.HashOperations; 9 | import org.springframework.data.redis.core.ListOperations; 10 | import org.springframework.data.redis.core.RedisCallback; 11 | import org.springframework.data.redis.core.RedisTemplate; 12 | import org.springframework.data.redis.serializer.RedisSerializer; 13 | import org.springframework.data.redis.support.atomic.RedisAtomicLong; 14 | 15 | import java.nio.charset.Charset; 16 | import java.util.*; 17 | 18 | /** 19 | * Redis Repository 20 | * 21 | * @author Guoqing 22 | */ 23 | public class RedisRepository { 24 | 25 | /** 26 | * Logger 27 | */ 28 | private static final Logger LOGGER = LoggerFactory.getLogger(RedisRepository.class); 29 | 30 | /** 31 | * 默认编码 32 | */ 33 | private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); 34 | 35 | /** 36 | * Spring Redis Template 37 | */ 38 | private RedisTemplate redisTemplate; 39 | 40 | @Autowired 41 | private JedisConnectionFactory jedisConnectionFactory; 42 | 43 | public RedisRepository(RedisTemplate redisTemplate) { 44 | this.redisTemplate = redisTemplate; 45 | } 46 | 47 | /** 48 | * 添加到带有 过期时间的 缓存 49 | * 50 | * @param key redis主键 51 | * @param value 值 52 | * @param time 过期时间 53 | */ 54 | public void setExpire(final byte[] key, final byte[] value, final long time) { 55 | redisTemplate.execute((RedisCallback) connection -> { 56 | connection.set(key, value); 57 | connection.expire(key, time); 58 | LOGGER.info("[redisTemplate redis]放入 缓存 url:{} ========缓存时间为{}秒", key, time); 59 | return 1L; 60 | }); 61 | } 62 | 63 | /** 64 | * 添加到带有 过期时间的 缓存 65 | * 66 | * @param key redis主键 67 | * @param value 值 68 | * @param time 过期时间 69 | */ 70 | public void setExpire(final String key, final String value, final long time) { 71 | redisTemplate.execute((RedisCallback) connection -> { 72 | RedisSerializer serializer = getRedisSerializer(); 73 | byte[] keys = serializer.serialize(key); 74 | byte[] values = serializer.serialize(value); 75 | connection.set(keys, values); 76 | connection.expire(keys, time); 77 | LOGGER.info("[redisTemplate redis]放入 缓存 url:{} ========缓存时间为{}秒", key, time); 78 | return 1L; 79 | }); 80 | } 81 | 82 | /** 83 | * 一次性添加数组到 过期时间的 缓存,不用多次连接,节省开销 84 | * 85 | * @param keys redis主键数组 86 | * @param values 值数组 87 | * @param time 过期时间 88 | */ 89 | public void setExpire(final String[] keys, final String[] values, final long time) { 90 | redisTemplate.execute((RedisCallback) connection -> { 91 | RedisSerializer serializer = getRedisSerializer(); 92 | for (int i = 0; i < keys.length; i++) { 93 | byte[] bKeys = serializer.serialize(keys[i]); 94 | byte[] bValues = serializer.serialize(values[i]); 95 | connection.set(bKeys, bValues); 96 | connection.expire(bKeys, time); 97 | LOGGER.info("[redisTemplate redis]放入 缓存 url:{} ========缓存时间为:{}秒", keys[i], time); 98 | } 99 | return 1L; 100 | }); 101 | } 102 | 103 | 104 | /** 105 | * 一次性添加数组到 过期时间的 缓存,不用多次连接,节省开销 106 | * 107 | * @param keys the keys 108 | * @param values the values 109 | */ 110 | public void set(final String[] keys, final String[] values) { 111 | redisTemplate.execute((RedisCallback) connection -> { 112 | RedisSerializer serializer = getRedisSerializer(); 113 | for (int i = 0; i < keys.length; i++) { 114 | byte[] bKeys = serializer.serialize(keys[i]); 115 | byte[] bValues = serializer.serialize(values[i]); 116 | connection.set(bKeys, bValues); 117 | LOGGER.info("[redisTemplate redis]放入 缓存 url:{}", keys[i]); 118 | } 119 | return 1L; 120 | }); 121 | } 122 | 123 | 124 | /** 125 | * 添加到缓存 126 | * 127 | * @param key the key 128 | * @param value the value 129 | */ 130 | public void set(final String key, final String value) { 131 | redisTemplate.execute((RedisCallback) connection -> { 132 | RedisSerializer serializer = getRedisSerializer(); 133 | byte[] keys = serializer.serialize(key); 134 | byte[] values = serializer.serialize(value); 135 | connection.set(keys, values); 136 | LOGGER.info("[redisTemplate redis]放入 缓存 url:{}", key); 137 | return 1L; 138 | }); 139 | } 140 | 141 | /** 142 | * 查询在这个时间段内即将过期的key 143 | * 144 | * @param key the key 145 | * @param time the time 146 | * @return the list 147 | */ 148 | public List willExpire(final String key, final long time) { 149 | final List keysList = new ArrayList<>(); 150 | redisTemplate.execute((RedisCallback>) connection -> { 151 | Set keys = redisTemplate.keys(key + "*"); 152 | for (String key1 : keys) { 153 | Long ttl = connection.ttl(key1.getBytes(DEFAULT_CHARSET)); 154 | if (0 <= ttl && ttl <= 2 * time) { 155 | keysList.add(key1); 156 | } 157 | } 158 | return keysList; 159 | }); 160 | return keysList; 161 | } 162 | 163 | 164 | /** 165 | * 查询在以keyPatten的所有 key 166 | * 167 | * @param keyPatten the key patten 168 | * @return the set 169 | */ 170 | public Set keys(final String keyPatten) { 171 | return redisTemplate.execute((RedisCallback>) connection -> redisTemplate.keys(keyPatten + "*")); 172 | } 173 | 174 | /** 175 | * 根据key获取对象 176 | * 177 | * @param key the key 178 | * @return the byte [ ] 179 | */ 180 | public byte[] get(final byte[] key) { 181 | byte[] result = redisTemplate.execute((RedisCallback) connection -> connection.get(key)); 182 | // LOGGER.info("[redisTemplate redis]取出 缓存 url:{} ", key); 183 | return result; 184 | } 185 | 186 | /** 187 | * 根据key获取对象 188 | * 189 | * @param key the key 190 | * @return the string 191 | */ 192 | public String get(final String key) { 193 | String resultStr = redisTemplate.execute((RedisCallback) connection -> { 194 | RedisSerializer serializer = getRedisSerializer(); 195 | byte[] keys = serializer.serialize(key); 196 | byte[] values = connection.get(keys); 197 | return serializer.deserialize(values); 198 | }); 199 | // LOGGER.info("[redisTemplate redis]取出 缓存 url:{} ", key); 200 | return resultStr; 201 | } 202 | 203 | 204 | /** 205 | * 根据key获取对象 206 | * 207 | * @param keyPatten the key patten 208 | * @return the keys values 209 | */ 210 | public Map getKeysValues(final String keyPatten) { 211 | LOGGER.info("[redisTemplate redis] getValues() patten={} ", keyPatten); 212 | return redisTemplate.execute((RedisCallback>) connection -> { 213 | RedisSerializer serializer = getRedisSerializer(); 214 | Map maps = new HashMap<>(); 215 | Set keys = redisTemplate.keys(keyPatten + "*"); 216 | for (String key : keys) { 217 | byte[] bKeys = serializer.serialize(key); 218 | byte[] bValues = connection.get(bKeys); 219 | String value = serializer.deserialize(bValues); 220 | maps.put(key, value); 221 | } 222 | return maps; 223 | }); 224 | } 225 | 226 | /** 227 | * Ops for hash hash operations. 228 | * 229 | * @return the hash operations 230 | */ 231 | public HashOperations opsForHash() { 232 | return redisTemplate.opsForHash(); 233 | } 234 | 235 | /** 236 | * 对HashMap操作 237 | * 238 | * @param key the key 239 | * @param hashKey the hash key 240 | * @param hashValue the hash value 241 | */ 242 | public void putHashValue(String key, String hashKey, String hashValue) { 243 | LOGGER.info("[redisTemplate redis] putHashValue() key={},hashKey={},hashValue={} ", key, hashKey, hashValue); 244 | opsForHash().put(key, hashKey, hashValue); 245 | } 246 | 247 | /** 248 | * 获取单个field对应的值 249 | * 250 | * @param key the key 251 | * @param hashKey the hash key 252 | * @return the hash values 253 | */ 254 | public Object getHashValues(String key, String hashKey) { 255 | LOGGER.info("[redisTemplate redis] getHashValues() key={},hashKey={}", key, hashKey); 256 | return opsForHash().get(key, hashKey); 257 | } 258 | 259 | /** 260 | * 根据key值删除 261 | * 262 | * @param key the key 263 | * @param hashKeys the hash keys 264 | */ 265 | public void delHashValues(String key, Object... hashKeys) { 266 | LOGGER.info("[redisTemplate redis] delHashValues() key={}", key); 267 | opsForHash().delete(key, hashKeys); 268 | } 269 | 270 | /** 271 | * key只匹配map 272 | * 273 | * @param key the key 274 | * @return the hash value 275 | */ 276 | public Map getHashValue(String key) { 277 | LOGGER.info("[redisTemplate redis] getHashValue() key={}", key); 278 | return opsForHash().entries(key); 279 | } 280 | 281 | /** 282 | * 批量添加 283 | * 284 | * @param key the key 285 | * @param map the map 286 | */ 287 | public void putHashValues(String key, Map map) { 288 | opsForHash().putAll(key, map); 289 | } 290 | 291 | /** 292 | * 集合数量 293 | * 294 | * @return the long 295 | */ 296 | public long dbSize() { 297 | return redisTemplate.execute(RedisServerCommands::dbSize); 298 | } 299 | 300 | /** 301 | * 清空redis存储的数据 302 | * 303 | * @return the string 304 | */ 305 | public String flushDB() { 306 | return redisTemplate.execute((RedisCallback) connection -> { 307 | connection.flushDb(); 308 | return "ok"; 309 | }); 310 | } 311 | 312 | /** 313 | * 判断某个主键是否存在 314 | * 315 | * @param key the key 316 | * @return the boolean 317 | */ 318 | public boolean exists(final String key) { 319 | return redisTemplate.execute((RedisCallback) connection -> connection.exists(key.getBytes(DEFAULT_CHARSET))); 320 | } 321 | 322 | 323 | /** 324 | * 删除key 325 | * 326 | * @param keys the keys 327 | * @return the long 328 | */ 329 | public long del(final String... keys) { 330 | return redisTemplate.execute((RedisCallback) connection -> { 331 | long result = 0; 332 | for (String key : keys) { 333 | result = connection.del(key.getBytes(DEFAULT_CHARSET)); 334 | } 335 | return result; 336 | }); 337 | } 338 | 339 | /** 340 | * 获取 RedisSerializer 341 | * 342 | * @return the redis serializer 343 | */ 344 | protected RedisSerializer getRedisSerializer() { 345 | return redisTemplate.getStringSerializer(); 346 | } 347 | 348 | /** 349 | * 对某个主键对应的值加一,value值必须是全数字的字符串 350 | * 351 | * @param key the key 352 | * @return the long 353 | */ 354 | public long incr(final String key) { 355 | return redisTemplate.execute((RedisCallback) connection -> { 356 | RedisSerializer redisSerializer = getRedisSerializer(); 357 | return connection.incr(redisSerializer.serialize(key)); 358 | }); 359 | } 360 | 361 | /** 362 | * 对某个主键对应的值减一,value值必须是全数字的字符串 363 | *

Title: decr

364 | *

Description:

365 | * @param key 366 | * @return 367 | */ 368 | public long decr(final String key) { 369 | return redisTemplate.execute((RedisCallback) connection -> { 370 | RedisSerializer redisSerializer = getRedisSerializer(); 371 | return connection.decr(redisSerializer.serialize(key)); 372 | }); 373 | } 374 | 375 | /** 376 | * 自增指定的长度 377 | * @param key 378 | * @param increment 379 | * @return 380 | */ 381 | public long incrBy(final String key, long increment) { 382 | return redisTemplate.execute((RedisCallback) connection -> { 383 | RedisSerializer redisSerializer = getRedisSerializer(); 384 | return connection.incrBy(redisSerializer.serialize(key), increment); 385 | }); 386 | } 387 | 388 | /** 389 | * 自减指定的长度 390 | * @param key 391 | * @param decrement 392 | * @return 393 | */ 394 | public long decrBy(final String key, long decrement) { 395 | return redisTemplate.execute((RedisCallback) connection -> { 396 | RedisSerializer redisSerializer = getRedisSerializer(); 397 | return connection.decrBy(redisSerializer.serialize(key), decrement); 398 | }); 399 | } 400 | 401 | /** 402 | * redis List 引擎 403 | * 404 | * @return the list operations 405 | */ 406 | public ListOperations opsForList() { 407 | return redisTemplate.opsForList(); 408 | } 409 | 410 | /** 411 | * redis List数据结构 : 将一个或多个值 value 插入到列表 key 的表头 412 | * 413 | * @param key the key 414 | * @param value the value 415 | * @return the long 416 | */ 417 | public Long leftPush(String key, String value) { 418 | return opsForList().leftPush(key, value); 419 | } 420 | 421 | /** 422 | * redis List数据结构 : 移除并返回列表 key 的头元素 423 | * 424 | * @param key the key 425 | * @return the string 426 | */ 427 | public String leftPop(String key) { 428 | return opsForList().leftPop(key); 429 | } 430 | 431 | /** 432 | * redis List数据结构 :将一个或多个值 value 插入到列表 key 的表尾(最右边)。 433 | * 434 | * @param key the key 435 | * @param value the value 436 | * @return the long 437 | */ 438 | public Long in(String key, String value) { 439 | return opsForList().rightPush(key, value); 440 | } 441 | 442 | /** 443 | * redis List数据结构 : 移除并返回列表 key 的末尾元素 444 | * 445 | * @param key the key 446 | * @return the string 447 | */ 448 | public String rightPop(String key) { 449 | return opsForList().rightPop(key); 450 | } 451 | 452 | 453 | /** 454 | * redis List数据结构 : 返回列表 key 的长度 ; 如果 key 不存在,则 key 被解释为一个空列表,返回 0 ; 如果 key 不是列表类型,返回一个错误。 455 | * 456 | * @param key the key 457 | * @return the long 458 | */ 459 | public Long length(String key) { 460 | return opsForList().size(key); 461 | } 462 | 463 | 464 | /** 465 | * redis List数据结构 : 根据参数 i 的值,移除列表中与参数 value 相等的元素 466 | * 467 | * @param key the key 468 | * @param i the 469 | * @param value the value 470 | */ 471 | public void remove(String key, long i, String value) { 472 | opsForList().remove(key, i, value); 473 | } 474 | 475 | /** 476 | * redis List数据结构 : 将列表 key 下标为 index 的元素的值设置为 value 477 | * 478 | * @param key the key 479 | * @param index the index 480 | * @param value the value 481 | */ 482 | public void set(String key, long index, String value) { 483 | opsForList().set(key, index, value); 484 | } 485 | 486 | /** 487 | * redis List数据结构 : 返回列表 key 中指定区间内的元素,区间以偏移量 start 和 end 指定。 488 | * 489 | * @param key the key 490 | * @param start the start 491 | * @param end the end 492 | * @return the list 493 | */ 494 | public List getList(String key, int start, int end) { 495 | return opsForList().range(key, start, end); 496 | } 497 | 498 | /** 499 | * redis List数据结构 : 批量存储 500 | * 501 | * @param key the key 502 | * @param list the list 503 | * @return the long 504 | */ 505 | public Long leftPushAll(String key, List list) { 506 | return opsForList().leftPushAll(key, list); 507 | } 508 | 509 | /** 510 | * redis List数据结构 : 将值 value 插入到列表 key 当中,位于值 index 之前或之后,默认之后。 511 | * 512 | * @param key the key 513 | * @param index the index 514 | * @param value the value 515 | */ 516 | public void insert(String key, long index, String value) { 517 | opsForList().set(key, index, value); 518 | } 519 | 520 | /** 521 | * 利用redis的单线程原子自增性保证数据自增的唯一性 522 | * 523 | * @param key 524 | * @return 525 | */ 526 | public RedisAtomicLong getRedisAtomicLong(String key) { 527 | return new RedisAtomicLong(key, jedisConnectionFactory); 528 | } 529 | 530 | /** 531 | * ZINCRBY key increment member 532 | * 533 | * @param key 534 | * @param increment 535 | * @param member 536 | */ 537 | public void doZincrby(String key, Integer increment, String member) { 538 | redisTemplate.execute((RedisCallback) connection -> { 539 | RedisSerializer redisSerializer = getRedisSerializer(); 540 | return connection.zIncrBy(redisSerializer.serialize(key), increment, redisSerializer.serialize(member)); 541 | }); 542 | } 543 | 544 | /** 545 | * ZREVRANGE key start stop [WITHSCORES] 546 | * 547 | * @return 548 | */ 549 | public List doZrevrange(String key, Integer start, Integer end) { 550 | 551 | List stringList = new ArrayList<>(); 552 | RedisSerializer redisSerializer = getRedisSerializer(); 553 | Set strBytes = redisTemplate.execute((RedisCallback>) connection -> connection.zRevRange(redisSerializer.serialize(key), start, end)); 554 | Iterator byteIter = strBytes.iterator(); 555 | while (byteIter.hasNext()) { 556 | stringList.add(redisSerializer.deserialize((byte[]) byteIter.next())); 557 | } 558 | return stringList; 559 | } 560 | 561 | } 562 | -------------------------------------------------------------------------------- /src/main/java/cn/com/bluemoon/utils/DateUtil.java: -------------------------------------------------------------------------------- 1 | package cn.com.bluemoon.utils; 2 | 3 | import java.text.ParseException; 4 | import java.text.SimpleDateFormat; 5 | import java.util.ArrayList; 6 | import java.util.Calendar; 7 | import java.util.Date; 8 | import java.util.List; 9 | import java.util.Locale; 10 | import java.util.TimeZone; 11 | 12 | /** 13 | * 14 | * @Description: 日期工具类 15 | * @author Luxh 16 | * @date Nov 13, 2012 17 | * @version V1.0 18 | */ 19 | public class DateUtil { 20 | 21 | /** 22 | * get current date 23 | */ 24 | public static Date getCurrentDate() { 25 | return new Date(); 26 | } 27 | 28 | /** 29 | * serialVersionUID 30 | */ 31 | private static final long serialVersionUID = 1238226379012286690L; 32 | /** 33 | * AM/PM 34 | */ 35 | public static final String AM_PM = "a"; 36 | /** 37 | * 一个月里第几天 38 | */ 39 | public static final String DAY_IN_MONTH = "dd"; 40 | /** 41 | * 一年里第几天 42 | */ 43 | public static final String DAY_IN_YEAR = "DD"; 44 | /** 45 | * 一周里第几天(从Sunday开始) 46 | */ 47 | public static final String DAY_OF_WEEK = "EEEE"; 48 | /** 49 | * 以天为单位 50 | */ 51 | public static final int DIFF_DAY = Calendar.DAY_OF_MONTH; 52 | /** 53 | * 以小时为单位 54 | */ 55 | public static final int DIFF_HOUR = Calendar.HOUR_OF_DAY; 56 | /** 57 | * 以毫秒为单位 58 | */ 59 | public static final int DIFF_MILLSECOND = Calendar.MILLISECOND; 60 | /** 61 | * 以分钟为单位 62 | */ 63 | public static final int DIFF_MINUTE = Calendar.MINUTE; 64 | /** 65 | * 以月份为单位,按照每月30天计算 66 | */ 67 | public static final int DIFF_MONTH = Calendar.MONTH; 68 | /** 69 | * 以秒为单位 70 | */ 71 | public static final int DIFF_SECOND = Calendar.SECOND; 72 | /** 73 | * 以星期为单位,按照每星期7天计算 74 | */ 75 | public static final int DIFF_WEEK = Calendar.WEEK_OF_MONTH; 76 | /** 77 | * 以年为单位,按照每年365天计算 78 | */ 79 | public static final int DIFF_YEAR = Calendar.YEAR; 80 | /** 81 | * 半天内小时(0-11) 82 | */ 83 | public static final String HOUR_IN_APM = "KK"; 84 | /** 85 | * 一天内小时(0-23) 86 | */ 87 | public static final String HOUR_IN_DAY = "HH"; 88 | /** 89 | * 半天内小时(1-12) 90 | */ 91 | public static final String HOUR_OF_APM = "hh"; 92 | /** 93 | * 一天内小时(1-24) 94 | */ 95 | public static final String HOUR_OF_DAY = "kk"; 96 | 97 | /** 98 | * 年(四位) 99 | */ 100 | public static final String LONG_YEAR = "yyyy"; 101 | /** 102 | * 毫秒 103 | */ 104 | public static final String MILL_SECOND = "SSS"; 105 | /** 106 | * 分钟 107 | */ 108 | public static final String MINUTE = "mm"; 109 | /** 110 | * 月 111 | */ 112 | public static final String MONTH = "MM"; 113 | /** 114 | * 秒 115 | */ 116 | public static final String SECOND = "ss"; 117 | /** 118 | * 年(二位) 119 | */ 120 | public static final String SHORT_YEAR = "yy"; 121 | /** 122 | * 一个月里第几周 123 | */ 124 | public static final String WEEK_IN_MONTH = "W"; 125 | /** 126 | * 一年里第几周 127 | */ 128 | public static final String WEEK_IN_YEAR = "ww"; 129 | 130 | public static final String DATE_FORMAT = "yyyy-MM-dd"; 131 | 132 | /** 133 | * 日期格式 134 | */ 135 | private static final String[] PARSE_PATTERNS = { 136 | "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy-MM", 137 | "yyyy/MM/dd", "yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm", "yyyy/MM", 138 | "yyyy.MM.dd", "yyyy.MM.dd HH:mm:ss", "yyyy.MM.dd HH:mm", "yyyy.MM" 139 | }; 140 | /** 141 | * 检查目的时间是否已超过源时间值加上时间段长度 142 | *

143 | * 用于判别当前是否已经超时 144 | * 145 | * @param destDate 目的时间,一般为当前时间 146 | * @param sourceDate 源时间,一般为事件产生时间 147 | * @param type 时间计算单位,为分钟、小时等 148 | * @param elapse 持续时间长度 149 | * @return 是否超时 150 | * @throws RuntimeException 151 | */ 152 | public static boolean compareElapsedTime( 153 | Date destDate, 154 | Date sourceDate, 155 | int type, 156 | int elapse) 157 | throws RuntimeException { 158 | if (destDate == null || sourceDate == null) 159 | throw new RuntimeException("compared date invalid"); 160 | 161 | return destDate.getTime() > getRelativeDate(sourceDate, type, elapse).getTime(); 162 | } 163 | 164 | /** 165 | * 取当前时间字符串 166 | *

167 | * 时间字符串格式为:年(4位)-月份(2位)-日期(2位) 小时(2位):分钟(2位):秒(2位) 168 | * @return 时间字符串 169 | */ 170 | public static String getCurrentDateString() { 171 | return getCurrentDateString("yyyy-MM-dd HH:mm:ss"); 172 | } 173 | 174 | /** 175 | * 按格式取当前时间字符串 176 | *

177 | * @param formatString 格式字符串 178 | * @return 179 | */ 180 | public static String getCurrentDateString(String formatString) { 181 | Date currentDate = new Date(); 182 | 183 | return getDateString(currentDate, formatString); 184 | } 185 | 186 | /** 187 | * 日期型字符串转化为日期 格式 188 | * { "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", 189 | * "yyyy/MM/dd", "yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm", 190 | * "yyyy.MM.dd", "yyyy.MM.dd HH:mm:ss", "yyyy.MM.dd HH:mm" } 191 | * 192 | * @param str the str 193 | * @return the date 194 | */ 195 | public static Date parseDate(Object str) { 196 | if (str == null) { 197 | return null; 198 | } 199 | try { 200 | return org.apache.commons.lang3.time.DateUtils.parseDate(str.toString(), PARSE_PATTERNS); 201 | } catch (ParseException e) { 202 | return null; 203 | } 204 | } 205 | 206 | /** 207 | * 取当天在一周的第几天 208 | *

209 | * @return int CurrentDayOfWeek 210 | */ 211 | public static int getCurrentDayOfWeek() { 212 | return getDayOfWeek(new Date()); 213 | } 214 | 215 | public static Date getDate(Date date) { 216 | return getDateFromString(getDateString(date, "yyyy-MM-dd"), "yyyy-MM-dd"); 217 | } 218 | 219 | /** 220 | * 根据时间字符串生成时间 221 | * 222 | * @param dateString 时间字符串格式 223 | * @return 时间 224 | * @throws RuntimeException 225 | */ 226 | public static Date getDateFromString(String dateString) 227 | throws RuntimeException { 228 | return getDateFromString(dateString, "yyyy-MM-dd HH:mm:ss"); 229 | } 230 | 231 | /** 232 | * 根据时间字符串生成时间 233 | * 234 | * @param dateString 时间字符串 格式 yyyy-MM-dd 235 | * @return 时间 236 | * @throws RuntimeException 237 | */ 238 | public static Date getDateFromString1(String dateString) 239 | throws RuntimeException { 240 | return getDateFromString(dateString, "yyyy-MM-dd"); 241 | } 242 | /** 243 | * 字符转换为日期。 244 | * 245 | * @param source 246 | * @param patterns日期格式串如yyyy 247 | * -MM-dd HH:mm:ss 248 | * @return 249 | */ 250 | public static Date stringToDate(String source, String patterns) { 251 | return stringToDate(source, patterns, true); 252 | } 253 | /** 254 | * 字符转换为日期。 255 | * 256 | * @param source 257 | * @param patterns日期格式串如yyyy 258 | * -MM-dd HH:mm:ss 259 | * @param locate 260 | * true--转化为东八区时间 261 | * @return 262 | */ 263 | public static Date stringToDate(String source, String patterns, 264 | boolean locate) { 265 | if (locate) 266 | return stringToDate(source, patterns, "GMT+8"); 267 | else 268 | return stringToDate(source, patterns, ""); 269 | } 270 | /** 271 | * 字符串转换为指定时区时间 272 | * 273 | * @param source 274 | * @param patterns 275 | * @param timeZone如东八区GMT 276 | * +8 277 | * @return 278 | */ 279 | public static Date stringToDate(String source, String patterns, 280 | String timeZone) { 281 | SimpleDateFormat dateFormat = new SimpleDateFormat(patterns); 282 | Date date = null; 283 | if (source == null) 284 | return date; 285 | if (timeZone != null && !timeZone.trim().equals("")) 286 | dateFormat.setTimeZone(TimeZone.getTimeZone(timeZone)); 287 | try { 288 | date = dateFormat.parse(source); 289 | } catch (java.text.ParseException e) { 290 | System.out.println("[string to date]" + e.getMessage()); 291 | } 292 | 293 | return date; 294 | } 295 | /** 296 | * 根据字符串生成时间 297 | * 298 | * @param dateString 时间字符串 299 | * @param pattern 时间字符串格式定义 300 | * @return 时间 301 | * @throws RuntimeException 302 | */ 303 | public static Date getDateFromString(String dateString, String pattern) 304 | throws RuntimeException { 305 | SimpleDateFormat dateFormat = new SimpleDateFormat(pattern); 306 | Date date = null; 307 | try { 308 | date = dateFormat.parse(dateString); 309 | } catch (java.text.ParseException e) { 310 | throw new RuntimeException( 311 | "parse date string '" 312 | + dateString 313 | + "' with pattern '" 314 | + pattern 315 | + "' failed: " 316 | + e.getMessage()); 317 | } 318 | 319 | return date; 320 | } 321 | 322 | /** 323 | * 取时间字符串 324 | * 325 | * @param date 时间 326 | * @return 时间字符串 327 | */ 328 | public static String getDateString(Date date) { 329 | return getDateString(date, "yyyy-MM-dd"); 330 | } 331 | 332 | /** 333 | * 取时间字符串 334 | * 335 | * @param date 时间 336 | * @param formatString 转换格式 337 | * @return 时间字符串 338 | */ 339 | public static String getDateString(Date date, String formatString) { 340 | return getDateString(date, formatString, Locale.PRC); 341 | } 342 | 343 | /** 344 | * 取时间字符串 345 | * 346 | * @param date 时间 347 | * @param formatString 转换格式 348 | * @param locale 地区 349 | * @return 时间字符串 350 | */ 351 | public static String getDateString(Date date, String formatString, Locale locale) { 352 | if (date == null) 353 | return null; 354 | 355 | SimpleDateFormat dateFormat = new SimpleDateFormat(formatString, locale); 356 | 357 | return dateFormat.format(date); 358 | } 359 | 360 | 361 | /** 362 | * 取日期在一周的第几天 363 | * 364 | * @param date 日期 365 | * @return 366 | */ 367 | public static int getDayOfWeek(Date date) { 368 | Calendar calendar = Calendar.getInstance(); 369 | calendar.setTime(date); 370 | 371 | return calendar.get(Calendar.DAY_OF_WEEK); 372 | } 373 | /** 374 | * 取日期在一周的星期几 375 | * 376 | * @param date 日期 377 | * @return 378 | */ 379 | public static String getDayOfWeekStr(String date) { 380 | int weeks=getDayOfWeek(getDateFromString1(date)); 381 | String weekStr=""; 382 | switch (weeks) { 383 | case 1:weekStr="星期日";break; 384 | case 2:weekStr="星期一";break; 385 | case 3:weekStr="星期二";break; 386 | case 4:weekStr="星期三";break; 387 | case 5:weekStr="星期四";break; 388 | case 6:weekStr="星期五";break; 389 | case 7:weekStr="星期六";break; 390 | } 391 | return weekStr; 392 | 393 | } 394 | 395 | /** 396 | * 取日期在一周的星期几 397 | * 398 | * @param date 日期 399 | * @return 400 | */ 401 | public static String getDayOfWeekStr(Date date) { 402 | int weeks=DateUtil.getDayOfWeek(date); 403 | String weekStr=""; 404 | switch (weeks) { 405 | case 1:weekStr="周日";break; 406 | case 2:weekStr="周一";break; 407 | case 3:weekStr="周二";break; 408 | case 4:weekStr="周三";break; 409 | case 5:weekStr="周四";break; 410 | case 6:weekStr="周五";break; 411 | case 7:weekStr="周六";break; 412 | } 413 | return weekStr; 414 | 415 | } 416 | /** 417 | * 取日期在一月的第几天 418 | * 419 | * @param date 日期 420 | * @return 421 | */ 422 | public static int getDayOfMonth(Date date) { 423 | Calendar calendar = Calendar.getInstance(); 424 | calendar.setTime(date); 425 | 426 | return calendar.get(Calendar.DAY_OF_MONTH); 427 | } 428 | 429 | /** 430 | * 取一个月的最大天数 431 | * 432 | * @param date 日期 433 | * @return 434 | */ 435 | public static int getDaysOfMonth(Date date) { 436 | Calendar calendar = Calendar.getInstance(); 437 | calendar.setTime(date); 438 | 439 | return calendar.getActualMaximum(Calendar.DAY_OF_MONTH); 440 | } 441 | 442 | /** 443 | * 取日期所在月份的最大天数 444 | * 445 | * @param date 日期 446 | * @return 447 | */ 448 | public static int getMaximumDay(Date date) { 449 | Calendar calendar = Calendar.getInstance(); 450 | calendar.setTime(date); 451 | 452 | return calendar.getMaximum(Calendar.DAY_OF_MONTH); 453 | } 454 | 455 | /** 456 | * 根据源时间和时长计算目的时间 457 | * 458 | * @param date 源时间 459 | * @param type 时间单位 460 | * @param relate 时长 461 | * @return 目的时间 462 | */ 463 | public static Date getRelativeDate(Date date, int type, int relate) { 464 | Calendar calendar = Calendar.getInstance(); 465 | calendar.setTime(date); 466 | calendar.add(type, relate); 467 | 468 | return calendar.getTime(); 469 | } 470 | 471 | /** 472 | * 根据当前时间和时长计算目的时间 473 | * 474 | * @param type 时间单位 475 | * @param relate 时长 476 | * @return 目的时间 477 | */ 478 | public static Date getRelativeDate(int type, int relate) { 479 | Date current = new Date(); 480 | 481 | return getRelativeDate(current, type, relate); 482 | } 483 | 484 | /** 485 | * 根据当前时间和时长生成目的时间字符串 486 | * 487 | * @param type 时间单位 488 | * @param relate 时长 489 | * @param formatString 时间格式 490 | * @return 时间字符串 491 | */ 492 | public static String getRelativeDateString( 493 | int type, 494 | int relate, 495 | String formatString) { 496 | return getDateString(getRelativeDate(type, relate), formatString); 497 | } 498 | 499 | /** 500 | * 取时间戳字符串 501 | * 502 | * @param date 时间 503 | * @return 时间戳字符串 504 | */ 505 | public static String getTimestampString(Date date) { 506 | return getDateString(date, "yyyyMMddHHmmssSSS"); 507 | } 508 | 509 | /** 510 | * 取当天日期值 511 | * 512 | * @return 日期的整数值 513 | */ 514 | public static int getToday() { 515 | return Integer.parseInt(getCurrentDateString("dd")); 516 | } 517 | 518 | public static long getTimeDiff(Date fromDate, Date toDate, int type) { 519 | fromDate = (fromDate == null) ? new Date() : fromDate; 520 | toDate = (toDate == null) ? new Date() : toDate; 521 | long diff = toDate.getTime() - fromDate.getTime(); 522 | 523 | switch(type) { 524 | case DIFF_MILLSECOND: 525 | break; 526 | 527 | case DIFF_SECOND: 528 | diff /= 1000; 529 | break; 530 | 531 | case DIFF_MINUTE: 532 | diff /= 1000 * 60; 533 | break; 534 | 535 | case DIFF_HOUR: 536 | diff /= 1000 * 60 * 60; 537 | break; 538 | 539 | case DIFF_DAY: 540 | diff /= 1000 * 60 * 60 * 24; 541 | break; 542 | 543 | case DIFF_MONTH: 544 | diff /= 1000 * 60 * 60 * 24 * 30; 545 | break; 546 | 547 | case DIFF_YEAR: 548 | diff /= 1000 * 60 * 60 * 24 * 365; 549 | break; 550 | 551 | default: 552 | diff = 0; 553 | break; 554 | } 555 | 556 | return diff; 557 | } 558 | 559 | /** 560 | * 比较时间戳是否相同 561 | * 562 | * @param arg0 时间 563 | * @param arg1 时间 564 | * @return 是否相同 565 | */ 566 | public static boolean isTimestampEqual(Date arg0, Date arg1) { 567 | return getTimestampString(arg0).compareTo(getTimestampString(arg1)) == 0; 568 | } 569 | 570 | /** 571 | * 判断给定日期是否超过参照时间 572 | *
573 | * @param srcTime 准备操作处理的日期 574 | * @param refTime 作为标准的参考日期 575 | * @return boolean 576 | */ 577 | public static boolean isTimestampPassed(Date srcTime,Date refTime){ 578 | boolean isPassed; 579 | int flag = srcTime.compareTo(refTime); 580 | if(flag >= 0){ 581 | isPassed = true; 582 | }else{ 583 | isPassed = false; 584 | } 585 | return isPassed; 586 | } 587 | 588 | /** 589 | * 将java.sql.Date时间装换为java.util.Date时间 590 | * @return java.util.Date 591 | */ 592 | public static java.util.Date getUtilDate(java.sql.Timestamp timestamp){ 593 | if(timestamp==null){ 594 | return null; 595 | } 596 | java.util.Date utilDate = new java.util.Date(timestamp.getTime()); 597 | return utilDate; 598 | } 599 | 600 | /** 601 | * 将java.util.Date时间装换为java.sql.Date时间 602 | * @param utilDate java.util.Date 603 | * @return java.sql.Date yyyy-MM-dd格式的日期,不带时间 604 | */ 605 | public static java.sql.Date getSQLDate(java.util.Date utilDate){ 606 | java.sql.Date sqlDate = new java.sql.Date(utilDate.getTime()); 607 | return sqlDate; 608 | } 609 | 610 | /** 611 | * 将java.util.Date转换为java.sql.Timestamp时间 612 | * @param utilDate java.util.Date 613 | * @return java.sql.Timestamp 614 | */ 615 | public static java.sql.Timestamp getSQLTimeStamp(java.util.Date utilDate){ 616 | if(null == utilDate || "".equals(utilDate)){ 617 | return null; 618 | }else{ 619 | java.sql.Timestamp timestamp = new java.sql.Timestamp(utilDate.getTime()); 620 | return timestamp; 621 | } 622 | } 623 | public static List getDays(int i) { 624 | List dates = new ArrayList(); 625 | //SimpleDateFormat format = new SimpleDateFormat("MM月dd日 E", Locale.CHINA); 626 | SimpleDateFormat format1 = new SimpleDateFormat("MM月dd日", Locale.CHINA); 627 | Calendar calendar = Calendar.getInstance(Locale.CHINA); 628 | for (int j = 0; j < i; j++) { 629 | String str = ""; 630 | if (j == 0) { 631 | str = "今天 "; 632 | dates.add(new String[] { str+format1.format(calendar.getTime()).toString(), timeToString2(calendar.getTime()).toString() }); 633 | } else { 634 | dates.add(new String[] { format1.format(calendar.getTime()).toString(), timeToString2(calendar.getTime()).toString() }); 635 | } 636 | calendar.add(Calendar.DATE, 1); 637 | } 638 | //测试用 639 | //dates.add(new String[]{"今天","2013-10-17"}); 640 | //dates.add(new String[]{"10月18日","2013-10-18"}); 641 | //dates.add(new String[]{"10月19日","2013-10-19"}); 642 | //dates.add(new String[]{"10月20日","2013-10-20"}); 643 | return dates; 644 | } 645 | public static String timeToString2(Date date) { 646 | SimpleDateFormat simpleDateFormat = new SimpleDateFormat(); 647 | simpleDateFormat.applyPattern("yyyy-MM-dd"); 648 | String str = null; 649 | if (date == null) 650 | return null; 651 | str = simpleDateFormat.format(date); 652 | return str; 653 | } 654 | /** 655 | * 获得指定日期的前一天 656 | * 657 | * @param specifiedDay 658 | * @return 659 | * @throws Exception 660 | */ 661 | public static String getSpecifiedDayBefore(String specifiedDay) { 662 | Calendar c = Calendar.getInstance(); 663 | Date date = null; 664 | try { 665 | date = new SimpleDateFormat("yy-MM-dd").parse(specifiedDay); 666 | } catch (java.text.ParseException e) { 667 | // TODO Auto-generated catch block 668 | e.printStackTrace(); 669 | } 670 | c.setTime(date); 671 | int day = c.get(Calendar.DATE); 672 | c.set(Calendar.DATE, day - 1); 673 | 674 | String dayBefore = new SimpleDateFormat("yyyy-MM-dd").format(c 675 | .getTime()); 676 | return dayBefore; 677 | } 678 | 679 | /** 680 | * 获得指定日期的后一天 681 | * 682 | * @param specifiedDay 683 | * @return 684 | */ 685 | public static String getSpecifiedDayAfter(String specifiedDay) { 686 | Calendar c = Calendar.getInstance(); 687 | Date date = null; 688 | try { 689 | date = new SimpleDateFormat("yy-MM-dd").parse(specifiedDay); 690 | } catch (java.text.ParseException e) { 691 | // TODO Auto-generated catch block 692 | e.printStackTrace(); 693 | } 694 | c.setTime(date); 695 | int day = c.get(Calendar.DATE); 696 | c.set(Calendar.DATE, day + 1); 697 | 698 | String dayAfter = new SimpleDateFormat("yyyy-MM-dd") 699 | .format(c.getTime()); 700 | return dayAfter; 701 | } 702 | /** 703 | * 计算两个日期的时间差 704 | * @param formatTime1 705 | * @param formatTime2 706 | * @return 707 | */ 708 | public static String getTimeDifference(Date formatTime1, Date formatTime2) { 709 | SimpleDateFormat timeformat = new SimpleDateFormat("yyyy-MM-dd,HH:mm:ss"); 710 | long t1 = 0L; 711 | long t2 = 0L; 712 | try { 713 | t1 = timeformat.parse(getTimeStampNumberFormat(formatTime1)).getTime(); 714 | } catch (java.text.ParseException e) { 715 | // TODO Auto-generated catch block 716 | e.printStackTrace(); 717 | } 718 | try { 719 | t2 = timeformat.parse(getTimeStampNumberFormat(formatTime2)).getTime(); 720 | } catch (java.text.ParseException e) { 721 | // TODO Auto-generated catch block 722 | e.printStackTrace(); 723 | } 724 | //因为t1-t2得到的是毫秒级,所以要初3600000得出小时.算天数或秒同理 725 | int hours=(int) ((t1 - t2)/3600000); 726 | int minutes=(int) (((t1 - t2)/1000-hours*3600)/60); 727 | int second=(int) ((t1 - t2)/1000-hours*3600-minutes*60); 728 | long ms= (long)((t1 - t2)/1000-hours*3600-minutes*60*1000); 729 | return ""+hours+"小时"+minutes+"分"+second+"秒"+ms+"毫秒"; 730 | } 731 | 732 | 733 | /** 734 | * 计算两个日期的时间差(毫秒) 735 | * @param formatTime1 736 | * @param formatTime2 737 | * @return 738 | */ 739 | public static long getTimeDifferenceMS(Date formatTime1, Date formatTime2) { 740 | SimpleDateFormat timeformat = new SimpleDateFormat("yyyy-MM-dd,HH:mm:ss"); 741 | long t1 = 0L; 742 | long t2 = 0L; 743 | try { 744 | t1 = timeformat.parse(getTimeStampNumberFormat(formatTime1)).getTime(); 745 | } catch (java.text.ParseException e) { 746 | // TODO Auto-generated catch block 747 | e.printStackTrace(); 748 | } 749 | try { 750 | t2 = timeformat.parse(getTimeStampNumberFormat(formatTime2)).getTime(); 751 | } catch (java.text.ParseException e) { 752 | // TODO Auto-generated catch block 753 | e.printStackTrace(); 754 | } 755 | 756 | long ms= t1 - t2; 757 | return ms; 758 | } 759 | 760 | 761 | 762 | /** 763 | * 格式化时间 764 | * Locale是设置语言敏感操作 765 | * @param formatTime 766 | * @return 767 | */ 768 | public static String getTimeStampNumberFormat(Date formatTime) { 769 | SimpleDateFormat m_format = new SimpleDateFormat("yyyy-MM-dd,HH:mm:ss", new Locale("zh", "cn")); 770 | return m_format.format(formatTime); 771 | } 772 | 773 | /** 774 | * 对应获取时间 775 | * @param iParam 776 | * @return 777 | */ 778 | public static String getAfterNDayStr(int iParam) {// iParam天后日期 779 | Calendar now = Calendar.getInstance(); 780 | now.add(Calendar.DAY_OF_YEAR, iParam); 781 | String sMonth = (now.get(Calendar.MONTH) + 1) > 9 ? "" 782 | + (now.get(Calendar.MONTH) + 1) : "0" 783 | + (now.get(Calendar.MONTH) + 1); 784 | String sDay = now.get(Calendar.DATE) > 9 ? "" + now.get(Calendar.DATE) 785 | : "0" + now.get(Calendar.DATE); 786 | String sHH = now.get(Calendar.HOUR_OF_DAY) > 9 ? "" 787 | + now.get(Calendar.HOUR_OF_DAY) : "0" 788 | + now.get(Calendar.HOUR_OF_DAY); 789 | String sMM = now.get(Calendar.MINUTE) > 9 ? "" 790 | + now.get(Calendar.MINUTE) : "0" + now.get(Calendar.MINUTE); 791 | String sAfterNDay = now.get(Calendar.YEAR) + "-" + sMonth + "-" + sDay 792 | + "," + sHH + sMM; 793 | return sAfterNDay; 794 | } 795 | 796 | /**当前时间+n天 797 | * @param n 798 | * @return 799 | */ 800 | public static String getDateByNowDate(int n){ 801 | String moreDate = ""; 802 | Date date = new Date(); 803 | Calendar cal = Calendar.getInstance(); 804 | cal.setTime(date); 805 | cal.add(Calendar.DATE,n); 806 | moreDate =(new SimpleDateFormat("yyyy-MM-dd")).format(cal.getTime()); 807 | return moreDate; 808 | } 809 | 810 | /**当前月份 811 | * @param n 812 | * @return 813 | */ 814 | public static int getCurrMonthe(){ 815 | Date date = new Date(); 816 | int currMonthe=date.getMonth()+1; 817 | return currMonthe; 818 | } 819 | 820 | /** 821 | * 获取时间戳 822 | * @param date 823 | * @return 824 | */ 825 | public static long getDateTimestamp(Date date){ 826 | Long time = date.getTime(); 827 | // String times = String.valueOf(time); 828 | // times = times.substring(0, 10); 829 | // time = Long.valueOf(times); 830 | return time; 831 | } 832 | 833 | /** 834 | * 获取时间戳 835 | * @param date 836 | * @return 837 | */ 838 | public static long getDateTimestamp2(Date date){ 839 | Long time = date.getTime(); 840 | String times = String.valueOf(time); 841 | times = times.substring(0, 10); 842 | time = Long.valueOf(times); 843 | return time; 844 | } 845 | 846 | /** 847 | * 根据时间戳获取日期 848 | * @param timestamp 849 | * @return 850 | */ 851 | public static Date getDateBylong(Long timestamp){ 852 | SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 853 | long lcc_time = Long.valueOf(timestamp); 854 | String re_StrTime = sdf.format(new Date(lcc_time * 1000L)); 855 | return getDateFromString(re_StrTime); 856 | } 857 | 858 | /** 859 | * 转换为时间(天,时:分:秒.毫秒) 860 | * @param timeMillis 861 | * @return 862 | */ 863 | public static String formatDateTime(long timeMillis){ 864 | long day = timeMillis/(24*60*60*1000); 865 | long hour = (timeMillis/(60*60*1000)-day*24); 866 | long min = ((timeMillis/(60*1000))-day*24*60-hour*60); 867 | long s = (timeMillis/1000-day*24*60*60-hour*60*60-min*60); 868 | long sss = (timeMillis-day*24*60*60*1000-hour*60*60*1000-min*60*1000-s*1000); 869 | return (day>0?day+",":"")+hour+":"+min+":"+s+"."+sss; 870 | } 871 | 872 | /** 873 | * 获取日期起始时间 874 | * @param date 875 | * @return 876 | */ 877 | public static Date getDayStartTime(Date date) { 878 | Calendar dayStart = Calendar.getInstance(); 879 | dayStart.setTime(date); 880 | dayStart.set(Calendar.HOUR, 0); 881 | dayStart.set(Calendar.MINUTE, 0); 882 | dayStart.set(Calendar.SECOND, 0); 883 | dayStart.set(Calendar.MILLISECOND, 0); 884 | return dayStart.getTime(); 885 | } 886 | 887 | /** 888 | * 获取日期起始时间 889 | * @param date 890 | * @return 891 | */ 892 | public static Date getDayStartTimeBySecond(Date date) { 893 | Calendar dayStart = Calendar.getInstance(); 894 | dayStart.setTime(date); 895 | dayStart.set(Calendar.SECOND, 0); 896 | dayStart.set(Calendar.MILLISECOND, 0); 897 | return dayStart.getTime(); 898 | } 899 | 900 | /** 901 | * 获取日期终结时间 902 | * @param date 903 | * @return 904 | */ 905 | public static Date getDayEndTime(Date date) { 906 | Calendar dayEnd = Calendar.getInstance(); 907 | dayEnd.setTime(date); 908 | dayEnd.set(Calendar.HOUR, 23); 909 | dayEnd.set(Calendar.MINUTE, 59); 910 | dayEnd.set(Calendar.SECOND, 59); 911 | dayEnd.set(Calendar.MILLISECOND, 999); 912 | return dayEnd.getTime(); 913 | } 914 | 915 | public static Date getDayEndTimeBySecond(Date date) { 916 | Calendar dayEnd = Calendar.getInstance(); 917 | dayEnd.setTime(date); 918 | dayEnd.set(Calendar.SECOND, 59); 919 | //dayEnd.set(Calendar.MILLISECOND, 999); 920 | return dayEnd.getTime(); 921 | } 922 | 923 | public static Date getWeekStartDate(Date date){ 924 | Calendar cal = Calendar.getInstance(); 925 | cal.setTime(date); 926 | cal.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY); 927 | cal.set(Calendar.HOUR_OF_DAY, 0); 928 | cal.set(Calendar.MINUTE, 0); 929 | cal.set(Calendar.SECOND, 0); 930 | cal.set(Calendar.MILLISECOND,0); 931 | return cal.getTime(); 932 | } 933 | 934 | public static Date getMoonStartDate(Date date){ 935 | Calendar cal = Calendar.getInstance(); 936 | cal.setTime(date); 937 | cal.set(Calendar.DATE, 1); 938 | cal.set(Calendar.HOUR_OF_DAY, 0); 939 | cal.set(Calendar.MINUTE, 0); 940 | cal.set(Calendar.SECOND, 0); 941 | cal.set(Calendar.MILLISECOND,0); 942 | return cal.getTime(); 943 | } 944 | 945 | } 946 | 947 | -------------------------------------------------------------------------------- /.factorypath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | --------------------------------------------------------------------------------