
一个程序员,欢迎骚扰!!!
能力说明:
了解变量作用域、Java类的结构,能够创建带main方法可执行的java应用,从命令行运行java程序;能够使用Java基本数据类型、运算符和控制结构、数组、循环结构书写和运行简单的Java程序。
暂时未有相关云产品技术能力~
阿里云技能认证
详细说明各位小伙伴们久等了,撸主花了无数个深夜吐血训练了100万小黄图做了一个鉴黄图床,终于在今天开放给大家了。2019年11月22日图床上线了,网友们也都很积极,甚是踊跃的上传了不少有趣的图片,当然由于一些特殊原因被过滤掉了,没能展示给各位网友。 也有不少小伙伴在后台问,图床什么时候开源呀?鉴黄是怎么做的啊?小黄图素材是从哪里找的啊?这个......恕无可奉告。后面也写了一系列文章来阐述相关功能,然而当时只是个半成品,一时半会放出来也不大合适,就拖到了现在。 此后的重点已经不是图床那么简单的功能了,后面撸主陆陆续续整合了不少功能模块,以方便大家能更加快速高效的开发。 前一阵子撸主上线了爪哇妹,一个集万千宠爱于一身的小程序。不过以后,爪哇妹可能要下线了,为了方便大家秒速预览,撸主接入了阿里云存储,奈何流量真的有点耗不起。爪哇妹里这么多妹子都是通过小程序后台管理模块添加的。 当然了,扩展不仅仅如此,目前项目分为组织机构、系统监控、应用管理、系统管理。 组织机构分为:机构管理、用户管理、角色管理、行政区域,一个比较通用的组件,各位小伙伴们可以通过它改造成一个后台管理系统。 系统监控目前只上线了系统日志和在线用户,后期会慢慢追加完善。 应用管理上线了任务调度、邮件管理、图片管理、文章管理,每个模块只需要你稍作修改就可以打造成一个项目了。 系统管理上线了敏捷开发、系统菜单、全局配置,小伙伴们只需要设计好表结构,三秒中就能撸出一个增删查改的模块,是不是特别爽,这样就能腾出手来提升自己。 好了,不多说了,放几张图吧。 推荐阅读 深夜吐血训练了100万小黄图撸了一个鉴黄接口 UCloud 云服务内容鉴黄 Java 版本实现 分享一款炒鸡好用的网盘+文件服务器 SpringBoot 2.x 开发案例之妹子图接入 Redis 缓存 SpringBoot 2.x 开发案例之 Shiro 整合 Redis SpringBoot 2.x 开发案例之优雅的处理异常 SpringBoot 2.0 开发案例之整合FastDFS分布式文件系统 源码 https://gitee.com/52itstyle/spring-boot-tools
前言 在之前的图床开发中撸主曾使用了分布式文件服务FASTDFS和阿里云的OSS对象存储来存储妹子图。奈何OSS太贵,FASTDFS搭建配置又太繁琐,今天给大家推荐一款极易上手的高性能对象存储服务MinIO 。 简介 MinIO 是高性能的对象存储,兼容 Amazon S3 接口,充分考虑开发人员的需求和体验;支持分布式存储,具备高扩展性、高可用性;部署简单但功能丰富。官方的文档也很详细。它有多种不同的部署模式(单机部署,分布式部署)。 为什么说 MinIO 简单易用,原因就在于它的启动、运行和配置都很简单。可以通过 docker 方式进行安装运行,也可以下载二进制文件,然后使用脚本运行。 安装 推荐使用 docker 一键安装: docker run -it -p 9000:9000 --name minio \ -d --restart=always \ -e "MINIO_ACCESS_KEY=admin" \ -e "MINIO_SECRET_KEY=admin123456" \ -v /mnt/minio/data:/data \ -v /mnt/minio/config:/root/.minio \ minio/minio server /data 注意: 密钥必须大于8位,否则会创建失败 文件目录和配置文件一定要映射到主机,你懂得 整合Nginx: server{ listen 80; server_name minio.cloudbed.vip; location /{ proxy_set_header Host $http_host; proxy_pass http://localhost:9000; } location ~ /\.ht { deny all; } } 这样,通过浏览器访问配置的地址,使用指定的 MINIO_ACCESS_KEY 及 MINIO_SECRET_KEY 登录即可。 简单看了一下,功能还算可以,支持创建Bucket,文件上传、删除、分享、下载,同时可以对Bucket设置读写权限。 整合 Minio支持接入JavaScript、Java、Python、Golang等多种语言,这里我们选择最熟悉的Java语言,使用最流行的框架 SpringBoot 2.x。 pom.xml引入: <dependency> <groupId>io.minio</groupId> <artifactId>minio</artifactId> <version>7.0.2</version> </dependency> application.properties引入: # MinIo文件服务器 min.io.endpoint = http://minio.cloudbed.vip min.io.accessKey = admin min.io.secretKey = admin123456 MinIoProperties.java 配置实体: /** * 实体类 * 爪哇笔记:https://blog.52itstyle.vip */ @Data @ConfigurationProperties(prefix = "min.io") public class MinIoProperties { private String endpoint; private String accessKey; private String secretKey; } 撸一个工具类: /** * 工具类 * 爪哇笔记:https://blog.52itstyle.vip */ @Component @Configuration @EnableConfigurationProperties({MinIoProperties.class}) public class MinIoUtils { private MinIoProperties minIo; public MinIoUtils(MinIoProperties minIo) { this.minIo = minIo; } private MinioClient instance; @PostConstruct public void init() { try { instance = new MinioClient(minIo.getEndpoint(),minIo.getAccessKey(),minIo.getSecretKey()); } catch (InvalidPortException e) { e.printStackTrace(); } catch (InvalidEndpointException e) { e.printStackTrace(); } } /** * 判断 bucket是否存在 * @param bucketName * @return */ public boolean bucketExists(String bucketName){ try { return instance.bucketExists(bucketName); } catch (Exception e) { e.printStackTrace(); } return false; } /** * 创建 bucket * @param bucketName */ public void makeBucket(String bucketName){ try { boolean isExist = instance.bucketExists(bucketName); if(!isExist) { instance.makeBucket(bucketName); } } catch (Exception e) { e.printStackTrace(); } } /** * 文件上传 * @param bucketName * @param objectName * @param filename */ public void putObject(String bucketName, String objectName, String filename){ try { instance.putObject(bucketName,objectName,filename,null); } catch (Exception e) { e.printStackTrace(); } } /** * 文件上传 * @param bucketName * @param objectName * @param stream */ public void putObject(String bucketName, String objectName, InputStream stream){ try { instance.putObject(bucketName,objectName,stream,null); } catch (Exception e) { e.printStackTrace(); } } /** * 删除文件 * @param bucketName * @param objectName */ public void removeObject(String bucketName, String objectName){ try { instance.removeObject(bucketName,objectName); } catch (Exception e) { e.printStackTrace(); } } //省略各种CRUD } 目前SDK不支持文件夹的创建,如果想创建文件夹,只能通过文件的方式上传并创建。 minIoUtils.putObject("itstyle","妹子图/爪哇妹.jpg","C:\\爪哇妹.jpg"); 一个实例只能有一个账号,如果想使用多个账号,需要创建多个实例。此外 minio还支持单主机,多块磁盘以及分布式部署,不过对于大部分单体应用来说,单体已经够用了。 小结 撸主在夜深人静的时候花了半个多小时就搞定了,是不是很简单,一键傻瓜式安装,丰富的SDK可供选择,小白用户是不是美滋滋。 重要的是她不仅可以作为文件服务,还可以当做私人网盘使用,一举两得岂不美滋滋。 源码 https://gitee.com/52itstyle/spring-boot-tools
前言 作为靠双手吃饭的广大程序猿媛们,大家基本都是从数据库的增删改查一步一步过来的,每天都有写不完的代码,好不容易写完了,又会因为改了需求,为了能完工不得不加班写这些简单并且耗时的代码。 那么问题来了,我们可不可以去掉这些繁琐的步骤,把时间更多的放在提升自己的能力上,而不是每天只是做些简单重复繁琐的工作。 推荐 今天撸主给大家推荐一款神器Spring Data REST,基于Spring Data的Repository之上,可以把 Repository 自动输出为REST资源,目前支持Spring Data JPA、Spring Data MongoDB、Spring Data Neo4j、Spring Data GemFire、Spring Data Cassandra的 repository 自动转换成REST服务。 案例 开发环境 Maven JDK1.8 SpringBoot 2.2.6 spring-boot-starter-data-jpa spring-boot-starter-data-rest 为了测试方便,这里我们使用h2内存数据库lombok插件,pom.xml引入: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-rest</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> application.properties 配置文件: # 定制根路径 spring.data.rest.base-path= /api spring.application.name=restful # 应用服务web访问端口 server.port=8080 定义用户实体类: /** * 实体类 * https://blog.52itstyle.vip */ @Data @Entity public class User { /** * 用户id */ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "user_id", nullable = false, length = 20) private Long userId; /** * 用户名 */ @Column(name = "username", nullable = false, length = 50) private String username; /** * 密码 */ @Column(name = "password", nullable = false, length = 50) private String password; /** * 姓名(昵称) */ @Column(name = "nickname", length = 50) private String nickname; /** * 邮箱 */ @Column(name = "email", length = 100) private String email; /** * 手机号 */ @Column(name = "mobile", length = 100) private String mobile; } 定义 Repository,不需要写一个接口: @RepositoryRestResource(collectionResourceRel = "user", path = "user") public interface UserRepository extends JpaRepository<User, Long> { } 启动项目,撸主默认初始化了几个用户。启动成功后,访问地址:http://localhost:8080/api如果出现以下提示,说明配置成功: { "_links" : { "user" : { "href" : "http://localhost:8080/api/user{?page,size,sort}", "templated" : true }, "profile" : { "href" : "http://localhost:8080/api/profile" } } } 获取单个用户: http://localhost:8080/api/user/2 分页查询: http://localhost:8080/api/user?page=0&size=10 更多API: POST请求新增用户 http://ip:port/api/user PUT请求更新id为1的用户 http://ip:port/api/user/1 DELETE请求删除id为1的用户 http://ip:port/api/user/1 如果以上满足不了,我们还可以自定义各种查询: @RepositoryRestResource(collectionResourceRel = "user", path = "user") public interface UserRepository extends JpaRepository<User, Long> { @RestResource(path = "nickname", rel = "nickname") List<User> findByNickname(@Param("nickname") String nickname); } 查询请求: http://ip:port/api/user/search/nickname?nickname=张三 小结 撸主觉得,这玩意撸一些简单的项目还是完全可以的,如果是复杂的业务逻辑可能吼不住,还需要自己进行进一步的封装处理。 案例 https://gitee.com/52itstyle/restful
前言 现在基本上各种手机APP注册都会用到手机验证码,包括一些PC端网站也会使用手机号作为唯一标识验证! 恰巧,小明的老板,让其开发一个用户注册的功能,并且强制用户注册绑定手机,美其名曰为了提升安全性,呵呵哒,就是为了多撸一点用户信息。 案例 一般来说,发送手机验证码不能过于频繁,前端发送按钮点击后一般会有一个60秒倒计时的功能。也就是说,如果用户点击发送一直没有收到验证码,只能60秒之后才可以进行重发。 那么问题来了,如果用户绕过前端,直接向后台API发送短信请求,然后写个无限循环脚本,相信不久你的短信账户就会发来预警提示短信(一般来说大的短信商都有预警设置功能)。 其实很简单,你只需要F12,查看发送请求就可以查找出后台请求地址,然后你可以在控制台输入相关JS代码,执行个十万遍,是不是很爽? 这里以七牛云为测试案例,打开注册页面,F12进入调试模式,输入手机号,手动点击发送,获取其短信发送后台请求地址。下面是七牛云的一个短信发送请求,撸主测试了一下,显然没有达到撸主的预期,毕竟是大厂,防御措施还是做的很牛逼的。 以下是JS脚本,复制粘贴到控制台回车就可以执行: var data = {"operation":1,"is_voice":false,"mobile_number":"17762018888","captcha_type":2}; for (var i = 0; i < 10; i++) { $.ajax({ type: 'POST', contentType: 'application/json;charset=UTF-8', data:JSON.stringify(data), url: 'https://portal.qiniu.com/api/gaea/verification/sms/send', success: function(data) { console.log(data) } }); } 控制台返回以下信息,前三次请求成功,后面的就出现了验证码校验并进行了限流操作。 {"code":200,"message":""} {"code":200,"message":""} {"code":200,"message":""} {"code": 7209,"message":"captcha required"} {"code": 7209,"message":"captcha required"} {"code": 429,"message":"too many requests"} {"code": 429,"message":"too many requests"} {"code": 429,"message":"too many requests"} {"code": 429,"message":"too many requests"} {"code": 7209,"message":"captcha required"} 撸主尝试刷新页面,随便输了一个手机号,再次点击发送,提示用户输入验证码,显然是加强了防备,触发了恶意请求认证拦截机制。 安全机制 对于开发者来说,他们不仅要考虑用户正常获取验证码的体验还要考虑短信接口的安全性,撸主总结了以下几点,希望对大家有所帮助。 后台请求限流,对单位时间内发送频率做限制。 验证码机制,切记不要一开始就限制验证码,体验及其不友好,触发限流以后开启验证码校验。 监控日发送短信数量,触发一定的阈值做相应的处理,根据实际业务需求。 验证码存储一定要保证key为手机号,切记不要以其它标识作为key,比如sessionId。 一定要设置验证码失效时间,比如五分钟,或者更短。 验证码尽量保证短小精悍,四到六位即可。 如果后台不做限制,切记前台一定要做个倒计时的限制,至少过滤一部分小白用户。 代码案例 给小伙伴分享一个简单的验证码生成、存储、失效代码案例: import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; public class Mobile { /** * 测试方便,这里设置了3秒失效 */ private static LoadingCache<String, String> caches = CacheBuilder.newBuilder() .maximumSize(1000) .expireAfterWrite(3, TimeUnit.SECONDS) .build(new CacheLoader<String, String>() { @Override public String load(String mobile) { return ""; } }); public static void main(String[] args) throws ExecutionException, InterruptedException { Integer code = (int)((Math.random()*9+1)*100000); caches.put("17762018888",code.toString()); System.out.println(caches.get("17762018888")); Thread.sleep(4000); System.out.println("是不是没了:"+caches.get("17762018888")); } } 小结 重要的功能必须进行前后端校验,必要的时候一定要做好限流、黑名单等骚操作!!! 作者: 小柒 出处: https://blog.52itstyle.vip
如今Java14已经发布许久了,Java15也在路上跑着了,然鹅不少小伙伴Java8的API应该还没用溜吧!今天跟各位小伙伴们聊聊Java Stream API的具体使用方法以及应用场景。 举哥简单的例子,定义一个数组: String[] users = new String[]{"张三","李四","王二"}; 我们来使用之前的方式遍历: for(int i=0;i<users.length;i++){ System.out.println(users[i]); } 亦或者使用: for(String u:users){ System.out.println(u); } 使用最新的API实现方式: Stream<String> stream = Stream.of(users); stream.forEach(user-> System.out.println(user)); 然鹅好像并没有多么方便,仅仅少撸了一行代码,下面跟大家分享一个稍微复杂一点的例子。 首先定义一个学生类: @Data @Builder public class Student { private String name; private Integer age; private Integer sex; private Double score; } 定义基础数据: List<Student> list = new ArrayList<>(); list.add(new Student("张三",28,1,78.9)); list.add(new Student("李四",20,1,98.0)); list.add(new Student("王二",29,0,100.0)); list.add(new Student("张三",28,1,78.5)); Stream<Student> stream = list.stream(); 遍历: stream.forEach(stu-> System.out.println(stu.getName())); 过滤性别: stream.filter(stu -> stu.getSex()==1) .forEach(stu -> System.out.println(stu.getName())); 名字去重: stream.distinct().forEach(stu -> System.out.println(stu.getName())); 年龄排序: stream.sorted(Comparator.comparingInt(Student::getAge)) .forEach(stu -> System.out.println(stu.getName())); 排序反转: stream.sorted(Comparator.comparingInt(Student::getAge).reversed()) .forEach(stu -> System.out.println(stu.getName())); 姓名集合: List<String> userList = stream.map(stu -> stu.getName()) .collect(Collectors.toList()); 分数求和: Optional<Double> totalScore = stream.map(Student::getScore) .reduce((x, y) -> x+y); System.out.println(totalScore.get()); 求分数最大值: Optional<Double> maxScore = stream.map(Student::getScore).reduce(Double::max); 求分数最小值: Optional<Double> minScore = stream.map(Student::getScore).reduce(Double::min); 取前三名同学: stream.sorted(Comparator.comparingDouble(Student::getScore).reversed()) .limit(3) .forEach(System.out::println); 扔了前三名同学: stream.sorted(Comparator.comparingDouble(Student::getScore).reversed()) .skip(3) .forEach(System.out::println); 卧槽太多了,撸主举不过来了,但是基本上常用的就这么几个。函数式编程写出的代码更加简洁且意图明确,使用stream接口让你从此告别for循环,小伙伴们赶紧入坑享受吧!!! 作者: 小柒 出处: https://blog.52itstyle.vip
前言 开发这么多年,肯定还有不少小伙伴搞不清各种类型的参数是如何传递的,很多同学都是拿来即用,复制粘贴一把撸,遇到问题还是一脸懵逼。 姿势 学习参数传递的正确姿势,先说怎么做,再说为什么,本质上还是复制粘贴一把撸,问题是你想问不想问为什么! 传递 用户登录 前端代码: var param = { "username": "admin", "password": "admin" } $.ajax({ url: "/sys/login", data: param, type: "post", dataType: "json", success: function(data) { } }); 后端代码: @RestController @RequestMapping("/sys") public class LoginController { private static final Logger logger = LoggerFactory.getLogger(LoginController.class); /** * 登录 */ @PostMapping("/login") public Result login(String username, String password){ logger.info("用户登录"+username); //业务逻辑 return Result.ok("登录成功"); } } 当然,你也可以这么实现,@RequestParam(value="username", required=true) ,required 默认为 true,如果前台不传递此参数,后台会报错。如果设置为 false,如果不传,默认为 null。 /** * 登录 * https://blog.52itstyle.vip */ @PostMapping("/login") public Result login(@RequestParam(value="username", required=true) String username, @RequestParam(value="password", required=true) String password){ logger.info("用户登录"+username); //业务逻辑 return Result.ok("登录成功"); } 用户注册 前端代码,提交方式与登录基本保持一致。 后端代码: 用一个对象来接收前台参数,一般后端有对应的实体类。 /** * 注册 * https://blog.52itstyle.vip */ @PostMapping("/register") public Result register(SysUser user){ logger.info("{},用户注册",user.getUsername()); //业务逻辑 return Result.ok("注册成功"); } 多参数无实体一 前端代码: var param = { "title": "爪哇笔记", "content": "一个有趣的公众号", "author": "小柒2012" } param = JSON.stringify(param); $.ajax({ url: "/sys/multiParameter", data: param, type: "post", contentType: "application/json", dataType: "json", success: function(data) { } }); 后端实现: /** * 多参数 * https://blog.52itstyle.vip */ @PostMapping("/multiParameter") public Result register(@RequestBody Map<String,Object> map){ logger.info("多参数传递:{},{}",map.get("title"),map.get("content")); //业务逻辑 return Result.ok("接收多参数成功"); } 多参数无实体二 前端代码: var param = { "title": "爪哇笔记", "content": "一个有趣的公众号", "author": "小柒2012" } $.ajax({ url: "/sys/multiParameter", data: param, type: "post", dataType: "json", success: function(data) { } }); 后端实现: /** * 多参数 * https://blog.52itstyle.vip */ @PostMapping("/multiParameter") public Result register(@RequestParam Map<String,Object> map){ logger.info("多参数传递:{},{}",map.get("title"),map.get("content")); //业务逻辑 return Result.ok("接收多参数成功"); } 传递数组 前端代码: var param = { "ids": [1, 2, 3] } $.ajax({ url: "/sys/array", data: param, type: "post", dataType: "json", success: function(data) { } }); 后端实现: /** * 数组 * https://blog.52itstyle.vip */ @PostMapping("array") public Result array(@RequestParam(value = "ids[]") Integer[] ids) { logger.info("数据{}", Arrays.asList(ids)); //业务逻辑 return Result.ok(); } 传递集合 前端代码与传递数组保持一致。 后端实现: /** * 集合 * https://blog.52itstyle.vip */ @PostMapping("array") public Result array(@RequestParam(value = "ids[]") List<Integer> ids) { logger.info("数据{}", ids.toString()); //业务逻辑 return Result.ok(); } 传递集合实体对象 比如,后端想接收一个实体对象集合 List<SysUser> 前端代码: var list = []; list.push({ "username": "小柒2012", "mobile": "17762288888" }); list.push({ "username": "小柒2013", "mobile": "17762289999" }); $.ajax({ url: "/sys/listUser", data: JSON.stringify(list), type: "post", contentType: "application/json", dataType: "json", success: function(data) { } }); 后端代码: /** * 爪哇笔记 * https://blog.52itstyle.vip */ @PostMapping("listUser") public Result listUser(@RequestBody List<SysUser> list) { logger.info("数据{}", list.size()); list.forEach(user->{ //输出实体对象 System.out.println(user.getUsername()); }); //业务逻辑 return Result.ok(); } 传递集合实体对象一对多 比如,一个用户有多个角色 List<SysRole> roleList 前端代码: var roleList = []; roleList.push({ "roleSign": "admin", "roleName": "管理员" }); roleList.push({ "roleSign": "user", "roleName": "普通用户" }); var list = []; var user = { "username": "小柒2012", "mobile": "17762288888" }; user.roleList = roleList; list.push(user); $.ajax({ url: "/sys/listUserRole", data: JSON.stringify(list), type: "post", contentType: "application/json", dataType: "json", success: function(data) { } }); 后端实现: /** * 爪哇笔记 * https://blog.52itstyle.vip */ @PostMapping("listUserRole") public Result listUserRole(@RequestBody List<SysUser> list) { logger.info("数据{}", list.size()); list.forEach(user->{ List<SysRole> roleList = user.getRoleList(); roleList.forEach(role->{ System.out.println(role.getRoleName()); }); }); return Result.ok(); } 炒鸡复杂 传输对象有实体,有集合,有各种类型的数据,这时候最简单的方式就是传递 Key-Value 结构的 JSON 字符串,后台 Map 类型接收,然后通过FastJson的 JSON.parseObject() 和 JSON.parseArray() 方法转化为对应的实体或者集合。 String user = parseMap.get("user").toString(); SysUser sysUser = JSON.parseObject(user,SysUser.class); String contractClause = parseMap.get("rules").toString(); List<Rule> ruleList = JSON.parseArray(contractClause,Rule.class); RESTful 风格 比如,访问某篇文章: /** * 爪哇笔记 * https://blog.52itstyle.vip */ @GetMapping("article/{id}") public void article(@PathVariable("id") String id) { logger.info("文章{}",id); //业务逻辑 } 原则 记住一下几点: @RequestBody 注解,必须与 contentType 类型application/json配合使用。 @RequestParam 注解,必须与 contentType 类型application/x-www-form-urlencoded配合使用,其为默认类型。 JSON.stringify() 把对象类型转换为字符串类型,一般配合 @RequestBody 注解和contentType 类型application/json使用。 扩展 在以上只涉及了两种 contentType 类型,其实还有两种常见的类型: multipart/form-data 一般用于表单文件上传,必须让 form 的 enctype 等于这个值。 <form action="/upload" method="post" enctype="multipart/form-data"> <input type="text" name="description" value="爪哇笔记,一个神奇的公众号"> <input type="file" name="myFile"> <button type="submit">Submit</button> </form> text/xml 做过微信支付的小伙伴一定会知道,微信就喜欢用这种方式,去年还发生过 XXE 漏洞,在解析XML文档时,解析器通过 ENTITY 扩展的功能,读取本地受保护的文件,并且使用扩展功能将受保护的文件发送到远程地址。 小结 不敢说是最完整的传参方案,但绝对敢保证是最正确的,因为所有的传参方式都经过 360° 官方检验。
前言 最近几天,好几个小伙伴在后台询问,改造后的 sentinel-dashboard 什么时候开源。讲真,不是不想给大家放出来,是因为一些地方还没有完善好,怕误导了大家,在经过了一个星期业余时间的努力,终于把基础版本搞定了。小伙伴们终于可以进行拉取测试了。 历程 首先回顾一下改造之路: SpringBoot 2.0 + Sentinel 动态限流实战 SpringBoot 2.0 + Nacos + Sentinel 流控规则集中存储 SpringBoot 2.0 + InfluxDB+ Sentinel 实时监控数据存储 阿里巴巴 Sentinel + InfluxDB + Chronograf 实现监控大屏 最终架构 持续学习 sentinel 的学习已经告一段落,后面会持续学InfluxDB,它是目前比较流行的时间序列数据库。 那么什么是时间序列数据库?最简单的定义就是数据格式里包含Timestamp字段的数据,比如某一时间环境的温度、湿度,以及机器的CPU的使用率等等。 随着物联网的发展,作为一名程序员,时序数据库是必不可少的一项必备技能。所以,在开源 sentinel-dashboard 项目下,会持续提交一些 InfluxDB 的学习笔记以及使用场景,有兴趣的小伙伴可以一起加入进来。 源码 https://gitee.com/52itstyle/sentinel-dashboard
前言 在上一篇推文中,我们使用时序数据库 InfluxDb 做了流控数据存储,但是数据存储不是目的,分析监控预警才是最终目标,那么问题来了,如何更好的实现呢?用过阿里巴巴 Sentinel 控制台的小伙伴,是不是觉得它的控制台丑爆了,而且只有短短的五厘米,显然不能满足大部分人或者场景的使用。 架构 工具 sentinel-dashboard(控制台,收集数据) Influxdb(时序数据库,存储数据) Chronograf (展示控制台,显示数据并实现预警) 安装 Sentinel 控制台 和 时序数据库 Influxdb 的安装方式前面已经聊过,这里不再赘述,简单说下 Chronograf 展示控制台的安装方式,这里推荐使用 Docker 安装方式。 $ docker run -p 8888:8888 \ -v $PWD:/var/lib/chronograf \ chronograf 安装成功以后,浏览器访问 http://ip:8888 你应该看到一个欢迎页面: 然后,自行配置数据源,根据业务场景组装监控大屏。 大屏 这里根据 Sentinel 限流组件采集的数据,组装了一个简单的监控大屏,可以监控历史访问总量、最近一小时的访问量、限流数以及最近几分钟或者几小时的访问曲线等等,相比于阿里演示版是不是瞬间高大上的些许。 总访问量 SELECT SUM("successQps") AS "总访问量" FROM "sentinel_log"."autogen"."sentinelInfo" 最近一小时访问量 SELECT SUM("successQps") AS "访问量" FROM "sentinel_log"."autogen"."sentinelInfo" WHERE TIME > NOW() - 1h 最近一小时限流数 SELECT SUM("blockQps") AS "限流数" FROM "sentinel_log"."autogen"."sentinelInfo" WHERE time > now() - 1h 最近一小时异常数 SELECT SUM("exceptionQps") AS "异常数" FROM "sentinel_log"."autogen"."sentinelInfo" WHERE time > now() - 1h 最近一小时的访问趋势图(秒级别) SELECT SUM("successQps") AS "访问量" FROM "sentinel_log"."autogen"."sentinelInfo" WHERE time > now() - 1h GROUP BY time(1s) 最近12小时资源访问排名 SELECT SUM("successQps") AS "成功qps", SUM("blockQps") AS "限流qps" FROM "sentinel_log"."autogen"."sentinelInfo" WHERE time > now() - 12h GROUP BY resource 预警 后期我们在 Chronograf 中接入 Kapacitor ,Chronograf会自动打开该Configure Alert Endpoints部分,Kapacitor支持多个警报端点/事件处理程序。有兴趣的小伙伴也可以在 Sentinel 控制台中根据流控数据进行更智能化的设置,比如根据限流失败数以及机器指标动态调整流控规则。 小结 有了她,小哥哥、小姐姐们再也不用担心凌晨一点的闹钟了,是不是很爽?以上只是冰山一角,目前我们上线的监控系统平台,通过各种第三方组件库(Telegraf、InfluxDB、Chronograf、Kapacitor、Grafana、Prometheus、Consul、Elasticsearch、Kibana),接入了 1000 台服务器实时监控,200个监控大屏,上千个监控指标,每日处理成吨的数据。是不是很吊!?我唧唧都佩服我自己的想象力。 源码 https://gitee.com/52itstyle/sentinel-dashboard 参考 https://hub.docker.com/_/chronograf
前言 阿里巴巴提供的控制台只是用于演示 Sentinel 的基本能力和工作流程,并没有依赖生产环境中所必需的组件,比如持久化的后端数据库、可靠的配置中心等。目前 Sentinel 采用内存态的方式存储监控和规则数据,监控最长存储时间为 5 分钟,控制台重启后数据丢失。 企业版 这里推荐一下阿里云的官方版,AHAS Sentinel 控制台 是 Sentinel 控制台的阿里云上版本,提供企业级的控制台服务,包括: 实时请求链路查看 还有各种酷炫的监控图表 可靠的实时监控和历史监控数据查询,无需自行存储、拉取 动态规则管理/推送,无需自行配置外部数据源 免费版,可以提供 5 个节点的免费额度。开通专业版即可享受不限量节点额度。 专业版没有实例连接限制,开通后每天前5个限流降级节点不计费,超出部分按3元/天/实例收取相应的费用。 思路 官方文档也提供了思路,若需要监控数据持久化的功能,可以自行扩展实现 MetricsRepository 接口(0.2.0 版本),然后注册成 Spring Bean 并在相应位置通过 @Qualifier 注解指定对应的 bean name 即可。MetricsRepository 接口定义了以下功能: save 与 saveAll:存储对应的监控数据 queryByAppAndResourceBetween:查询某段时间内的某个应用的某个资源的监控数据 listResourcesOfApp:查询某个应用下的所有资源 其中默认的监控数据类型为 MetricEntity,包含应用名称、时间戳、资源名称、异常数、请求通过数、请求拒绝数、平均响应时间等信息。 对于监控数据的存储,用户需要根据自己的存储精度,来考虑如何存储这些监控数据。显然我们要使用目前最流行的时序数据库InfluxDB解决方案,不要问什么?闭眼享受就可以了。 选型 InfluxDB是一个开源分布式时序、事件和指标数据库。使用 Go 语言编写,无需外部依赖。 应用:性能监控,应用程序指标,物联网传感器数据和实时分析等的后端存储。 强大的类SQL语法 内置http支持,使用http读写 基于事件:它支持任意的事件数据 无结构(无模式):可以是任意数量的列 可度量性:你可以实时对大量数据进行计算 持续高并发写入、无更新、数据压缩存储、低查询延时 支持min, max, sum, count, mean, median 等一系列函数 基于时间序列,支持与时间有关的相关函数(如最大,最小,求和等) 改造 InfluxDB 安装 首先你得先有个 Influxdb 数据库,建议使用 Docker 方式安装,更多可以参考文末链接。 需要注意的是,从1.1.0版开始不推荐使用管理员界面,并将在1.3.0版中删除。默认情况下禁用。如果需要,仍可以通过设置如下环境变量来启用它。 以下端口很重要,并由InfluxDB使用。 8086 HTTP API端口 8083 管理员界面端口(如果已启用,1.7.8貌似启用也不好使),官方推荐使用chronograf 通过该命令, 生成默认配置文件: docker run --rm influxdb influxd config > influxdb.conf 创建并运行容器: docker run -d \ -p 8086:8086 \ -p 8083:8083 \ -e INFLUXDB_ADMIN_ENABLED=true \ -v $PWD/data:/var/lib/influxdb/ \ -v $PWD/config/influxdb.conf:/etc/influxdb/influxdb.conf:ro \ --name influx \ influxdb -config /etc/influxdb/influxdb.conf 生产环境一定要开启权限验证,修改 influxdb.conf 配置: [http] enabled = true bind-address = ":8086" auth-enabled = true # 鉴权 创建用户: # 进入容器 docker exec -it influx /bin/sh # 连接 influx # 创建用户 CREATE USER admin with PASSWORD 'admin' WITH ALL PRIVILEGES 退出重新登录: # 用户密码登录 influx -username admin -password admin # 创建数据库 CREATE DATABASE sentinel_log Sentinel 控制台改造 pom.xml引入 influxdb 官方开源工具包: <dependency> <groupId>org.influxdb</groupId> <artifactId>influxdb-java</artifactId> <version>2.15</version> </dependency> 配置文件引入: # 自行替换 API 地址:端口 spring.influx.url=http://127.0.0.1:8086 spring.influx.user=admin spring.influx.password=admin spring.influx.database=sentinel_log 配置数据源: /** * InfluxDb 配置 * 创建者 爪哇笔记 * 网址 https://blog.52itstyle.vip */ @Configuration public class InfluxDbConfig { @Value("${spring.influx.url:''}") private String influxDBUrl; @Value("${spring.influx.user:''}") private String userName; @Value("${spring.influx.password:''}") private String password; @Value("${spring.influx.database:''}") private String database; @Bean public InfluxDB influxDB(){ InfluxDB influxDB = InfluxDBFactory.connect(influxDBUrl, userName, password); try { /** * 异步插入: * enableBatch这里第一个是point的个数,第二个是时间,单位毫秒 * point的个数和时间是联合使用的,如果满100条或者2000毫秒 * 满足任何一个条件就会发送一次写的请求。 */ influxDB.setDatabase(database) .enableBatch(100,2000, TimeUnit.MILLISECONDS); } catch (Exception e) { e.printStackTrace(); } finally { influxDB.setRetentionPolicy("autogen"); } influxDB.setLogLevel(InfluxDB.LogLevel.BASIC); return influxDB; } } 实现 MetricsRepository 接口,重写实现: /** * 数据 CURD * 创建者 爪哇笔记 * 网址 https://blog.52itstyle.vip */ @Component("inInfluxdbMetricsRepository") public class InInfluxdbMetricsRepository implements MetricsRepository<MetricEntity> { @Autowired public InfluxDB influxDB; @Override public synchronized void save(MetricEntity metric) { //省略代码,太长了,参考内存写法,参考 saveAll 这里是单条插入 } @Override public synchronized void saveAll(Iterable<MetricEntity> metrics) { if (metrics == null) { return; } BatchPoints batchPoints = BatchPoints.builder() .tag("async", "true") .consistency(InfluxDB.ConsistencyLevel.ALL) .build(); metrics.forEach(metric->{ Point point = Point .measurement("sentinelInfo") //这里使用微妙、如果还有覆盖数据就使用纳秒,保证 time 和 tag 唯一就可以 .time(System.currentTimeMillis(), TimeUnit.MICROSECONDS) .tag("app",metric.getApp())//tag 数据走索引 .addField("gmtCreate", metric.getGmtCreate().getTime()) .addField("gmtModified", metric.getGmtModified().getTime()) .addField("timestamp", metric.getTimestamp().getTime()) .addField("resource", metric.getResource()) .addField("passQps", metric.getPassQps()) .addField("successQps", metric.getSuccessQps()) .addField("blockQps", metric.getBlockQps()) .addField("exceptionQps", metric.getExceptionQps()) .addField("rt", metric.getRt()) .addField("count", metric.getCount()) .addField("resourceCode", metric.getResourceCode()) .build(); batchPoints.point(point); }); //批量插入 influxDB.write(batchPoints); } @Override public synchronized List<MetricEntity> queryByAppAndResourceBetween(String app, String resource, long startTime, long endTime) { //省略代码,太长了,参考内存写法 } @Override public synchronized List<String> listResourcesOfApp(String app) { //省略代码,太长了,参考内存写法 } } 分别修改 MetricFetcher 和 MetricController中 metricStore 的注入方式,使用 Influxdb 实现: /** * 注入 * 创建者 爪哇笔记 * 网址 https://blog.52itstyle.vip */ @Autowired @Qualifier("inInfluxdbMetricsRepository") private MetricsRepository<MetricEntity> metricStore; 配置完成后,我们重启控制台,然后访问客户端项目,如果控制台打印以下数据,说明配置成功: 2019-09-21 19:47:25 [sentinel-dashboard-metrics-fetchWorker-thread-2] INFO okhttp3.OkHttpClient - --> POST http://118.190.247.102:8086/write?db=sentinel_log&precision=n&consistency=all (486-byte body) 2019-09-21 19:47:25 [sentinel-dashboard-metrics-fetchWorker-thread-2] INFO okhttp3.OkHttpClient - <-- 204 No Content http://118.190.247.102:8086/write?db=sentinel_log&precision=n&consistency=all (46ms, 0-byte body) 多访问几次客户端项目,然后登陆控制台查看,出现以下效果,说明改造成功: 注意事项: 官方前端并没有实现按照时间范围的查询搜索,需要自行实现 官方控制台实时监控默认查询的是最近一分钟的热点资源排行,见方法 listResourcesOfApp 官方控制台实时监控右侧 Table 默认查询的是最近五分钟的热点访问详情,见方法 queryTopResourceMetric 小结 对于官方五分钟的阉割版,时序数据库实现的流控数据存储,对于生产环境还是很有帮助的,比如实时数据分析,热点资源、监控预警等等。小伙伴们还可以根据实际生产需求结合Chronograf、Grafana 做出更炫酷的大屏监控。 源码 https://gitee.com/52itstyle/sentinel-dashboard 参考 https://blog.52itstyle.vip/archives/4460/ https://hub.docker.com/_/influxdb https://hub.docker.com/_/chronograf https://github.com/influxdata/influxdb-java https://github.com/influxdata/influxdb-python https://help.aliyun.com/document_detail/97578.htm
前言 Sentinel 原生版本的规则管理通过API 将规则推送至客户端并直接更新到内存中,并不能直接用于生产环境。不过官方也提供了一种 Push模式,扩展读数据源ReadableDataSource,规则中心统一推送,客户端通过注册监听器的方式时刻监听变化,比如使用 Nacos、Zookeeper 等配置中心。这种方式有更好的实时性和一致性保证。这里我们通过配置 Nacos 来实现流控规则的统一存储配置。 架构 控制台推送规则至配置中心,客户端通过监听事件从配置中心获取流控规则。 客户端配置 pom.xml 引入: <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</artifactId> <version>1.6.3</version> </dependency> 配置文件: # nacos的访问地址,配置参考 https://blog.52itstyle.vip/archives/4174/ spring.cloud.sentinel.datasource.ds.nacos.server-addr=47.104.187.19:8848 #nacos中存储规则的dataId,对于dataId使用了${spring.application.name}变量,这样可以根据应用名来区分不同的规则配置 spring.cloud.sentinel.datasource.ds.nacos.dataId=${spring.application.name}-flow-rules #nacos中存储规则的groupId spring.cloud.sentinel.datasource.ds.nacos.groupId=SENTINEL_GROUP #定义存储的规则类型 spring.cloud.sentinel.datasource.ds.nacos.rule-type=flow 控制台配置 修改 pom.xml,原来的<scope>test</scope>去掉: <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</artifactId> </dependency> 把 src/test 下面的包 com.alibaba.csp.sentinel.dashboard.rule.nacos 拷贝到 src/main/java 下面。 修改 NacosConfig: /** * @author Eric Zhao * @since 1.4.0 */ @Configuration public class NacosConfig { @Value("${nacos.address}") private String address; @Bean public Converter<List<FlowRuleEntity>, String> flowRuleEntityEncoder() { return JSON::toJSONString; } @Bean public Converter<String, List<FlowRuleEntity>> flowRuleEntityDecoder() { return s -> JSON.parseArray(s, FlowRuleEntity.class); } @Bean public ConfigService nacosConfigService() throws Exception { Properties properties = new Properties(); properties.put("serverAddr",address); return ConfigFactory.createConfigService(properties); } } application.properties 配置引入 Nacos: # nacos的访问地址,配置参考 https://blog.52itstyle.vip/archives/4174/ nacos.address=47.104.197.19:8848 FlowControllerV2 指定对应的 Bean 开启 Nacos 适配。 @Autowired @Qualifier("flowRuleNacosProvider") private DynamicRuleProvider<List<FlowRuleEntity>> ruleProvider; @Autowired @Qualifier("flowRuleNacosPublisher") private DynamicRulePublisher<List<FlowRuleEntity>> rulePublisher; 修改sidebar.html页面, 流控规则路由从 dashboard.flowV1 改成 dashboard.flow <-- nacos 动态规则配置--> <li ui-sref-active="active" ng-if="!entry.isGateway"> <a ui-sref="dashboard.flow({app: entry.app})"> <i class="glyphicon glyphicon-filter"></i>&nbsp;&nbsp;流控规则</a> </li> 如图所示,界面会多了一个回到单机页面的按钮,这里我们新增一个流控规则。 登录 Nacos 后台,配置管理->配置列表: 点击进入配置详情,配置内容如下: [{ "app": "blog", "clusterConfig": { "fallbackToLocalWhenFail": true, "sampleCount": 10, "strategy": 0, "thresholdType": 0, "windowIntervalMs": 1000 }, "clusterMode": false, "controlBehavior": 0, "count": 2.0, "gmtCreate": 1568617671812, "gmtModified": 1568622253889, "grade": 1, "id": 6, "ip": "10.136.168.88", "limitApp": "default", "port": 8720, "resource": "blogView", "strategy": 0 }] 小结 生产环境下,推送规则正确做法应该是 配置中心控制台/Sentinel 控制台 → 配置中心 → Sentinel 数据源 → Sentinel。 案例 https://gitee.com/52itstyle/spring-boot-blog 参考 https://github.com/alibaba/Sentinel https://github.com/alibaba/Sentinel/tree/master/sentinel-dashboard
前言 在从0到1构建分布式秒杀系统和打造十万博文系统中,限流是不可缺少的一个环节,在系统能承受的范围内既能减少资源开销又能防御恶意攻击。 在前面的文章中,我们使用了开源工具包 Guava 提供的限流工具类 RateLimiter 和 OpenResty 的 Lua 脚本分别进行 API 和应用层面的限流。今天,我们来聊聊阿里开源的分布式系统的流量防卫兵 Sentinel。 Sentinel 是什么? 随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。 Sentinel 具有以下特征: 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。 完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。 Sentinel 的主要特性: Sentinel 的开源生态: Sentinel 分为两个部分: 核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。 控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。 控制台配置 Sentinel 控制台最少应该包含如下功能: 查看机器列表以及健康情况:收集 Sentinel 客户端发送的心跳包,用于判断机器是否在线。 监控 (单机和集群聚合):通过 Sentinel 客户端暴露的监控 API,定期拉取并且聚合应用监控信息,最终可以实现秒级的实时监控。 规则管理和推送:统一管理推送规则。 鉴权:生产环境中鉴权非常重要。这里每个开发者需要根据自己的实际情况进行定制。 可以直接从 release 页面 下载最新版本的控制台 jar 包,启动 Sentinel 控制台需要 JDK 版本为 1.8 及以上版本。。 启动脚本 sentinel.sh: #!/bin/bash java -Dsentinel.dashboard.auth.username=admin \ -Dsentinel.dashboard.auth.password=admin \ -Dserver.port=8084 -Dcsp.sentinel.dashboard.server=localhost:8084 \ -Dproject.name=sentinel-dashboard \ -jar sentinel-dashboard-1.6.3.jar & 用户可以通过如下参数进行配置: -Dsentinel.dashboard.auth.username=admin 用于指定控制台的登录用户名为 admin; -Dsentinel.dashboard.auth.password=admin 用于指定控制台的登录密码为 admin;如果省略这两个参数,默认用户和密码均为 sentinel; -Dserver.servlet.session.timeout=7200 用于指定 Spring Boot 服务端 session 的过期时间,如 7200 表示 7200 秒;60m 表示 60 分钟,默认为 30 分钟; 客户端配置 pom.xml 引入以下依赖: <!-- https://blog.52itstyle.vip --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.5.RELEASE</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency> </dependencies> <dependencyManagement> <!--注意跟 SpringBoot 保持一致 2.1.x for Spring Boot 2.1.x--> <dependencies> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2.1.0.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> 配置文件: # 应用名称 https://blog.52itstyle.vip spring.application.name=blog spring.cloud.sentinel.transport.port=8720 # 测试请替换为自己的地址 spring.cloud.sentinel.transport.dashboard=116.190.247.112:8084 这里的 spring.cloud.sentinel.transport.port 端口配置会在应用对应的机器上启动一个 Http Server,该 Server 会与 Sentinel 控制台做交互。比如 Sentinel 控制台添加了1个限流规则,会把规则数据 push 给这个 Http Server 接收,Http Server 再将规则注册到 Sentinel 中。 代码配置: /** * 博文 https://blog.52itstyle.vip */ @RequestMapping("{id}.shtml") @SentinelResource("blogView") public String page(@PathVariable("id") Long id, ModelMap model) { try{ Blog blog = blogService.getById(id); String key = "blog_"+id; Long views = redisUtil.size(key); blog.setViews(views+blog.getViews()); model.addAttribute("blog",blog); } catch (Throwable e) { return "error/404"; } return "article"; } @SentinelResource 注解用来标识资源是否被限流、降级。上述例子上该注解的属性 'blogView' 表示资源名。 默认情况,Sentinel 会拦截所有的 Controller 请求,这里标识资源名,是因为所有的文章都会走这个请求,为了方便统计和流控,这里自定义资源标识。 更多注解支持,请参考:Sentinel/wiki/注解支持。 访问客户端项目,随便点击几个页面,然后登录 Sentinel 控制台,如果看到以下界面,说明配置成功。 配置限流,搜索我们刚才配置的资源名称,选择流控功能。 输入阈值参数,为了测试方便,这里直接输入2,连续刷新浏览器,如果后台出现以下错误,并伴随着前台页面无法正常显示说明配置生效。 Caused by: com.alibaba.csp.sentinel.slots.block.flow.FlowException: null 当然,Sentinel 流程功能不仅仅这么简单,还支持集群模式,在终极版十万博文中,我们可以为集群中的节点,设置单机均分,也可以设置一个总体的阈值。 生产环境中使用 Sentinel 核心库目前已可用于生产环境,目前除了阿里巴巴以外,也有多家企业在生产环境中使用它们。 规则管理及推送 原生版本的规则管理通过API 将规则推送至客户端并直接更新到内存中,并不能直接用于生产环境。 不过 Sentinel提供了扩展读数据源ReadableDataSource,规则中心统一推送,客户端通过注册监听器的方式时刻监听变化,比如使用 Nacos、Zookeeper 等配置中心。这种方式有更好的实时性和一致性保证。 监控 Sentinel 会记录资源访问的秒级数据(若没有访问则不进行记录)并保存在本地日志中。Sentinel 控制台可以通过 Sentinel 客户端预留的 HTTP API 从秒级监控日志中拉取监控数据,并进行聚合。 目前 Sentinel 控制台中监控数据聚合后直接存在内存中,未进行持久化,且仅保留最近 5 分钟的监控数据。若需要监控数据持久化的功能,可以自行扩展实现。 注意事项 由于一开始没有认真读文档,把控制台部署到了外网,而客户端在内网启动,导致客户端无法被访问到,实时链路和簇点链路数据无法正常显示。 测试的小伙伴注意了,原始模式下,客户端和控制台必须相互被访问到,客户端会向控制台定时发送心跳请求,控制台会向客户端推送规则、拉取流控数据并聚合。 源码 https://gitee.com/52itstyle/spring-boot-blog 参考 https://github.com/alibaba/Sentinel
前言 在开发十万博客系统的的过程中,前面主要分享了爬虫、缓存穿透以及文章阅读量计数等等。爬虫的目的就是解决十万+问题;缓存穿透是为了保护后端数据库查询服务;计数服务解决了接近真实阅读数以及数据库服务的压力。 架构图 限流 就拿十万博客来说,如果存在热点文章,可能会有数十万级别的并发用户参与阅读。如果想让这些用户正常访问,无非就是加机器横向扩展各种服务,但凡事都有一个利益平衡点,有时候只需要少量的机器保证大部分用户在大部分时间可以正常访问即可。 亦或是,如果存在大量爬虫或者恶意攻击,我们必须采取一定的措施来保证服务的正常运行。这时候我们就要考虑限流来保证服务的可用性,以防止非预期的请求对系统压力过大而引起的系统瘫痪。通常的策略就是拒绝多余的访问,或者让多余的访问排队等待服务。 限流算法 任何限流都不是漫无目的的,也不是一个开关就可以解决的问题,常用的限流算法有:令牌桶,漏桶。 令牌桶 令牌桶算法是网络流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一种算法。典型情况下,令牌桶算法用来控制发送到网络上的数据的数目,并允许突发数据的发送(百科)。 用户的请求速率是不固定的,这里我们假定为10r/s,令牌按照5个每秒的速率放入令牌桶,桶中最多存放20个令牌。仔细想想,是不是总有那么一部分请求被丢弃。 漏桶 漏桶算法的主要目的是控制数据注入到网络的速率,平滑网络上的突发流量。漏桶算法提供了一种机制,通过它,突发流量可以被整形以便为网络提供一个稳定的流量(百科)。 令牌桶是无论你流入速率多大,我都按照既定的速率去处理,如果桶满则拒绝服务。 应用限流 Tomcat 在Tomcat容器中,我们可以通过自定义线程池,配置最大连接数,请求处理队列等参数来达到限流的目的。 Tomcat默认使用自带的连接池,这里我们也可以自定义实现,打开/conf/server.xml文件,在Connector之前配置一个线程池: <Executor name="tomcatThreadPool" namePrefix="tomcatThreadPool-" maxThreads="1000" maxIdleTime="300000" minSpareThreads="200"/> name:共享线程池的名字。这是Connector为了共享线程池要引用的名字,该名字必须唯一。默认值:None; namePrefix:在JVM上,每个运行线程都可以有一个name 字符串。这一属性为线程池中每个线程的name字符串设置了一个前缀,Tomcat将把线程号追加到这一前缀的后面。默认值:tomcat-exec-; maxThreads:该线程池可以容纳的最大线程数。默认值:200; maxIdleTime:在Tomcat关闭一个空闲线程之前,允许空闲线程持续的时间(以毫秒为单位)。只有当前活跃的线程数大于minSpareThread的值,才会关闭空闲线程。默认值:60000(一分钟)。 minSpareThreads:Tomcat应该始终打开的最小不活跃线程数。默认值:25。 配置Connector <Connector executor="tomcatThreadPool" port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" minProcessors="5" maxProcessors="75" acceptCount="1000"/> executor:表示使用该参数值对应的线程池; minProcessors:服务器启动时创建的处理请求的线程数; maxProcessors:最大可以创建的处理请求的线程数; acceptCount:指定当所有可以使用的处理请求的线程数都被使用时,可以放到处理队列中的请求数,超过这个数的请求将不予处理。 API限流 这里我们采用开源工具包guava提供的限流工具类RateLimiter进行API限流,该类基于"令牌桶算法",开箱即用。 自定义定义注解 /** * 自定义注解 限流 * 创建者 爪洼笔记 * 博客 https://blog.52itstyle.vip * 创建时间 2019年8月15日 */ @Target({ElementType.PARAMETER, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface ServiceLimit { /** * 描述 */ String description() default ""; /** * key */ String key() default ""; /** * 类型 */ LimitType limitType() default LimitType.CUSTOMER; enum LimitType { /** * 自定义key */ CUSTOMER, /** * 根据请求者IP */ IP } } 自定义切面 /** * 限流 AOP * 创建者 爪洼笔记 * 博客 https://blog.52itstyle.vip * 创建时间 2019年8月15日 */ @Aspect @Configuration @Order(1) public class LimitAspect{ //根据IP分不同的令牌桶, 每天自动清理缓存 private static LoadingCache<String, RateLimiter> caches = CacheBuilder.newBuilder() .maximumSize(1000) .expireAfterWrite(1, TimeUnit.DAYS) .build(new CacheLoader<String, RateLimiter>() { @Override public RateLimiter load(String key){ // 新的IP初始化 每秒只发出5个令牌 return RateLimiter.create(5); } }); //Service层切点 限流 @Pointcut("@annotation(com.itstyle.blog.common.limit.ServiceLimit)") public void ServiceAspect() { } @Around("ServiceAspect()") public Object around(ProceedingJoinPoint joinPoint) { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); ServiceLimit limitAnnotation = method.getAnnotation(ServiceLimit.class); ServiceLimit.LimitType limitType = limitAnnotation.limitType(); String key = limitAnnotation.key(); Object obj; try { if(limitType.equals(ServiceLimit.LimitType.IP)){ key = IPUtils.getIpAddr(); } RateLimiter rateLimiter = caches.get(key); Boolean flag = rateLimiter.tryAcquire(); if(flag){ obj = joinPoint.proceed(); }else{ throw new RrException("小同志,你访问的太频繁了"); } } catch (Throwable e) { throw new RrException("小同志,你访问的太频繁了"); } return obj; } } 业务实现: /** * 执行顺序 * 1)限流 * 2)布隆 * 3)计数 * 4) 缓存 * @param id * @return */ @Override @ServiceLimit(limitType= ServiceLimit.LimitType.IP) @BloomLimit @HyperLogLimit @Cacheable(cacheNames ="blog") public Blog getById(Long id) { String nativeSql = "SELECT * FROM blog WHERE id=?"; return dynamicQuery.nativeQuerySingleResult(Blog.class,nativeSql,new Object[]{id}); } 分布式限流 Nginx 如何使用Nginx实现基本的限流,比如单个IP限制每秒访问50次。通过Nginx限流模块,我们可以设置一旦并发连接数超过我们的设置,将返回503错误给客户端。 配置nginx.conf #统一在http域中进行配置 #限制请求 limit_req_zone $binary_remote_addr $uri zone=api_read:20m rate=50r/s; #按ip配置一个连接 zone limit_conn_zone $binary_remote_addr zone=perip_conn:10m; #按server配置一个连接 zone limit_conn_zone $server_name zone=perserver_conn:100m; server { listen 80; server_name blog.52itstyle.top; index index.jsp; location / { #请求限流排队通过 burst默认是0 limit_req zone=api_read burst=5; #连接数限制,每个IP并发请求为2 limit_conn perip_conn 2; #服务所限制的连接数(即限制了该server并发连接数量) limit_conn perserver_conn 1000; #连接限速 limit_rate 100k; proxy_pass http://seckill; } } upstream seckill { fair; server 172.16.1.120:8080 weight=1 max_fails=2 fail_timeout=30s; server 172.16.1.130:8080 weight=1 max_fails=2 fail_timeout=30s; } 配置说明 imit_conn_zone 是针对每个IP定义一个存储session状态的容器。这个示例中定义了一个100m的容器,按照32bytes/session,可以处理3200000个session。 limit_rate 300k; 对每个连接限速300k. 注意,这里是对连接限速,而不是对IP限速。如果一个IP允许两个并发连接,那么这个IP就是限速limit_rate×2。 burst=5; 这相当于桶的大小,如果某个请求超过了系统处理速度,会被放入桶中,等待被处理。如果桶满了,那么抱歉,请求直接返回503,客户端得到一个服务器忙的响应。如果系统处理请求的速度比较慢,桶里的请求也不能一直待在里面,如果超过一定时间,也是会被直接退回,返回服务器忙的响应。 OpenResty 这里我们使用 OpenResty 开源的限流方案,测试案例使用OpenResty1.15.8.1最新版本,自带lua-resty-limit-traffic模块以及案例 ,实现起来更为方便。 限制接口总并发数/请求数 热点博文,由于突发流量暴增,有可能会影响整个系统的稳定性从而造成崩溃,这时候我们就要限制热点博文的总并发数/请求数。 这里我们采用 lua-resty-limit-traffic中的resty.limit.count模块实现: -- 限制接口总并发数/请求数 local limit_count = require "resty.limit.count" -- 这里我们使用AB测试,-n访问10000次, -c并发1200个 -- ab -n 10000 -c 1200 http://121.42.155.213/ ,第一次测试数据:1000个请求会有差不多8801请求失败,符合以下配置说明 -- 限制 一分钟内只能调用 1200 次 接口(允许在时间段开始的时候一次性放过1200个请求) local lim, err = limit_count.new("my_limit_count_store", 1200, 60) if not lim then ngx.log(ngx.ERR, "failed to instantiate a resty.limit.count object: ", err) return ngx.exit(500) end -- use the Authorization header as the limiting key local key = ngx.req.get_headers()["Authorization"] or "public" local delay, err = lim:incoming(key, true) if not delay then if err == "rejected" then ngx.header["X-RateLimit-Limit"] = "5000" ngx.header["X-RateLimit-Remaining"] = 0 return ngx.exit(503) end ngx.log(ngx.ERR, "failed to limit count: ", err) return ngx.exit(500) end -- the 2nd return value holds the current remaining number -- of requests for the specified key. local remaining = err ngx.header["X-RateLimit-Limit"] = "5000" ngx.header["X-RateLimit-Remaining"] = remaining 限制接口时间窗请求数 现在网络爬虫泛滥,有时候并不是人为的去点击,亦或是存在恶意攻击的情况。此时我们就要对客户端单位时间内的请求数进行限制,以至于黑客不是那么猖獗。当然了道高一尺魔高一丈,攻击者总是会有办法绕开你的防线,从另一方面讲也促进了技术的进步。 这里我们采用 lua-resty-limit-traffic中的resty.limit.conn模块实现: -- well, we could put the require() and new() calls in our own Lua -- modules to save overhead. here we put them below just for -- convenience. local limit_conn = require "resty.limit.conn" -- 这里我们使用AB测试,-n访问1000次, -c并发100个 -- ab -n 1000 -c 100 http://121.42.155.213/ ,这里1000个请求将会有700个失败 -- 相同IP段的人将不能被访问,不影响其它IP -- 限制 IP 总请求数 -- 限制单个 ip 客户端最大 200 req/sec 并且允许100 req/sec的突发请求 -- 就是说我们会把200以上300一下的请求请求给延迟, 超过300的请求将会被拒绝 -- 最后一个参数其实是你要预估这些并发(或者说单个请求)要处理多久,可以通过的log_by_lua中的leaving()调用进行动态调整 local lim, err = limit_conn.new("my_limit_conn_store", 200, 100, 0.5) if not lim then ngx.log(ngx.ERR, "failed to instantiate a resty.limit.conn object: ", err) return ngx.exit(500) end -- the following call must be per-request. -- here we use the remote (IP) address as the limiting key -- commit 为true 代表要更新shared dict中key的值, -- false 代表只是查看当前请求要处理的延时情况和前面还未被处理的请求数 local key = ngx.var.binary_remote_addr local delay, err = lim:incoming(key, true) if not delay then if err == "rejected" then return ngx.exit(503) end ngx.log(ngx.ERR, "failed to limit req: ", err) return ngx.exit(500) end if lim:is_committed() then local ctx = ngx.ctx ctx.limit_conn = lim ctx.limit_conn_key = key ctx.limit_conn_delay = delay end -- the 2nd return value holds the current concurrency level -- for the specified key. local conn = err if delay >= 0.001 then -- 其实这里的 delay 肯定是上面说的并发处理时间的整数倍, -- 举个例子,每秒处理100并发,桶容量200个,当时同时来500个并发,则200个拒掉 -- 100个在被处理,然后200个进入桶中暂存,被暂存的这200个连接中,0-100个连接其实应该延后0.5秒处理, -- 101-200个则应该延后0.5*2=1秒处理(0.5是上面预估的并发处理时间) -- the request exceeding the 200 connections ratio but below -- 300 connections, so -- we intentionally delay it here a bit to conform to the -- 200 connection limit. -- ngx.log(ngx.WARN, "delaying") ngx.sleep(delay) end 平滑限制接口请求数 之前的限流方式允许突发流量,也就是说瞬时流量都会被允许。突然流量如果不加以限制会影响整个系统的稳定性,因此在秒杀场景中需要对请求整形为平均速率处理,即20r/s。 这里我们采用 lua-resty-limit-traffic 中的resty.limit.req 模块实现漏桶限流和令牌桶限流。 其实漏桶和令牌桶根本的区别就是,如何处理超过请求速率的请求。漏桶会把请求放入队列中去等待均速处理,队列满则拒绝服务;令牌桶在桶容量允许的情况下直接处理这些突发请求。 漏桶 桶容量大于零,并且是延迟模式。如果桶没满,则进入请求队列以固定速率等待处理,否则请求被拒绝。 令牌桶 桶容量大于零,并且是非延迟模式。如果桶中存在令牌,则允许突发流量,否则请求被拒绝。 压测 为了测试以上配置效果,我们采用AB压测,Linux下执行以下命令即可: # 安装 yum -y install httpd-tools # 查看ab版本 ab -v # 查看帮助 ab --help 测试命令: ab -n 1000 -c 100 http://127.0.0.1/ 测试结果: Server Software: openresty/1.15.8.1 #服务器软件 Server Hostname: 127.0.0.1 #IP Server Port: 80 #请求端口号 Document Path: / #文件路径 Document Length: 12 bytes #页面字节数 Concurrency Level: 100 #请求的并发数 Time taken for tests: 4.999 seconds #总访问时间 Complete requests: 1000 #总请求树 Failed requests: 0 #请求失败数量 Write errors: 0 Total transferred: 140000 bytes #请求总数据大小 HTML transferred: 12000 bytes #html页面实际总字节数 Requests per second: 200.06 [#/sec](mean) #每秒多少请求,这个是非常重要的参数数值,服务器的吞吐量 Time per request: 499.857 [ms](mean) #用户平均请求等待时间 Time per request: 4.999 [ms](mean, across all concurrent requests) # 服务器平均处理时间,也就是服务器吞吐量的倒数 Transfer rate: 27.35 [Kbytes/sec] received #每秒获取的数据长度 Connection Times (ms) min mean[+/-sd] median max Connect: 0 0 0.8 0 4 Processing: 5 474 89.1 500 501 Waiting: 2 474 89.2 500 501 Total: 9 475 88.4 500 501 Percentage of the requests served within a certain time (ms) 50% 500 66% 500 75% 500 80% 500 90% 501 95% 501 98% 501 99% 501 100% 501 (longest request) 源码 SpringBoot开发案例之打造十万博文Web篇 总结 以上限流方案,只是针对此次十万博文做一个简单的小结,大家也不要刻意区分那种方案的好坏,只要适合业务场景就是最好的。
前言 在分布式系统中,为了提升系统性能,通常会对单体项目进行拆分,分解成多个基于功能的微服务,如果有条件,可能还会对单个微服务进行水平扩展,保证服务高可用。 那么问题来了,如果使用传统管理 Session 的方式,我们会遇到什么样的问题? 案例 这里拿下单举例,用户小明在天猫上相中了一个的娃娃,觉得不错,果断购买,选尺寸,挑身高,然后确认选择,赶紧提交订单,然后就跳转到了登录页面!小明表示很郁闷,大写的问号??? 小明进入娃娃页面,此时请求通过代理服务发送到业务系统一。 小明选尺寸,挑身高,此操作并没有对后端服务发送请求。 小明提交订单,此时请求通过代理服务发送到业务系统二,然鹅,二系统此时并没有查询到小明的登录信息,就被无情的跳转到登录页了。 方案 HttpSession 默认使用内存来管理 Session,通常服务端把用户信息存储到各自的 Jvm 内存中。所以小明下单的时候找不到登录信息,那么我么何不把用户信息集中存储!? 为了测试效果,这里我们搭建一个演示案例,项目涉及 SpringBoot、spring-session、redis、nginx 等相关组件。 pom.xml引入依赖: <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> 配置 redis 参数,软件自行安装: ## redis #session存储类型 spring.session.store-type=redis spring.redis.database=0 spring.redis.host=127.0.0.1 spring.redis.port=6379 spring.redis.password=123456 spring.redis.pool.max-active=8 spring.redis.pool.max-wait=-1 spring.redis.pool.max-idle=8 spring.redis.pool.min-idle=0 spring.redis.timeout=3000 简单的用户登录实现,省略部分代码: @RequestMapping(value="login",method=RequestMethod.POST) public Result login(String username,String password,HttpServletRequest request,HttpServletResponse response) throws Exception { SysUser user = userService.getUser(username); if(user==null) { return Result.error("用户不存在"); }else { if(user.getPassword().equals(password)) { request.getSession().setAttribute("user", user); return Result.ok(); }else { return Result.error("密码错误"); } } } 配置代理实现,基于 Nginx: server { listen 80; server_name blog.52itstyle.vip; location / { proxy_pass http://192.168.1.2:8080; } location /cart { proxy_pass http://192.168.1.3:8080$request_uri; } location /order { proxy_pass http://192.168.1.4:8080$request_uri; } } 配置成功后登录系统,在 redis 中查询用户信息: 127.0.0.1:6379> keys * 1) "spring:session:expirations:1562577660000" 2) "spring:session:sessions:1076c2bd-95b1-4f23-abd4-ab3780e32f6f" 3) "spring:session:sessions:expires:1076c2bd-95b1-4f23-abd4-ab3780e32f6f" 小结 这样,小明就可以开心的买娃娃了!
前言 在开发过程中,通常我们会配置一些参数来实现某些功能,比如是否开启某项服务,告警邮件配置等等。一般会通过硬编码、配置文件或者数据库的形式实现。 那么问题来了,如何更加优雅的实现?欢迎来到 Nacos 的世界! Nacos 配置管理 Nacos 是阿里巴巴的开源的项目,全称 Naming Configuration Service ,专注于服务发现和配置管理领域。 Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。 Nacos 生态图 如 Nacos 全景图所示,Nacos 无缝支持一些主流的开源生态,例如 Spring Cloud Apache Dubbo and Dubbo Mesh TODO Kubernetes and CNCF TODO。 使用 Nacos 简化服务发现、配置管理、服务治理及管理的解决方案,让微服务的发现、管理、共享、组合更加容易。 Nacos Spring Boot 快速开始 这里以为 Spring-Boot2.x 为例: pom.xml引入依赖: <dependency> <groupId>com.alibaba.boot</groupId> <artifactId>nacos-config-spring-boot-starter</artifactId> <version>0.2.1</version> </dependency> 启动类: package com.itstyle.nacos; import com.alibaba.nacos.spring.context.annotation.config.NacosPropertySource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * 启动类 * 创建者 爪哇笔记 https://blog.52itstyle.vip * 创建时间 2019年7月14日 * dataId 可以根据自己的项目自定义 * autoRefreshed 是一个布尔值, Nacos 就会把最新的配置推送到该应用的所有机器上,简单而高效。 */ @SpringBootApplication @NacosPropertySource(dataId = "itstyle.blog", autoRefreshed = true) public class Application { private static final Logger logger = LoggerFactory.getLogger(Application.class); public static void main(String[] args){ SpringApplication.run(Application.class, args); logger.info("启动"); } 使用案例: package com.itstyle.nacos; import com.alibaba.nacos.api.config.annotation.NacosValue; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; /** * 创建者 爪哇笔记 https://blog.52itstyle.vip */ @Controller @RequestMapping(value = "config") public class NacosConfigController { @NacosValue(value = "${useLocalCache:false}", autoRefreshed = true) private boolean useLocalCache; @RequestMapping(value = "/get", method = RequestMethod.GET) @ResponseBody public boolean get() { return useLocalCache; } } 配置文件引入: # 安全机制,建议走内网、配置防火墙 nacos.config.server-addr=127.0.0.1:8848 服务端安装配置请参考: https://nacos.io/zh-cn/docs/quick-start.html 主页: dataId 一定要与系统配置保持一致,配置内容为键值对的方式。 实例化数据库 Nacos Server 默认使用的是内嵌的数据库,生产环境建议修改使用 mysql 数据库存储配置信息。 在配置文件application.properties添加配置: spring.datasource.platform=mysql db.num=1 db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true db.user=root db.password=root 创建数据库,在Nacos Server conf文件夹下,找到nacos-mysql.sql文件,导入创建的数据库即可。 Nacos默认账号密码为:nacos,修改密码需要使用引入: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> 然后使用代码加密: package com.itstyle.nacos; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; /** * 创建者 爪哇笔记 https://blog.52itstyle.vip */ public class PasswordEncoderUtil { public static void main(String[] args) { System.out.println(new BCryptPasswordEncoder().encode("nacos")); } } 小结 总的来说,Nacos 还是蛮方便的,配置中心也仅仅是它的一个小功能而已。 参考 https://nacos.io/en-us/
前言 在之前的 Dubbo 服务开发中,我们一般使用 Zookeeper 作为注册中心,同时还需要部署 Dubbo 监控中心和管理后台。 Nacos 注册中心 Nacos 是阿里巴巴的开源的项目,全称 Naming Configuration Service ,专注于服务发现和配置管理领域。 Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。 Nacos 生态图 如 Nacos 全景图所示,Nacos 无缝支持一些主流的开源生态,例如 Spring CloudApache Dubbo and Dubbo Mesh TODOKubernetes and CNCF TODO。使用 Nacos 简化服务发现、配置管理、服务治理及管理的解决方案,让微服务的发现、管理、共享、组合更加容易。 Nacos Spring Boot 快速开始 <!-- Dubbo Nacos registry dependency --> <dependency> <groupId>com.alibaba</groupId> <artifactId>dubbo-registry-nacos</artifactId> <version>2.6.7</version> </dependency> <!-- Dubbo dependency --> <dependency> <groupId>com.alibaba</groupId> <artifactId>dubbo</artifactId> <version>2.6.5</version> </dependency> <!-- Alibaba Spring Context extension --> <dependency> <groupId>com.alibaba.spring</groupId> <artifactId>spring-context-support</artifactId> <version>1.0.2</version> </dependency> <!--Dubbo 依赖--> <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.32.Final</version> </dependency> 配置文件: ## application dubbo.application.name = spring-boot-pay dubbo.registry.address = nacos://47.104.197.9:8848 dubbo.protocol.name=dubbo dubbo.protocol.port=-1 启动类引入 Dubbo 注解: @EnableDubbo @SpringBootApplication public class Application { private static final Logger logger = LoggerFactory.getLogger(AliPayServiceImpl.class); public static void main(String[] args){ SpringApplication.run(Application.class, args); logger.info("启动成功"); } } 接口实现: //省略部分代码 import com.alibaba.dubbo.config.annotation.Service; @Service(group = "itstyle-nacos", retries = 1, timeout = 10000) public class AliPayServiceImpl implements IAliPayService { } 打包接口: <!-- 打包接口 https://blog.52itstyle.vip --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <executions> <execution> <id>service</id> <phase>package</phase> <goals> <goal>jar</goal> </goals> <configuration> <classesDirectory>${project.build.directory}/classes</classesDirectory> <finalName>pay-service</finalName> <outputDirectory>${project.build.directory}</outputDirectory> <includes> <include>com/itstyle/modules/alipay/service/*.*</include> <include>com/itstyle/modules/unionpay/service/*.*</include> <include>com/itstyle/modules/weixinpay/service/*.*</include> <include>com/itstyle/common/model/*.*</include> </includes> </configuration> </execution> </executions> </plugin> 服务引用: /** * 支付宝支付 * 创建者 爪哇笔记 https://blog.52itstyle.vip * 创建时间 2019年7月20日 */ @Controller @RequestMapping(value = "alipay") public class AliPayController { @Reference private IAliPayService aliPayService; } 启动项目,登录到管理控制中心,如果发现有数据,说明注册成功。 小结 一个 Nacos 就轻松搞定了,还捎带着配置管理中心,一举两得,何乐不为。 参考案例 https://gitee.com/52itstyle/spring-boot-pay/tree/spring-boot-nacos-pay
前言 最近在做邮件发送的服务,正常来说 SpringBoot 整合mail还是很方便的,然而来了新的需求:A请求使用邮箱C发送,B请求使用邮箱D发送,也就是说我们需要配置两套发送服务。 单实例 首先我们来看下单个服务的配置: spring.mail.host=smtp.mxhichina.com spring.mail.username=admin@52itstyle.com spring.mail.password=123456 spring.mail.properties.mail.smtp.auth=true spring.mail.properties.mail.smtp.starttls.enable=true spring.mail.properties.mail.smtp.starttls.required=true 其他的不用管,我们只需要在用到的时候注入以下即可: @Autowired private JavaMailSender mailSender;//执行者 如果大家对如何加载配置以及初始化感兴趣,可以了解下 spring-boot-autoconfigure 的原理。 多实例 由于 mail 并没有像数据库那样提供多数据源,这里只能我们自己手动获取了: /** * 创建发送器 */ public class MailUtil { public static JavaMailSenderImpl createMailSender(){ JavaMailSenderImpl sender = new JavaMailSenderImpl(); sender.setHost("smtp.mxhichina.com"); sender.setPort(25); sender.setUsername("admin@52itstyle.com"); sender.setPassword("123456"); sender.setDefaultEncoding("Utf-8"); Properties p = new Properties(); p.setProperty("mail.smtp.timeout",1000+""); p.setProperty("mail.smtp.auth","true"); sender.setJavaMailProperties(p); return sender; } } 这里,顺便说一个小功能,在发送邮件的时候,如何自定义显示发件人名称: MimeMessageHelper helper = new MimeMessageHelper(message, true); helper.setFrom("345849402@qq.com","爪哇笔记"); 最后我们在使用的时候,只需要根据不同的请求使用不同的 sender 就可以了。 项目源码 码云:https://gitee.com/52itstyle/spring-boot-mail
前言 2017年,曾在自己的博客中写下这样一段话:有一种力量无人能抵挡,它永不言败生来倔强。有一种理想照亮了迷茫,在那写满荣耀的地方。 如今2018年已过大半,虽然没有大理想抱负,但是却有自己的小计划。下面是这一年来,自己利用闲暇周末时间搞得几个开源项目,可能群里的小伙伴很多都接触过,但是这里还是要分享给大家,与君共勉,一起学习。 项目案例 项目一:支付服务 简介:支付服务:支付宝、微信、银联详细 代码案例,目前已经1800+Star。十分钟让你快速搭建一个支付服务,内附各种教程。 项目地址:https://gitee.com/52itstyle/spring-boot-pay 项目二:秒杀案例 简介:从0到1构建分布式秒杀系统,脱离案例讲架构都是耍流氓,码云GVP项目。这个是自5月以来最上心的一个项目,尽管只是一个案例,但是从中也学到了不少知识。 项目地址:https://gitee.com/52itstyle/spring-boot-seckill 项目三:邮件服务 简介:邮件发送服务,文本,附件,模板,队列,多线程,定时任务实现多种功能。 项目地址:https://gitee.com/52itstyle/spring-boot-mail 项目四:全文搜索服务 简介:ES全文搜索引擎,基于Elasticsearch构建网站日志处理系统,通过数据同步工具等一些列开源组件来快速构建一个日志处理系统,项目雏形初步成型中。 项目地址:https://gitee.com/52itstyle/spring-boot-elasticsearch 项目五:任务管理系统 简介:基于spring-boot+quartz的CRUD任务管理系统 。 项目地址:https://gitee.com/52itstyle/spring-boot-quartz 项目六:在线文档管理系统 简介:spring-boot-doc是一款针对IT团队开发的简单好用的文档管理系统。 项目地址:https://gitee.com/52itstyle/spring-boot-doc 项目七:分布式文件系统 简介:没什么好介绍的,集成了Fastdfs而已。 项目地址:https://gitee.com/52itstyle/spring-boot-fastdfs 项目八:讯飞语音 简介:讯飞语音JavaWeb语音合成解决方案。 项目地址:https://gitee.com/52itstyle/xufei_msc 小结 总结这一年来,收获还是蛮大的,通过开源本身技能得到提升,同时也接触了不少热爱技术的小伙伴。 没有对比就没有伤害,你一直想成为优秀的人,可是你却没有付出一丁点而的努力。你想想比你优秀的人都还在努力,你有什么资格不去努力!山无棱天地合,喂点鸡汤给大家。 最后,不管你是否努力,妹子还是要送的!
前言 最近懒成一坨屎,学不动系列一波接一波,大多还都是底层原理相关的。上周末抽时间重读了周志明大湿的 JVM 高效并发部分,每读一遍都有不同的感悟。路漫漫,借此,把前段时间搞着玩的秒杀案例中的分布式锁深入了解一下。 案例介绍 在尝试了解分布式锁之前,大家可以想象一下,什么场景下会使用分布式锁? 单机应用架构中,秒杀案例使用ReentrantLcok或者synchronized来达到秒杀商品互斥的目的。然而在分布式系统中,会存在多台机器并行去实现同一个功能。也就是说,在多进程中,如果还使用以上JDK提供的进程锁,来并发访问数据库资源就可能会出现商品超卖的情况。因此,需要我们来实现自己的分布式锁。 实现一个分布式锁应该具备的特性: 高可用、高性能的获取锁与释放锁 在分布式系统环境下,一个方法或者变量同一时间只能被一个线程操作 具备锁失效机制,网络中断或宕机无法释放锁时,锁必须被删除,防止死锁 具备阻塞锁特性,即没有获取到锁,则继续等待获取锁 具备非阻塞锁特性,即没有获取到锁,则直接返回获取锁失败 具备可重入特性,一个线程中可以多次获取同一把锁,比如一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法,而无需重新获得锁 在之前的秒杀案例中,我们曾介绍过关于分布式锁几种实现方式: 基于数据库实现分布式锁 基于 Redis 实现分布式锁 基于 Zookeeper 实现分布式锁 前两种对于分布式生产环境来说并不是特别推荐,高并发下数据库锁性能太差,Redis在锁时间限制和缓存一致性存在一定问题。这里我们重点介绍一下 Zookeeper 如何实现分布式锁。 实现原理 ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能存在唯一文件名。 数据模型 PERSISTENT 持久化节点,节点创建后,不会因为会话失效而消失 EPHEMERAL 临时节点, 客户端session超时此类节点就会被自动删除 EPHEMERAL_SEQUENTIAL 临时自动编号节点 PERSISTENT_SEQUENTIAL 顺序自动编号持久化节点,这种节点会根据当前已存在的节点数自动加 1 监视器(watcher) 当创建一个节点时,可以注册一个该节点的监视器,当节点状态发生改变时,watch被触发时,ZooKeeper将会向客户端发送且仅发送一条通知,因为watch只能被触发一次。 根据zookeeper的这些特性,我们来看看如何利用这些特性来实现分布式锁: 创建一个锁目录lock 线程A获取锁会在lock目录下,创建临时顺序节点 获取锁目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁 线程B创建临时节点并获取所有兄弟节点,判断自己不是最小节点,设置监听(watcher)比自己次小的节点(只关注比自己次小的节点是为了防止发生“羊群效应”) 线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是最小的节点,获得锁 代码分析 尽管ZooKeeper已经封装好复杂易出错的关键服务,将简单易用的接口和性能高效、功能稳定的系统提供给用户。但是如果让一个普通开发者去手撸一个分布式锁还是比较困难的,在秒杀案例中我们直接使用 Apache 开源的curator 开实现 Zookeeper 分布式锁。 这里我们使用以下版本,截止目前最新版4.0.1: <!-- zookeeper 分布式锁、注意zookeeper版本 这里对应的是3.4.6--> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> <version>2.10.0</version> </dependency> 首先,我们看下InterProcessLock接口中的几个方法: /** * 获取锁、阻塞等待、可重入 */ public void acquire() throws Exception; /** * 获取锁、阻塞等待、可重入、超时则获取失败 */ public boolean acquire(long time, TimeUnit unit) throws Exception; /** * 释放锁 */ public void release() throws Exception; /** * Returns true if the mutex is acquired by a thread in this JVM */ boolean isAcquiredInThisProcess(); 获取锁: //获取锁 public void acquire() throws Exception { if ( !internalLock(-1, null) ) { throw new IOException("Lost connection while trying to acquire lock: " + basePath); } } private boolean internalLock(long time, TimeUnit unit) throws Exception { /* 实现同一个线程可重入性,如果当前线程已经获得锁, 则增加锁数据中lockCount的数量(重入次数),直接返回成功 */ //获取当前线程 Thread currentThread = Thread.currentThread(); //获取当前线程重入锁相关数据 LockData lockData = threadData.get(currentThread); if ( lockData != null ) { //原子递增一个当前值,记录重入次数,后面锁释放会用到 lockData.lockCount.incrementAndGet(); return true; } //尝试连接zookeeper获取锁 String lockPath = internals.attemptLock(time, unit, getLockNodeBytes()); if ( lockPath != null ) { //创建可重入锁数据,用于记录当前线程重入次数 LockData newLockData = new LockData(currentThread, lockPath); threadData.put(currentThread, newLockData); return true; } //获取锁超时或者zk通信异常返回失败 return false; } Zookeeper获取锁实现: String attemptLock(long time, TimeUnit unit, byte[] lockNodeBytes) throws Exception { //获取当前时间戳 final long startMillis = System.currentTimeMillis(); //如果unit不为空(非阻塞锁),把当前传入time转为毫秒 final Long millisToWait = (unit != null) ? unit.toMillis(time) : null; //子节点标识 final byte[] localLockNodeBytes = (revocable.get() != null) ? new byte[0] : lockNodeBytes; //尝试次数 int retryCount = 0; String ourPath = null; boolean hasTheLock = false; boolean isDone = false; //自旋锁,循环获取锁 while ( !isDone ) { isDone = true; try { //在锁节点下创建临时且有序的子节点,例如:_c_008c1b07-d577-4e5f-8699-8f0f98a013b4-lock-000000001 ourPath = driver.createsTheLock(client, path, localLockNodeBytes); //如果当前子节点序号最小,获得锁则直接返回,否则阻塞等待前一个子节点删除通知(release释放锁) hasTheLock = internalLockLoop(startMillis, millisToWait, ourPath); } catch ( KeeperException.NoNodeException e ) { //异常处理,如果找不到节点,这可能发生在session过期等时,因此,如果重试允许,只需重试一次即可 if ( client.getZookeeperClient().getRetryPolicy().allowRetry(retryCount++, System.currentTimeMillis() - startMillis, RetryLoop.getDefaultRetrySleeper()) ) { isDone = false; } else { throw e; } } } //如果获取锁则返回当前锁子节点路径 if ( hasTheLock ) { return ourPath; } return null; } private boolean internalLockLoop(long startMillis, Long millisToWait, String ourPath) throws Exception { boolean haveTheLock = false; boolean doDelete = false; try { if ( revocable.get() != null ) { client.getData().usingWatcher(revocableWatcher).forPath(ourPath); } //自旋获取锁 while ( (client.getState() == CuratorFrameworkState.STARTED) && !haveTheLock ) { //获取所有子节点集合 List<String> children = getSortedChildren(); //判断当前子节点是否为最小子节点 String sequenceNodeName = ourPath.substring(basePath.length() + 1); // +1 to include the slash PredicateResults predicateResults = driver.getsTheLock(client, children, sequenceNodeName, maxLeases); //如果是最小节点则获取锁 if ( predicateResults.getsTheLock() ) { haveTheLock = true; } else { //获取前一个节点,用于监听 String previousSequencePath = basePath + "/" + predicateResults.getPathToWatch(); synchronized(this) { try { //这里使用getData()接口而不是checkExists()是因为,如果前一个子节点已经被删除了那么会抛出异常而且不会设置事件监听器,而checkExists虽然也可以获取到节点是否存在的信息但是同时设置了监听器,这个监听器其实永远不会触发,对于Zookeeper来说属于资源泄露 client.getData().usingWatcher(watcher).forPath(previousSequencePath); if ( millisToWait != null ) { millisToWait -= (System.currentTimeMillis() - startMillis); startMillis = System.currentTimeMillis(); //如果设置了获取锁等待时间 if ( millisToWait <= 0 ) { doDelete = true; // 超时则删除子节点 break; } //等待超时时间 wait(millisToWait); } else { wait();//一直等待 } } catch ( KeeperException.NoNodeException e ) { // it has been deleted (i.e. lock released). Try to acquire again //如果前一个子节点已经被删除则deException,只需要自旋获取一次即可 } } } } } catch ( Exception e ) { ThreadUtils.checkInterrupted(e); doDelete = true; throw e; } finally { if ( doDelete ) { deleteOurPath(ourPath);//获取锁超时则删除节点 } } return haveTheLock; } 释放锁: public void release() throws Exception { Thread currentThread = Thread.currentThread(); LockData lockData = threadData.get(currentThread); //没有获取锁,你释放个球球,如果为空抛出异常 if ( lockData == null ) { throw new IllegalMonitorStateException("You do not own the lock: " + basePath); } //获取重入数量 int newLockCount = lockData.lockCount.decrementAndGet(); //如果重入锁次数大于0,直接返回 if ( newLockCount > 0 ) { return; } //如果重入锁次数小于0,抛出异常 if ( newLockCount < 0 ) { throw new IllegalMonitorStateException("Lock count has gone negative for lock: " + basePath); } try { //释放锁 internals.releaseLock(lockData.lockPath); } finally { //移除当前线程锁数据 threadData.remove(currentThread); } } 测试案例 为了更好的理解其原理和代码分析中获取锁的过程,这里我们实现一个简单的Demo: /** * 基于curator的zookeeper分布式锁 */ public class CuratorUtil { private static String address = "192.168.1.180:2181"; public static void main(String[] args) { //1、重试策略:初试时间为1s 重试3次 RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3); //2、通过工厂创建连接 CuratorFramework client = CuratorFrameworkFactory.newClient(address, retryPolicy); //3、开启连接 client.start(); //4 分布式锁 final InterProcessMutex mutex = new InterProcessMutex(client, "/curator/lock"); //读写锁 //InterProcessReadWriteLock readWriteLock = new InterProcessReadWriteLock(client, "/readwriter"); ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5); for (int i = 0; i < 5; i++) { fixedThreadPool.submit(new Runnable() { @Override public void run() { boolean flag = false; try { //尝试获取锁,最多等待5秒 flag = mutex.acquire(5, TimeUnit.SECONDS); Thread currentThread = Thread.currentThread(); if(flag){ System.out.println("线程"+currentThread.getId()+"获取锁成功"); }else{ System.out.println("线程"+currentThread.getId()+"获取锁失败"); } //模拟业务逻辑,延时4秒 Thread.sleep(4000); } catch (Exception e) { e.printStackTrace(); } finally{ if(flag){ try { mutex.release(); } catch (Exception e) { e.printStackTrace(); } } } } }); } } } 这里我们开启5个线程,每个线程获取锁的最大等待时间为5秒,为了模拟具体业务场景,方法中设置4秒等待时间。开始执行main方法,通过ZooInspector监控/curator/lock下的节点如下图: 对,没错,设置4秒的业务处理时长就是为了观察生成了几个顺序节点。果然如案例中所述,每个线程都会生成一个节点并且还是有序的。 观察控制台,我们会发现只有两个线程获取锁成功,另外三个线程超时获取锁失败会自动删除节点。线程执行完毕我们刷新一下/curator/lock节点,发现刚才创建的五个子节点已经不存在了。 小结 通过分析第三方开源工具实现的分布式锁方式,收获还是满满的。学习本身就是一个由浅入深的过程,从如何调用API,到理解其代码逻辑实现,想要更深入可以去挖掘Zookeeper的核心算法ZAB协议。 最后为了方便大家学习,总结了学习过程中遇到的几个关键词:重入锁、自旋锁、有序节点、阻塞、非阻塞、监听,希望对大家有所帮助。 秒杀案例:https://gitee.com/52itstyle/spring-boot-seckill 参考 https://yq.aliyun.com/articles/60663 http://www.hollischuang.com/archives/1716 http://www.cnblogs.com/sunddenly/p/4033574.html http://ifeve.com/zookeeper-lock/ 作者: 小柒 出处: https://blog.52itstyle.com 分享是快乐的,也见证了个人成长历程,文章大多都是工作经验总结以及平时学习积累,基于自身认知不足之处在所难免,也请大家指正,共同进步。
前言 前一段时间自家养的几只猫经常出问题,由于没有有效的监控预警手段,以至于问题出现或者许久一段时间才会被通知到。凌晨一点这个锅可谁都不想背,为此基于目前的情况搭建了以下这么一套监控预警系统。 相关软件 Nginx:代理访问 Grafana Grafana: 可视化面板(Dashboard),有着非常漂亮的图表和布局展示 Influxdb:开源的时间序列数据库,适用于记录度量,事件及执行分析 Telegraf:收集系统和服务的统计数据 Docker:开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中 监控架构 GTI监控预警系统,架构流程说明: 第一步:数据采集,Telegraf 采集 Tomcat 相关参数数据 第二步:数据存储,Influxdb 存储 Telegraf 采集的数据 第三步:数据可视化,Grafana 配置 Tomcat 监控面板 第四步:预警通知,配置钉钉、邮件等预警 安装配置 这里只对Grafana、Telegraf、Influxdb、Tomcat 做相应的安装说明,Nginx 以及 Docker 请自行查阅资料。 Grafana Grafana只是一个接入数据源的可视化面板,这里为了方便,我们选择Docker安装。 mkdir grafana ID=$(id -u) docker run -d --user $ID --name=grafana --volume "$PWD/grafana:/var/lib/grafana" -p 3000:3000 grafana/grafana # 如果生产环境配置,最好提前配置好域名 docker run -d --user $ID --name=grafana --volume "$PWD/data:/var/lib/grafana" -p 3000:3000 -e "GF_SERVER_ROOT_URL=http://monitor.52itstyle.com" grafana/grafana 执行成功以后,执行以下命令: docker ps 如果出现grafana运行容器说明安装成功。 查看容器相关参数: docker inspect docker.io/grafana/grafana 进入: docker exec -it grafana /bin/sh Grafana的默认配置文件grafana.ini位于容器中的/etc/grafana,这个文件是映射不出来的。不过可以先创建并运行一个容器,拷贝出来重新创建运行容器。 参数说明(这里截取了部分重点参数): ##################### Grafana 几个重要的参数(参考一下) ##################### [paths] # 存放临时文件、session以及sqlite3数据库的目录 ;data = /var/lib/grafana # 存放日志的地方 ;logs = /var/log/grafana # 存放相关插件的地方 ;plugins = /var/lib/grafana/plugins #################################### Server #################################### [server] # 默认协议 支持(http, https, socket) ;protocol = http # 默认端口 ;http_port = 3000 # 这里配置访问地址,如果使用了反向代理请配置域名,发送告警通知的时候作为访问地址 root_url = http://grafana.52itstyle.com #################################### Database #################################### [database] # 默认使用的数据库sqlite3,位于/var/lib/grafana目录下面 ;path = grafana.db #################################### Session #################################### [session] # session 存储方式,默认是file即可 Either "memory", "file", "redis", "mysql", "postgres", default is "file" ;provider = file #################################### SMTP / Emailing ########################## [smtp] # 邮件服务器配置,自行修改配置 enabled = true host = smtp.mxhichina.com:465 user = admin@52itstyle.com # If the password contains # or ; you have to wrap it with trippel quotes. Ex """#password;""" password = 123456 ;cert_file = ;key_file = ;skip_verify = false from_address = admin@52itstyle.com # 这里不要设置中文,否则会发送失败 from_name = Grafana Influxdb 创建并运行容器 docker run -d -p 8083:8083 -p 8086:8086 -e ADMIN_USER="root" -e INFLUXDB_INIT_PWD="root" -e PRE_CREATE_DB="telegraf" --name influxdb tutum/influxdb:latest 各个参数含义: -d:容器在后台运行 --name:容器名称 -e:指定环境变量,容器中可以使用该环境变量 -p:将容器内端口映射到宿主机端口,格式为 宿主机端口:容器内端口;8083是influxdb的web管理工具端口,8086是influxdb的HTTP API端口 执行成功以后,执行以下命令: docker ps 如果出现influxdb运行容器说明安装成功。 访问地址:http://ip:8083/ Telegraf docker pull telegraf 把telegraf相关配置拷贝到宿机 docker cp telegraf:/etc/telegraf/telegraf.conf ./telegraf 采集Tomcat数据: 如果想监控多个Tomcat,这里配置多个[[inputs.tomcat]]即可,但是一定要配置不同的tags标识。 [[inputs.tomcat]] url = "http://192.168.1.190:8080/manager/status/all?XML=true" # Tomcat访问账号密码 必须配置 username = "tomcat" password = "tomcat" timeout = "5s" # 标识Tomcat名称、根据实际项目部署情况而定 [inputs.tomcat.tags] host = "blog" [[inputs.tomcat]] url = "http://192.168.1.190:8081/manager/status/all?XML=true" # Tomcat访问账号密码 必须配置 username = "tomcat" password = "tomcat" timeout = "5s" # 标识Tomcat名称、根据实际项目部署情况而定 [inputs.tomcat.tags] host = "bbs" 采集数据到influxdb: [[outputs.influxdb]] # urls = ["udp://localhost:8089"] # UDP endpoint example urls = ["http://localhost:8086"] # required,这个url改成自己host ## The target database for metrics (telegraf will create it if not exists). database = "telegraf" # 这个会在influx库创建一个库 把配置文件复制到容器: docker cp telegraf.conf telegraf:/etc/telegraf/telegraf.conf 重启telegraf服务: docker restart docker Tomcat 由于telegraf收集Tomcat相关数据需要配置访问权限,这里我们选择Tomcat7做配置说明。 修改位于conf下的tomcat-users.xml文件: <tomcat-users> <user username="tomcat" password="tomcat" roles="manager-gui,manager-script,manager-jmx,manager-status"/> </tomcat-users> 重启Tomcat容器,访问以下地址: http://ip:8080/manager/status/all?XML=true 如果出现以上界面,说明配置成功。 监控配置 依次启动Tomcat、Influxdb、Telegraf、Grafana完成后,我们进入Grafana后台管理进行相关配置。 配置Influxdb数据源: 选择 datasources/Add datasource 输入正确的HTTP地址以及数据库账号密码,点击保存,如果出现绿色提示框,说明配置成功。 配置Tomcat仪表盘: 选择 dashboard/import 这里有三种方式导入面板: 选择输入官方面板ID或者URL 直接复制黏贴JSON格式代码 导入第三方面板JSON格式文件 这里我们导入事先自己定制保存的Tomcat监控面板,最后点击导入保存。 如果不出意外,将会是下图的样子。 告警配置 前期做了这么多,我们的最终目的是为了提前预警通知,在系统即将发生灾难之前作出相应的准备调整。这里我们以Tomcat的线程数量阈值作为预警通知。 点击线程面板-选择编辑: 配置相关参数: 1、Alert名称,可以自定义。2、执行的频率,这里我选择每60s检测一次。3、判断标准,默认是avg,这里是下拉框,自己按需求选择。4、query(A,5m,now),字母A代表选择的metrics中设置的sql,也可以选择其它在metrics中设置的,但这里是单选。5m代表从现在起往之前的五分钟,即5m之前的那个点为时间的起始点,now为时间的结束点,此外这里可以自己手动输入时间。5、设置的预警临界点,这里手动输入,和6是同样功能,6可以手动移动,两种操作是等同的。 配置预警信息以及通知方式: 这里我们选择的是邮件预警通知,但是要提前进行配置,详见一开始grafana.ini中 SMTP / Emailing 相关参数配置。 点击发送测试,提示成功会发送一份告警Demo到指定邮箱: 总结 讲道理,这一套东西还是挺强大的。特别是对于中小公司来说,各种成熟的开源组间一整合完美搭建出一套监控系统,时间成本、人力成本、技术成本可以降到最低。 参考文档 大家安装过程中,版本可能不尽相同,相关页面展示会不一致,但是不会影响最终功能呈现。 http://docs.grafana.org/ https://docs.influxdata.com/influxdb/ https://docs.influxdata.com/telegraf/ https://blog.52itstyle.com/archives/2014/ https://blog.52itstyle.com/archives/2029/ https://github.com/influxdata/telegraf/pull/3277
前言 秒杀架构到后期,我们采用了消息队列的形式实现抢购逻辑,那么之前抛出过这样一个问题:消息队列异步处理完每个用户请求后,如何通知给相应用户秒杀成功? 场景映射 首先,我们举一个生活中比较常见的例子:我们去银行办理业务,一般会选择相关业务打印一个排号纸,然后就可以坐在小板凳上玩着手机,等待被小喇叭报号。当小喇叭喊到你所持有的号码,就可以拿着排号纸去柜台办理自己的业务。 这里,假设当我们取排号纸的时候,银行根据时间段内的排队情况,比较人性化的提示用户:排队人数较多,您是否继续等待?否的话我们可以换个时间段再来办理。 由此我们把生活场景映射到真实的秒杀业务逻辑中来: 我们可以把柜台比喻成商品下单处理逻辑单元 拿到排号纸说明你进入相应商品处理队列 拿到排号纸的请求直接返回前台,提示用户抢购进行中 排号纸进入队列后,等待商品业务处理逻辑 小喇叭叫到自己的排号相当于服务端通知用户秒杀成功,这时候可以进行支付逻辑 那些拿不到票号的同学,相当于队列已满直接返回秒杀失败 解决方案 通过上面的场景,我们很容易能够想到一种方案就是服务端通知,那么如何做到服务端异步通知的呢?下面,主角开始登场了,就是我们的Websocket。 WebSocket是HTML5开始提供的一种浏览器与服务器间进行全双工通讯的网络技术。依靠这种技术可以实现客户端和服务器端的长连接,双向实时通信。 特点: 异步、事件触发 可以发送文本,图片等流文件 数据格式比较轻量,性能开销小,通信高效 使用ws或者wss协议的客户端socket,能够实现真正意义上的推送功能 缺点: 部分浏览器不支持,浏览器支持的程度与方式有区别,需要各种兼容写法。 集成案例 由于我们的秒杀架构项目案例中使用了SpringBoot,因此集成webSocket也是相对比较简单的。 首先pom.xml引入以下依赖: <!-- webSocket 秒杀通知--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> WebSocketConfig 配置: /** * WebSocket配置 * 创建者 爪哇笔记 * 创建时间 2018年5月29日 */ @Configuration public class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } } WebSocketServer 配置: @ServerEndpoint("/websocket/{userId}") @Component public class WebSocketServer { private final static Logger log = LoggerFactory.getLogger(WebSocketServer.class); //静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。 private static int onlineCount = 0; //concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。 private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<WebSocketServer>(); //与某个客户端的连接会话,需要通过它来给客户端发送数据 private Session session; //接收userId private String userId=""; /** * 连接建立成功调用的方法*/ @OnOpen public void onOpen(Session session,@PathParam("userId") String userId) { this.session = session; webSocketSet.add(this); //加入set中 addOnlineCount(); //在线数加1 log.info("有新窗口开始监听:"+userId+",当前在线人数为" + getOnlineCount()); this.userId=userId; try { sendMessage("连接成功"); } catch (IOException e) { log.error("websocket IO异常"); } } /** * 连接关闭调用的方法 */ @OnClose public void onClose() { webSocketSet.remove(this); //从set中删除 subOnlineCount(); //在线数减1 log.info("有一连接关闭!当前在线人数为" + getOnlineCount()); } /** * 收到客户端消息后调用的方法 * @param message 客户端发送过来的消息*/ @OnMessage public void onMessage(String message, Session session) { log.info("收到来自窗口"+userId+"的信息:"+message); //群发消息 for (WebSocketServer item : webSocketSet) { try { item.sendMessage(message); } catch (IOException e) { e.printStackTrace(); } } } /** * @param session * @param error */ @OnError public void onError(Session session, Throwable error) { log.error("发生错误"); error.printStackTrace(); } /** * 实现服务器主动推送 */ public void sendMessage(String message) throws IOException { this.session.getBasicRemote().sendText(message); } /** * 群发自定义消息 * */ public static void sendInfo(String message,@PathParam("userId") String userId){ log.info("推送消息到窗口"+userId+",推送内容:"+message); for (WebSocketServer item : webSocketSet) { try { //这里可以设定只推送给这个userId的,为null则全部推送 if(userId==null) { item.sendMessage(message); }else if(item.userId.equals(userId)){ item.sendMessage(message); } } catch (IOException e) { continue; } } } public static synchronized int getOnlineCount() { return onlineCount; } public static synchronized void addOnlineCount() { WebSocketServer.onlineCount++; } public static synchronized void subOnlineCount() { WebSocketServer.onlineCount--; } } KafkaConsumer 消费配置,通知用户是否秒杀成功: /** * 消费者 spring-kafka 2.0 + 依赖JDK8 * @author 科帮网 By https://blog.52itstyle.com */ @Component public class KafkaConsumer { @Autowired private ISeckillService seckillService; private static RedisUtil redisUtil = new RedisUtil(); /** * 监听seckill主题,有消息就读取 * @param message */ @KafkaListener(topics = {"seckill"}) public void receiveMessage(String message){ //收到通道的消息之后执行秒杀操作 String[] array = message.split(";"); if(redisUtil.getValue(array[0])!=null){//control层已经判断了,其实这里不需要再判断了 Result result = seckillService.startSeckil(Long.parseLong(array[0]), Long.parseLong(array[1])); if(result.equals(Result.ok())){ WebSocketServer.sendInfo(array[0].toString(), "秒杀成功");//推送给前台 }else{ WebSocketServer.sendInfo(array[0].toString(), "秒杀失败");//推送给前台 redisUtil.cacheValue(array[0], "ok");//秒杀结束 } }else{ WebSocketServer.sendInfo(array[0].toString(), "秒杀失败");//推送给前台 } } } webSocket.js 前台通知逻辑: $(function(){ socket.init(); }); var basePath = "ws://localhost:8080/seckill/"; socket = { webSocket : "", init : function() { //userId:自行追加 if ('WebSocket' in window) { webSocket = new WebSocket(basePath+'websocket/1'); } else if ('MozWebSocket' in window) { webSocket = new MozWebSocket(basePath+"websocket/1"); } else { webSocket = new SockJS(basePath+"sockjs/websocket"); } webSocket.onerror = function(event) { alert("websockt连接发生错误,请刷新页面重试!") }; webSocket.onopen = function(event) { }; webSocket.onmessage = function(event) { var message = event.data; alert(message)//判断秒杀是否成功、自行处理逻辑 }; } } 客户端API 客户端与服务器通信 send() 向远程服务器发送数据 close() 关闭该websocket链接 监听函数 onopen 当网络连接建立时触发该事件 onerror 当网络发生错误时触发该事件 onclose 当websocket被关闭时触发该事件 onmessage 当websocket接收到服务器发来的消息的时触发的事件,也是通信中最重要的一个监听事件。msg.data readyState属性 这个属性可以返回websocket所处的状态。 CONNECTING(0) websocket正尝试与服务器建立连接 OPEN(1) websocket与服务器已经建立连接 CLOSING(2) websocket正在关闭与服务器的连接 CLOSED(3) websocket已经关闭了与服务器的连接 开源方案 goeasy GoEasy实时Web推送,支持后台推送和前台推送两种:后台推送可以选择Java SDK、 Restful API支持所有开发语言;前台推送:JS推送。无论选择哪种方式推送代码都十分简单(10分钟可搞定)。由于它支持websocket 和polling两种连接方式所以兼顾大多数主流浏览器,低版本的IE浏览器也是支持的。 地址:http://goeasy.io/ Pushlets Pushlets 是通过长连接方式实现“推”消息的。推送模式分为:Poll(轮询)、Pull(拉)。 地址:http://www.pushlets.com/ Pushlet Pushlet 是一个开源的 Comet 框架,Pushlet 使用了观察者模型:客户端发送请求,订阅感兴趣的事件;服务器端为每个客户端分配一个会话 ID 作为标记,事件源会把新产生的事件以多播的方式发送到订阅者的事件队列里。 地址:https://github.com/wjw465150/Pushlet 总结 其实前面有提过,尽管WebSocket有诸多优点,但是,如果服务端维护很多长连接也是挺耗费资源的,服务器集群以及览器或者客户端兼容性问题,也会带来了一些不确定性因素。大体了解了一下各大厂的做法,大多数都还是基于轮询的方式实现的,比如:腾讯PC端微信扫码登录、京东商城支付成功通知等等。 有些小伙伴可能会问了,轮询岂不是会更耗费资源?其实在我看来,有些轮询是不可能穿透到后端数据库查询服务的,比如秒杀,一个缓存标记位就可以判定是否秒杀成功。相对于WS的长连接以及其不确定因素,在秒杀场景下,轮询还是相对比较合适的。 思考 最后,思考一个问题:100件商品,假如有一万人进行抢购,该如何设置队列长度? 秒杀案例:https://gitee.com/52itstyle/spring-boot-seckill 参考 https://blog.52itstyle.com/archives/736/ https://www.xoriant.com/blog/mobility/websocket-web-stateful-now.html
前言 距离上次被DDOS攻击已经有10天左右的时间,距离上上次已经记不起具体那一天了,每一次都这么不了了只。然而近期一次相对持久的攻击,我觉得有必要静下心来,分享一下被黑的那段经历。 在叙述经历之前,先简单的介绍一下服务器配置情况: ECS 1核2G内存1MB带宽,Linux系统 RDS 2核240MB内存,最大连接数60 Redis 256MB共享实例,搬家之后没用到 CND 按量付费,缓存小文件 以上配置,对于一个日访问量几千的网站来说应该绰绰有余了,并发撑死十几个左右,以下是简单的网站部署情况: 经历 前段时间听说过互联网大佬阮一峰博客被DDOS的经历,可谓是持久啊,最终被迫转移服务器,据说还被勒索。然不知道为啥是哪个仙人板板居然盯上了我的小站?难道我比阮大神长得帅? 好吧,故事开始,2018年6月14日,凌晨两点三十收到了阿里云系统告警通知,告知网站无法访问,然而那会我还在睡梦中。 跟往常一样,差不多六点左右醒来,习惯性的翻看手机,恰好此时又发来了短信告警。要在平时的话是可以再睡两个小时的,然而此时一个激灵,瞬间困意全无,怎么说我也是有几千访问量的博主了。 于是,赶紧爬起来打开电脑,尝试访问下博客和论坛,果不其然浏览器在一直打转转。 问题排查 尝试远程登录服务器: 查看Nginx 和 PHP-FPM,ps -ef|grep xxxx 查看系统剩余内存 free -m 查看CPU使用情况 top 查看Nginx错误日志 tail -f error.log 查看日志容量 ll -h 查看并发连接数 netstat -nat|grep ESTABLISHED|wc -l 一顿骚操作之后,并没有什么异常,内存和CPU平稳,Nginx和PHP 进程没问题。然后分别重启了一下 PHP 和 Nginx,开始网站还可以访问,进入社区首页就被卡死。 查看错误日志,后台使劲的刷日志,随便查看了几个IP,有印度的,美国的,菲律宾的等等,当然大多数还是国内的IP。一晚上的时间居然刷了上百兆日志(上次被D我清理过一次),反正我觉得是不少了,对比网站平时的访问量来说。 之前有过几次攻击,但都是三三俩俩的过来,使用Nginx禁掉IP就是了。然而此次,显然不是禁掉IP可以解决问题的了,这么多IP收集是个问题(当然可以通过正则匹配获取),还有可能造成误伤。 上班途中 然而上班才是正事,心思着一时半会解决不了问题,瞄了一眼错误日志,还在使劲的刷着,然后顺手发了个朋友圈然后去洗漱: 路上一路嘟念,心想是不是到了9点,他们准时下夜班然后就可以正常访问了,自我开解一下。 上班中 到了公司,第一件事当然是远程登录下服务器,看了一下,错误日志还在使劲刷。正常来说这个是时间点是不会有用户来访问的。 重启了服务多次,访问一下首页就被卡死,然后瞬间瘫痪,整个网站(社区+博客)都不能访问了。既然这样,还是老实上班,坐等攻击停止吧。 期间群里的小伙伴们问网站怎么了,打不开了椰?将近中午的时候,查看了一下错误日志,还有那么几个IP再尝试请求不同的地址,一瞅就不是什么好东西,果断deny了一下。话说,现在请求没那么多了,重启了一些Nginx 和 PHP 进程,访问首页还是卡死?真是怪了个蛋。 心想是不是RDS数据库的问题,查看了监控报警面板,CPU和内存利用率和当前总连接数都正常,没有什么异常,凌晨两点-六点左右的确有波动,但是不至于被D死。既然都登录了,要不顺便把 ECS 和 RDS 都重启了吧。 果然,重启一下居然神奇的好了,吃午饭的时候还用手机访问了一下,正常,可以安心吃饭了。 问题解决 其实,最终问题怎么解决的,我并不清楚,说几个比较疑惑的点: ECS 服务器 CPU 和内存也在正常阈值 Nginx 和 PHP-FPM 进程都分别重启过 RDS 数据库连接数尽管有所波动,但是并没有占满未释放 看错误日志请求都是来自上百个不同的IP,并且大多都是访问的社区URL 还有这些肉鸡为什么都是晚上?晚上便宜?还是说在西半球组织攻击 此次是有针对性的,还是随机的?但愿是随机的 中间停止过一次社区,博客是可以一直正常访问的,怀疑是首页数据库查询的问题,基于连接数应该不是这个问题,难道是Discuz的Bug?但是后来重启数据库后的确可以正常访问了。 其实阿里云有基础的DDOS防护,清洗触发值: 每秒请求流量:300M 每秒报文数量:70000 对于一般小站来说,是万万不可能达到300M的流量阈值的,博客的CND峰值才3M而已。 所以说,这些小波流的攻击只能自身去默默承受,而机器配置不高,买不起带宽只能任攻击自由的撒欢,还不如直接关站,扔给他一个Nginx + 静态页面让它D去吧。 攻防策略 如果有人真D你的站点,你还真没有办法,当然我所说的群体是针对中小站长而言,你连DDOS基础防护的清洗阈值都达不到。 如果你只是一个默默无闻的小站,根本不需要想那么多。尽管现在DDOS成本很低,但谁不是无利不起早,除非你得罪了什么人。 当然对于一般的攻击我们也不能坐以待毙,这里总结了几个小技巧,分享给大家,反向代理使用的是openresty。 Nginx优化 Nginx号称最大并发5W,实际上对于中小站点来说几十或者上百个并发就不错了,最基本的参数就可以满足需求。但是为了安全期间,我们最好隐藏其版本号。 # 隐藏版本,防止已知漏洞被利用 server_tokens off; #在http 模块当中配置 PHP优化 在php渲染的网页header信息中,会包含php的版本号信息,比如: X-Powered-by: php/5.6.30,这有些不安全,有些黑客可能采用扫描的方式,批量寻找低版本的php服务器,利用php漏洞(比如hash冲突)来攻击服务器。 # 隐藏版本,防止已知漏洞被利用 php_admin_flag[expose_php] = off IP黑名单 对付那种最low的攻击,加入黑名单的确是一个不错的选择,不然别人AB就能把你压死: # 在Nginx的http模块添加以下配置即可 deny 61.136.197.xxx; # 禁封IP段 deny 61.136.197.0/24; IP日访问次数 限制单个IP的日访问次数,正常来说一个用户的访问深度很少超过10个,跳出率一般在50%-70%之间。其实我们要做的把单个IP的日访问量控制在100甚至50以内即可。 限制并发数 光限制访问次数还是不够的,攻击者可能瞬间涌入成百上千的请求,如果这些请求到后端服务,会打垮数据库服务的,所以我们还要基于我们自身网站访问情况设置并发数。 限制单个IP的并发数 限制总并发数 这里建议大家使用漏桶算法限流,来整形流量请求。 配置CND 基于带宽以及正常用户访问速度的考量,建议配置CND,以下是博客的流量使用情况,峰值3MB,对于我这1MB带宽的服务器肯定是抗不住啊,况且还有社区的访问。 配置缓存 数据库资源是宝贵的,所以尽量不要让请求直达后端。其实搬家之前,博客和社区都是配置过redis缓存的。由于之前购买的Redis服务是专有网络,新账号无法连接,然后就作罢了。 看来这次,需要在空闲服务器上配置一把了,反正闲着也是闲着,能起一丢丢作用也是好的。 阿里云Redis加速Discuz论坛访问 阿里云Redis加速Typecho博客访问 总结 前面也说了,对于攻击,小站真的无解,能做好基础的防护就可以了。但是对于那些肉鸡们或者即将成为肉鸡的人来说: 软件漏洞一定要及时打补丁,时刻关注互联网相关动态。 黑客利用被入侵的路由器获取网络流量,从而控制大连肉鸡。 大多数肉鸡是没有安全意识的,并且被长期利用,经发现,不少是云服务商主机、托管服务器主机,被黑客利用漏洞控制。 DDoS黑客攻击正在向产业化、平台服务化转变,如果有人想害你,一个按钮、几百块钱,就可以实现一整月的攻击,然后一首《凉凉》送给自己。 参考 限流脚本:从构建分布式秒杀系统聊聊限流特技
前言 最近有些朋友在面试阿里,加上 Java-Interview 项目的原因也有小伙伴和我讨论,近期也在负责部门的招聘,这让我想起年初那段长达三个月的奇葩面试经历。 本来没想拿出来说的,毕竟最后也没成。 但由于那几个月的经历让我了解到了大厂的工作方式、对候选同学的考察重点以及面试官的套路等都有了全新的认识。 当然最重要的是这段时间的查漏补缺也让自己精进不少。 先交代下背景吧: 从去年 12 月到今年三月底,我前前后后面了阿里三个部门。 其中两个部门通过了技术面试,还有一个跪在了三面。 光看结果还不错,但整个流程堪称曲折。 下面我会尽量描述流程以及大致的面试题目大纲,希望对想要跳槽、正在面试的同学带来点灵感,帮助可能谈不上,但启发还是能有。 以下内容较长,请再次备好瓜子板凳。 A 部门 首先是第一次机会,去年 12 月份有位大佬加我,后来才知道是一个部门的技术 Leader 在网上看到我的博客,问我想不想来阿里试试。 这时距离上次面阿里也过去一年多了,也想看看现在几斤几两,于是便同意了。 在推荐一周之后收到了杭州打来的电话,说来也巧,那时候我正在机场候机,距离登记还有大概一个小时,心想时间肯定够了。 那是我时隔一年多第一次面试,还是在机场这样嘈杂的环境里。多多少少还是有些紧张。 一面 以下是我印象比较深刻的内容: 面试官: 谈谈你做过项目中印象较深或自认为做的比较好的地方? 博主: 我觉得我在 XX 做的不错,用了 XX 需求实现 XX 功能,性能提高了 N 倍。 面试官: 你说使用到了 AOP ,能谈谈它的实现原理嘛? 博主: 它是依靠动态代理实现的,动态代理又分为 JDK 自身的以及 CGLIB 。。。。 面试官: 嗯,能说说他们的不同及优缺点嘛? 博主: JDK 是基于接口实现,而 CGLIB 继承代理类。。。 就是这样会一直问下去,如果聊的差不多了就开始问一些零散的问题: JMM 内存模型,如何划分的?分别存储什么内容?线程安全与否? 类加载机制,谈到双亲委派模型后会问到哪些违反了双亲委派模型?为什么?为什么要双亲委派?好处是什么? 平时怎么使用多线程?有哪些好处?线程池的几个核心参数的意义? 线程间通信的方式? HashMap 的原理?当谈到线程不安全时自然引申出 ConcurrentHashMap ,它的实现原理? 分库分表如何设计?垂直拆分、水平拆分? 业务 ID 的生成规则,有哪些方式? SQL 调优?平时使用数据库有哪些注意点? 当一个应用启动缓慢如何优化? 大概是以上这些,当聊到倒数第二个时我已经登机了。最后不得不提前挂断,结束之前告诉我之后会换一个同事和我沟通,听到这样的回复一面应该是过了,后面也确实证实了这点。 二面 大概过了一周,二面如期而至。 我听声音很熟,就尝试问下是不是之前一面的面试官,结果真是。 由于二面的面试官临时有事所以他来替一下。于是我赶紧问他能否把之前答的不好的再说说?的到了肯定的答复后开始了我的表演。 有了第一次的经验这一次自然也轻车熟路,原本感觉一切尽在掌握却被告知需要笔试突然被激醒。 笔试是一个在线平台,需要在网页中写代码,会有一个明确的题目: 从一个日志文件中根据关键字读取日志,记录出现的次数,最后按照次数排序打印。 在这过程中切记要和面试官多多交流,因为笔试有时间限制,别到最后发现题目理解错了,这就和高考作文写完发现方向错了一样要命。 而且在沟通过程中体现出你解题的思路,即使最终结果不对,但说不定思考的过程很符合面试官的胃口哦。这也和今年的高考改卷一样;过程正确得高分,只有结果得低分。 三面 又过了差不多一周的时间接到了三面的电话,一般到了三面会是技术 Leader 之类的角色。 这个过程中不会过多强调技术细节,更多的考察软件能,比如团队协作、学习能力等。 但我记得也问了以下一些技术问题: 谈谈你所理解的 HTTP 协议? 对 TCP 的理解?三次握手?滑动窗口? 基本算法,Base64 等。 Java 内存模型,Happen Before 的理解。 一周之后我接到了 HR 助理的电话约了和 HRBP 以及产品技术负责人的视频面试。 但是我却没有面下去,具体原因得往下看。 B 部门 在 A 部门三面完成后,我等了差不多一星期,这期间我却收到了一封邮件。 大概内容是他在 GitHub 上看到的我,他们的技术总监对我很感兴趣(我都不敢相信我的眼镜),问我想不想来阿里试试。 我对比了 A B 部门的区别发现 B 部门在做的事情上确实更加有诱惑力,之后我表达了有一个面试正在流程中的顾虑;对方表示可以私下和我快速的进行三面,如果一切没问题再交由我自行选择。至少对双方都是一个双赢嘛。 我想也不亏,并且对方很有诚意,就答应试试;于是便有了下面的面试: 一面 面试官: 对 Java 锁的理解? 博主: 我谈到了 synchronize,Lock 接口的应用。 面试官: 他们两者的区别以及优缺点呢? 博主: synchronize 在 JDK1.6 之前称为重量锁,是通过进出对象监视器来实现同步的;1.6 之后做了 XX 优化。。。 而 ReentrantLock 是利用了一个巧妙数据结构实现的,并且加锁解锁是显式的。。。 之后又引申到分布式锁,光这块就聊了差不多半个小时。 之后又聊到了我的开源项目: 是如何想做这个项目的? 已经有一些关注了后续是如何规划的? 你今后的学习计划是什么? 平时看哪些书? 之后技术聊的不是很多,但对于个人发展却聊了不少。 关于锁相关的内容可以参考这里:ReentrantLock 实现原理 synchronize 关键字原理 二面 隔了差不多一天的时间,二面很快就来了。 内容不是很多: 线程间通信的多种方式? 限流算法?单机限流?分布式限流? 提到了 Guava Cache ,了解它的实现原理嘛? 如何定位一个线上问题? CPU 高负载?OOM 排查等? 聊完之后表示第二天应该会有三面。 三面 三面的面试官应该是之前邮件中提到的那位总监大佬,以前应该也是一线的技术大牛;聊的问题不是很多: 谈谈对 Netty 的理解? Netty 的线程模型? 写一个 LRU 缓存。 笔试 本以为技术面试完了,结果后面告知所有的面试流程都得有笔试了,于是又参与了一次笔试: 交替打印奇偶数 这个相对比较简单,基于锁、等待唤醒机制都是可以的。最后也告知笔试通过。 之后在推荐我的那位大佬的帮助下戏剧般的通过了整个技术轮(真的很感谢他的认可),并且得知这个消息是在我刚好和 A 部门约好视频面试时间之后。 也就意味着我必须拒掉一个部门! 没看错,是我要拒掉一个。这对我来说确实太难了,我压根没想过还有两个机会摆在我面前。 最后凭着个人的爱好以及 B 部门的热情我很不好意思的拒掉了 A 部门。。。 HR 面 在面这之前我从来没有面过这样大厂的 HR 流程,于是疯狂搜索,希望能弥补点经验。 也许这就是乐极生悲吧,我确实猜中了 HR 问的大部分问题,但遗憾的是最终依然没能通过。 后来我在想如果我没有拒掉 A ,会不会结局不一样了? 但现实就是如此,没有那么多假设,并且每个人也得为自己的选择负责! 大概的问题是: 为什么想来阿里? 个人做的最成功最有挑战的事情是什么? 工作中最难忘的经历? 对加入我们团队有何期待? C 部门 HR 这关被 Pass 之后没多久我居然又收到了第三个部门的邀约。 说实话当时我是拒绝的,之前经历了将近两个月的时间却没能如愿我内心是崩溃的。 我向联系我的大佬表达了我的想法,他倒觉得我最后被 pass 的原因是个小问题,再尝试的话会有很大的几率通过。 我把这事给朋友说了之后也支持我再试试,反正也没啥损失嘛,而且面试的状态还在。 所以我又被打了鸡血,才有了下面的面试经过: 一面 面试官: 服务化框架的选型和差异? 博主: 一起探讨了 SpringCloud、Dubbo、Thrift 的差异,优缺点等。 面试官: 一致性 Hash 算法的原理? 博主: 将数据 Hash 之后落到一个 0 ~ 2^32-1 构成的一个环上。。。。 面试官: 谈谈你理解的 Zookeeper? 博主: 作为一个分布式协调器。。。 面试官: 如何处理 MQ 重复消费? 博主: 业务幂等处理。。。。 面试官: 客户端负载算法? 博主: 轮询、随机、一致性 Hash、故障转移、LRU 等。。 面试官: long 类型的赋值是否是原子的? 博主: 不是。。。 面试官: volatile 关键字的原理及作用?happen Before? 博主: 可见性、一致性。。 二面 一面之后大概一周的时间接到了二面的电话: 原以为会像之前一样直接进入笔试,这次上来先简单聊了下: 谈谈对微服务的理解,好处以及弊端? 分布式缓存的设计?热点缓存? 之后才正式进入笔试流程: 这次主要考察设计能力,其实就是对设计模式的理解?能否应对后续的扩展性。 笔试完了之后也和面试官交流,原以为会是算法之类的测试,后来得知他能看到前几轮的笔试情况,特地挑的没有做过的方向。 所以大家也不用刻意去押题,总有你想不到的,平时多积累才是硬道理。 三面 又过了两周左右,得到 HR 通知;希望能过去杭州参加现场面试。并且阿里包了来回的机票酒店等。 可见阿里对人才渴望还是舍得下成本的。 既然都这样了,就当成一次旅游所以去了一趟杭州。 现场面的时候有别于其他面试,是由两个面试官同时参与: 给一个场景,谈谈你的架构方式。 这就对平时的积累要求较高了。 还有一个印象较深的是: 在网页上点击一个按钮到服务器的整个流程,尽量完整。 其实之前看过,好像是 Google 的一个面试题。 完了之后让我回去等通知,没有见到 HR 我就知道凉了,果不其然。 总结 看到这里的朋友应该都是老铁了,我也把上文提到的大多数面试题整理在了 GitHub: 厂库地址: https://github.com/crossoverJie/Java-Interview 最后总结下这将近四个月的面试心得: 一定要积极的推销自己,像在 A 部门的三面时,由于基础答得不是很好;所以最后我表达了自己的态度,对工作、技术的积极性。让面试官看到你的潜力值得一个 HC 名额。 面试过程中遇到自己的不会的可以主动提出,切不可不懂装懂,这一问就露馅。可以将面试官引导到自己擅长的领域。比如当时我正好研究了锁,所以和面试官一聊就是半小时这就是加分项。 平时要主动积累知识。写博客和参与开源项目就是很好的方式。 博客可以记录自己踩过的坑,加深印象,而且在写的过程中可以查漏补缺,最后把整个知识体系巩固的比较牢固,良好的内容还可以得到意想不到的收获,比如我第一次面试的机会。 GitHub 是开发者的一张名片,积极参与开源项目可以和全球大佬头脑风暴,并且在面试过程中绝对是一个加分利器。 面试官一般最后都会问你有什么要问我的?千万不要问一些公司福利待遇之类的问题。可以问下本次面试的表现?还有哪些需要完善的?从而知道自己答得如何也能补全自己。 还有一点:不要在某次面试失利后否定自己,有时真的不是自己能力不行。这个也讲缘分。 塞翁失马焉知非福 我就是个例子,虽然最后没能去成阿里,现在在公司也是一个部门的技术负责人,在我们城市还有个窝,温馨的家,和女朋友一起为想要的生活努力奋斗。 原文:https://crossoverjie.top/2018/06/21/personal/Interview-experience/
前言 俗话说的好,冰冻三尺非一日之寒,滴水穿石非一日之功,罗马也不是一天就建成的。两周前秒杀案例初步成型,分享到了中国最大的同性交友网站-码云。同时也收到了不少小伙伴的建议和投诉。我从不认为分布式、集群、秒杀这些就应该是大厂的专利,在互联网的今天无论什么时候都要时刻武装自己,只有这样,也许你的春天就在明天。 在开发秒杀系统案例的过程中,前面主要分享了队列、缓存、锁和分布式锁以及静态化等等。缓存的目的是为了提升系统访问速度和增强系统的处理能力;分布式锁解决了集群下数据的安全一致性问题;静态化无疑是减轻了缓存以及DB层的压力。 限流 然而再牛逼的机器,再优化的设计,对于特殊场景我们也是要特殊处理的。就拿秒杀来说,可能会有百万级别的用户进行抢购,而商品数量远远小于用户数量。如果这些请求都进入队列或者查询缓存,对于最终结果没有任何意义,徒增后台华丽的数据。对此,为了减少资源浪费,减轻后端压力,我们还需要对秒杀进行限流,只需保障部分用户服务正常即可。 就秒杀接口来说,当访问频率或者并发请求超过其承受范围的时候,这时候我们就要考虑限流来保证接口的可用性,以防止非预期的请求对系统压力过大而引起的系统瘫痪。通常的策略就是拒绝多余的访问,或者让多余的访问排队等待服务。 限流算法 任何限流都不是漫无目的的,也不是一个开关就可以解决的问题,常用的限流算法有:令牌桶,漏桶。 令牌桶 令牌桶算法是网络流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一种算法。典型情况下,令牌桶算法用来控制发送到网络上的数据的数目,并允许突发数据的发送(百科)。 在秒杀活动中,用户的请求速率是不固定的,这里我们假定为10r/s,令牌按照5个每秒的速率放入令牌桶,桶中最多存放20个令牌。仔细想想,是不是总有那么一部分请求被丢弃。 漏桶 漏桶算法的主要目的是控制数据注入到网络的速率,平滑网络上的突发流量。漏桶算法提供了一种机制,通过它,突发流量可以被整形以便为网络提供一个稳定的流量(百科)。 令牌桶是无论你流入速率多大,我都按照既定的速率去处理,如果桶满则拒绝服务。 应用限流 Tomcat 在Tomcat容器中,我们可以通过自定义线程池,配置最大连接数,请求处理队列等参数来达到限流的目的。 Tomcat默认使用自带的连接池,这里我们也可以自定义实现,打开/conf/server.xml文件,在Connector之前配置一个线程池: <Executor name="tomcatThreadPool" namePrefix="tomcatThreadPool-" maxThreads="1000" maxIdleTime="300000" minSpareThreads="200"/> name:共享线程池的名字。这是Connector为了共享线程池要引用的名字,该名字必须唯一。默认值:None; namePrefix:在JVM上,每个运行线程都可以有一个name 字符串。这一属性为线程池中每个线程的name字符串设置了一个前缀,Tomcat将把线程号追加到这一前缀的后面。默认值:tomcat-exec-; maxThreads:该线程池可以容纳的最大线程数。默认值:200; maxIdleTime:在Tomcat关闭一个空闲线程之前,允许空闲线程持续的时间(以毫秒为单位)。只有当前活跃的线程数大于minSpareThread的值,才会关闭空闲线程。默认值:60000(一分钟)。 minSpareThreads:Tomcat应该始终打开的最小不活跃线程数。默认值:25。 配置Connector <Connector executor="tomcatThreadPool" port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" minProcessors="5" maxProcessors="75" acceptCount="1000"/> executor:表示使用该参数值对应的线程池; minProcessors:服务器启动时创建的处理请求的线程数; maxProcessors:最大可以创建的处理请求的线程数; acceptCount:指定当所有可以使用的处理请求的线程数都被使用时,可以放到处理队列中的请求数,超过这个数的请求将不予处理。 API限流 秒杀活动中,接口的请求量会是平时的数百倍甚至数千倍,从而有可能导致接口不可用,并引发连锁反应导致整个系统崩溃,甚至有可能会影响到其它服务。 那么如何应对这种突然事件呢?这里我们采用开源工具包guava提供的限流工具类RateLimiter进行API限流,该类基于"令牌桶算法",开箱即用。 自定义定义注解 /** * 自定义注解 限流 */ @Target({ElementType.PARAMETER, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface ServiceLimit { String description() default ""; } 自定义切面 /** * 限流 AOP */ @Component @Scope @Aspect public class LimitAspect { //每秒只发出100个令牌,此处是单进程服务的限流,内部采用令牌捅算法实现 private static RateLimiter rateLimiter = RateLimiter.create(100.0); //Service层切点 限流 @Pointcut("@annotation(com.itstyle.seckill.common.aop.ServiceLimit)") public void ServiceAspect() { } @Around("ServiceAspect()") public Object around(ProceedingJoinPoint joinPoint) { Boolean flag = rateLimiter.tryAcquire(); Object obj = null; try { if(flag){ obj = joinPoint.proceed(); } } catch (Throwable e) { e.printStackTrace(); } return obj; } } 业务实现: @Override @ServiceLimit @Transactional public Result startSeckil(long seckillId, long userId) { //省略部分业务代码,详见秒杀源码 } 分布式限流 Nginx 如何使用Nginx实现基本的限流,比如单个IP限制每秒访问50次。通过Nginx限流模块,我们可以设置一旦并发连接数超过我们的设置,将返回503错误给客户端。 配置nginx.conf #统一在http域中进行配置 #限制请求 limit_req_zone $binary_remote_addr $uri zone=api_read:20m rate=50r/s; #按ip配置一个连接 zone limit_conn_zone $binary_remote_addr zone=perip_conn:10m; #按server配置一个连接 zone limit_conn_zone $server_name zone=perserver_conn:100m; server { listen 80; server_name seckill.52itstyle.com; index index.jsp; location / { #请求限流排队通过 burst默认是0 limit_req zone=api_read burst=5; #连接数限制,每个IP并发请求为2 limit_conn perip_conn 2; #服务所限制的连接数(即限制了该server并发连接数量) limit_conn perserver_conn 1000; #连接限速 limit_rate 100k; proxy_pass http://seckill; } } upstream seckill { fair; server 172.16.1.120:8080 weight=1 max_fails=2 fail_timeout=30s; server 172.16.1.130:8080 weight=1 max_fails=2 fail_timeout=30s; } 配置说明 imit_conn_zone 是针对每个IP定义一个存储session状态的容器。这个示例中定义了一个100m的容器,按照32bytes/session,可以处理3200000个session。 limit_rate 300k; 对每个连接限速300k. 注意,这里是对连接限速,而不是对IP限速。如果一个IP允许两个并发连接,那么这个IP就是限速limit_rate×2。 burst=5; 这相当于桶的大小,如果某个请求超过了系统处理速度,会被放入桶中,等待被处理。如果桶满了,那么抱歉,请求直接返回503,客户端得到一个服务器忙的响应。如果系统处理请求的速度比较慢,桶里的请求也不能一直待在里面,如果超过一定时间,也是会被直接退回,返回服务器忙的响应。 OpenResty 背影有没有很熟悉,对这就是那个直呼理解万岁老罗,2015年老罗在锤子科技T2发布会上将门票收入捐赠给了 OpenResty,也相信老罗是个有情怀的胖子。 这里我们使用 OpenResty 开源的限流方案,测试案例使用OpenResty1.13.6.1最新版本,自带lua-resty-limit-traffic模块以及案例 ,实现起来更为方便。 限制接口总并发数/请求数 秒杀活动中,由于突发流量暴增,有可能会影响整个系统的稳定性从而造成崩溃,这时候我们就要限制秒杀接口的总并发数/请求数。 这里我们采用 lua-resty-limit-traffic中的resty.limit.count模块实现,由于文章篇幅具体代码参见源码openresty/lua/limit_count.lua。 限制接口时间窗请求数 秒杀场景下,有时候并都是人肉鼠标,比如12306的抢票软件,软件刷票可比人肉鼠标快多了。此时我们就要对客户端单位时间内的请求数进行限制,以至于刷票不是那么猖獗。当然了道高一尺魔高一丈,抢票软件总是会有办法绕开你的防线,从另一方面讲也促进了技术的进步。 这里我们采用 lua-resty-limit-traffic中的resty.limit.conn模块实现,具体代码参见源码openresty/lua/limit_conn.lua。 平滑限制接口请求数 之前的限流方式允许突发流量,也就是说瞬时流量都会被允许。突然流量如果不加以限制会影响整个系统的稳定性,因此在秒杀场景中需要对请求整形为平均速率处理,即20r/s。 这里我们采用 lua-resty-limit-traffic 中的resty.limit.req 模块实现漏桶限流和令牌桶限流。 其实漏桶和令牌桶根本的区别就是,如何处理超过请求速率的请求。漏桶会把请求放入队列中去等待均速处理,队列满则拒绝服务;令牌桶在桶容量允许的情况下直接处理这些突发请求。 漏桶 桶容量大于零,并且是延迟模式。如果桶没满,则进入请求队列以固定速率等待处理,否则请求被拒绝。 令牌桶 桶容量大于零,并且是非延迟模式。如果桶中存在令牌,则允许突发流量,否则请求被拒绝。 压测 为了测试以上配置效果,我们采用AB压测,Linux下执行以下命令即可: # 安装 yum -y install httpd-tools # 查看ab版本 ab -v # 查看帮助 ab --help 测试命令: ab -n 1000 -c 100 http://127.0.0.1/ 测试结果: Server Software: openresty/1.13.6.1 #服务器软件 Server Hostname: 127.0.0.1 #IP Server Port: 80 #请求端口号 Document Path: / #文件路径 Document Length: 12 bytes #页面字节数 Concurrency Level: 100 #请求的并发数 Time taken for tests: 4.999 seconds #总访问时间 Complete requests: 1000 #总请求树 Failed requests: 0 #请求失败数量 Write errors: 0 Total transferred: 140000 bytes #请求总数据大小 HTML transferred: 12000 bytes #html页面实际总字节数 Requests per second: 200.06 [#/sec](mean) #每秒多少请求,这个是非常重要的参数数值,服务器的吞吐量 Time per request: 499.857 [ms](mean) #用户平均请求等待时间 Time per request: 4.999 [ms](mean, across all concurrent requests) # 服务器平均处理时间,也就是服务器吞吐量的倒数 Transfer rate: 27.35 [Kbytes/sec] received #每秒获取的数据长度 Connection Times (ms) min mean[+/-sd] median max Connect: 0 0 0.8 0 4 Processing: 5 474 89.1 500 501 Waiting: 2 474 89.2 500 501 Total: 9 475 88.4 500 501 Percentage of the requests served within a certain time (ms) 50% 500 66% 500 75% 500 80% 500 90% 501 95% 501 98% 501 99% 501 100% 501 (longest request) 源码:从0到1构建分布式秒杀系统 总结 以上限流方案,只是针对此次秒杀案例做一个简单的小结,大家也不要刻意区分那种方案的好坏,只要适合业务场景就是最好的。 参考 https://github.com/openresty/lua-resty-limit-traffichttps://blog.52itstyle.com/archives/1764/https://blog.52itstyle.com/archives/775/ 作者: 小柒2012 欢迎关注: https://blog.52itstyle.com
前言 秒杀架构持续优化中,基于自身认知不足之处在所难免,也请大家指正,共同进步。文章标题来自码友的建议,希望可以把阻塞队列ArrayBlockingQueue这个队列替换成Disruptor,由于之前曾接触过这个东西,听说很不错,正好借此机会整合进来。 简介 LMAX Disruptor是一个高性能的线程间消息库。它源于LMAX对并发性,性能和非阻塞算法的研究,如今构成了Exchange基础架构的核心部分。 Disruptor它是一个开源的并发框架,并获得2011 Duke’s 程序框架创新奖,能够在无锁的情况下实现网络的Queue并发操作。 Disruptor是一个高性能的异步处理框架,或者可以认为是最快的消息框架(轻量的JMS),也可以认为是一个观察者模式的实现,或者事件监听模式的实现。 在这里你可以跟BlockingQueue队列作比对,简单的理解为它是一种高效的"生产者-消费者"模型,先了解后深入底层原理。 核心 写代码案例之前,大家最好先了解 Disruptor 的核心概念,至少知道它是如何运作的。 Ring Buffer如其名,环形的缓冲区。曾经 RingBuffer 是 Disruptor 中的最主要的对象,但从3.0版本开始,其职责被简化为仅仅负责对通过 Disruptor 进行交换的数据(事件)进行存储和更新。在一些更高级的应用场景中,Ring Buffer 可以由用户的自定义实现来完全替代。 Sequence Disruptor通过顺序递增的序号来编号管理通过其进行交换的数据(事件),对数据(事件)的处理过程总是沿着序号逐个递增处理。一个 Sequence 用于跟踪标识某个特定的事件处理者( RingBuffer/Consumer )的处理进度。虽然一个 AtomicLong 也可以用于标识进度,但定义 Sequence 来负责该问题还有另一个目的,那就是防止不同的 Sequence 之间的CPU缓存伪共享(Flase Sharing)问题。 Sequencer Sequencer 是 Disruptor 的真正核心。此接口有两个实现类 SingleProducerSequencer、MultiProducerSequencer ,它们定义在生产者和消费者之间快速、正确地传递数据的并发算法。 Sequence Barrier用于保持对RingBuffer的 main published Sequence 和Consumer依赖的其它Consumer的 Sequence 的引用。 Sequence Barrier 还定义了决定 Consumer 是否还有可处理的事件的逻辑。 Wait Strategy定义 Consumer 如何进行等待下一个事件的策略。 (注:Disruptor 定义了多种不同的策略,针对不同的场景,提供了不一样的性能表现) Event在 Disruptor 的语义中,生产者和消费者之间进行交换的数据被称为事件(Event)。它不是一个被 Disruptor 定义的特定类型,而是由 Disruptor 的使用者定义并指定。 EventProcessorEventProcessor 持有特定消费者(Consumer)的 Sequence,并提供用于调用事件处理实现的事件循环(Event Loop)。 EventHandlerDisruptor 定义的事件处理接口,由用户实现,用于处理事件,是 Consumer 的真正实现。 Producer即生产者,只是泛指调用 Disruptor 发布事件的用户代码,Disruptor 没有定义特定接口或类型。 优点 剖析Disruptor:为什么会这么快?(一)锁的缺点 剖析Disruptor:为什么会这么快?(二)神奇的缓存行填充 剖析Disruptor:为什么会这么快?(三)伪共享 剖析Disruptor:为什么会这么快?(四)揭秘内存屏障 有兴趣的参考:https://coolshell.cn/articles/9169.html https://www.cnblogs.com/daoqidelv/p/6995888.html 使用案例 这里以我们系统中的秒杀作为案例,后面有相对复杂的场景介绍。 定义秒杀事件对象: /** * 事件对象(秒杀事件) * 创建者 科帮网 */ public class SeckillEvent implements Serializable { private static final long serialVersionUID = 1L; private long seckillId; private long userId; public SeckillEvent(){ } public long getSeckillId() { return seckillId; } public void setSeckillId(long seckillId) { this.seckillId = seckillId; } public long getUserId() { return userId; } public void setUserId(long userId) { this.userId = userId; } } 为了让Disruptor为我们预先分配这些事件,我们需要一个将执行构造的EventFactory: /** * 事件生成工厂(用来初始化预分配事件对象) * 创建者 科帮网 */ public class SeckillEventFactory implements EventFactory<SeckillEvent> { public SeckillEvent newInstance() { return new SeckillEvent(); } } 然后,我们需要创建一个处理这些事件的消费者: /** * 消费者(秒杀处理器) * 创建者 科帮网 */ public class SeckillEventConsumer implements EventHandler<SeckillEvent> { //业务处理、这里是无法注入的,需要手动获取,见源码 private ISeckillService seckillService = (ISeckillService) SpringUtil.getBean("seckillService"); public void onEvent(SeckillEvent seckillEvent, long seq, boolean bool) throws Exception { seckillService.startSeckil(seckillEvent.getSeckillId(), seckillEvent.getUserId()); } } 既然有消费者,我们将需要这些秒杀事件的来源: /** * 使用translator方式生产者 * 创建者 科帮网 */ public class SeckillEventProducer { private final static EventTranslatorVararg<SeckillEvent> translator = new EventTranslatorVararg<SeckillEvent>() { public void translateTo(SeckillEvent seckillEvent, long seq, Object... objs) { seckillEvent.setSeckillId((Long) objs[0]); seckillEvent.setUserId((Long) objs[1]); } }; private final RingBuffer<SeckillEvent> ringBuffer; public SeckillEventProducer(RingBuffer<SeckillEvent> ringBuffer){ this.ringBuffer = ringBuffer; } public void seckill(long seckillId, long userId){ this.ringBuffer.publishEvent(translator, seckillId, userId); } } 最后,我们来写一个测试类,运行一下(跑不通,需要修改消费者): /** * 測試類 * 创建者 科帮网 */ public class SeckillEventMain { public static void main(String[] args) { producerWithTranslator(); } public static void producerWithTranslator(){ SeckillEventFactory factory = new SeckillEventFactory(); int ringBufferSize = 1024; ThreadFactory threadFactory = new ThreadFactory() { public Thread newThread(Runnable runnable) { return new Thread(runnable); } }; //创建disruptor Disruptor<SeckillEvent> disruptor = new Disruptor<SeckillEvent>(factory, ringBufferSize, threadFactory); //连接消费事件方法 disruptor.handleEventsWith(new SeckillEventConsumer()); //启动 disruptor.start(); RingBuffer<SeckillEvent> ringBuffer = disruptor.getRingBuffer(); SeckillEventProducer producer = new SeckillEventProducer(ringBuffer); for(long i = 0; i<10; i++){ producer.seckill(i, i); } disruptor.shutdown();//关闭 disruptor,方法会堵塞,直至所有的事件都得到处理; } } 使用场景 PCP (生产者-消费者问题) 网上搜了下国内实战案例并不多,大厂可能有在使用 这里举一个大家日常的例子,停车场景。当汽车进入停车场时(A),系统首先会记录汽车信息(B)。同时也会发送消息到其他系统处理相关业务(C),最后发送短信通知车主收费开始(D)。 一个生产者A与三个消费者B、C、D,D的事件处理需要B与C先完成。则该模型结构如下: 在这个结构下,每个消费者拥有各自独立的事件序号Sequence,消费者之间不存在共享竞态。SequenceBarrier1监听RingBuffer的序号cursor,消费者B与C通过SequenceBarrier1等待可消费事件。SequenceBarrier2除了监听cursor,同时也监听B与C的序号Sequence,从而将最小的序号返回给消费者D,由此实现了D依赖B与C的逻辑。 代码案例:从0到1构建分布式秒杀系统 参考:https://github.com/LMAX-Exchange/disruptor/wiki https://github.com/LMAX-Exchange/disruptor/wiki/Getting-Started http://wiki.jikexueyuan.com/project/disruptor-getting-started/lmax-framework.html
前言 最近在做一款秒杀的案例,涉及到了同步锁、数据库锁、分布式锁、进程内队列以及分布式消息队列,这里对SpringBoot集成Kafka实现消息队列做一个简单的记录。 Kafka简介 Kafka是由Apache软件基金会开发的一个开源流处理平台,由Scala和Java编写。Kafka是一种高吞吐量的分布式发布订阅消息系统,它可以处理消费者规模的网站中的所有动作流数据。 这种动作(网页浏览,搜索和其他用户的行动)是在现代网络上的许多社会功能的一个关键因素。 这些数据通常是由于吞吐量的要求而通过处理日志和日志聚合来解决。 对于像Hadoop的一样的日志数据和离线分析系统,但又要求实时处理的限制,这是一个可行的解决方案。Kafka的目的是通过Hadoop的并行加载机制来统一线上和离线的消息处理,也是为了通过集群来提供实时的消息。 Kafka是一种高吞吐量的分布式发布订阅消息系统,有如下特性: 通过O(1)的磁盘数据结构提供消息的持久化,这种结构对于即使数以TB的消息存储也能够保持长时间的稳定性能。 高吞吐量:即使是非常普通的硬件Kafka也可以支持每秒数百万的消息。 支持通过Kafka服务器和消费机集群来分区消息。 支持Hadoop并行数据加载。 术语介绍 Broker Kafka集群包含一个或多个服务器,这种服务器被称为broker Topic每条发布到Kafka集群的消息都有一个类别,这个类别被称为Topic。(物理上不同Topic的消息分开存储,逻辑上一个Topic的消息虽然保存于一个或多个broker上但用户只需指定消息的Topic即可生产或消费数据而不必关心数据存于何处) PartitionPartition是物理上的概念,每个Topic包含一个或多个Partition. Producer负责发布消息到Kafka broker Consumer消息消费者,向Kafka broker读取消息的客户端。 Consumer Group每个Consumer属于一个特定的Consumer Group(可为每个Consumer指定group name,若不指定group name则属于默认的group)。 Kafka安装 Kafka需要依赖JAVA环境运行,如何安装JDK这里不做介绍。 下载kafka: wget http://mirror.bit.edu.cn/apache/kafka/1.1.0/kafka_2.11-1.1.0.tgz 将包下载到执行目录并解压: cd /usr/local/ tar -xcvf kafka_2.11-0.10.0.1.tgz 修改kafka配置文件: cd kafka_2.11-0.10.0.1/config/ #编辑配置文件 vi server.properties broker.id=0 #端口号、记得开启端口,云服务器要开放安全组 port=9092 #服务器IP地址,修改为自己的服务器IP host.name=127.0.0.1 #zookeeper地址和端口, Kafka支持内置的Zookeeper和引用外部的Zookeeper zookeeper.connect=localhost:2181 分别启动 kafka 和 zookeeper: ./zookeeper-server-start.sh /usr/local/kafka_2.11-0.10.0.1/config/zookeeper.properties & ./kafka-server-start.sh /usr/local/kafka_2.11-0.10.0.1/config/server.properties & SpringBoot集成 pom.xml引入: <!--kafka支持--> <dependency> <groupId>org.springframework.kafka</groupId> <artifactId>spring-kafka</artifactId> <version>1.3.5.RELEASE</version><!--$NO-MVN-MAN-VER$--> </dependency> application.properties配置: #kafka相关配置 spring.kafka.bootstrap-servers=192.168.1.180:9092 #设置一个默认组 spring.kafka.consumer.group-id=0 #key-value序列化反序列化 spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer #每次批量发送消息的数量 spring.kafka.producer.batch-size=65536 spring.kafka.producer.buffer-memory=524288 生产者KafkaSender: /** * 生产者 * @author 科帮网 By https://blog.52itstyle.com */ @Component public class KafkaSender { @Autowired private KafkaTemplate<String,String> kafkaTemplate; /** * 发送消息到kafka */ public void sendChannelMess(String channel, String message){ kafkaTemplate.send(channel,message); } } 消费者: /** * 消费者 spring-kafka 2.0 + 依赖JDK8 * @author 科帮网 By https://blog.52itstyle.com */ @Component public class KafkaConsumer { /** * 监听seckill主题,有消息就读取 * @param message */ @KafkaListener(topics = {"seckill"}) public void receiveMessage(String message){ //收到通道的消息之后执行秒杀操作 } } 码云下载:从0到1构建分布式秒杀系统 参考 http://kafka.apache.org/ 作者: 小柒2012 欢迎关注: https://blog.52itstyle.com
前言 最近,被推送了不少秒杀架构的文章,忙里偷闲自己也总结了一下互联网平台秒杀架构设计,当然也借鉴了不少同学的思路。俗话说,脱离案例讲架构都是耍流氓,最终使用SpringBoot模拟实现了部分秒杀场景,同时跟大家分享交流一下。 秒杀场景 秒杀场景无非就是多个用户在同时抢购一件或者多件商品,专用词汇就是所谓的高并发。现实中经常被大家喜闻乐见的场景,一群大妈抢购打折鸡蛋的画面一定不会陌生,如此场面让服务员大姐很无奈,赶上不要钱了。 业务特点 瞬间高并发、电脑旁边的小哥哥、小姐姐们如超市哄抢的大妈一般,疯狂的点着鼠标 库存少、便宜、稀缺限量,值得大家去抢购,如苹果肾,小米粉,锤子粉(理解万岁) 用户规模 用户规模可大可小,几百或者上千人的活动单体架构足以可以应付,简单的加锁、进程内队列就可以轻松搞定。一旦上升到百万、千万级别的规模就要考虑分布式集群来应对瞬时高并发。 秒杀架构 架构层级 一般商家在做活动的时候,经常会遇到各种不怀好意的DDOS攻击(利用无辜的吃瓜群众夺取资源),导致真正的我们无法获得服务!所以说高防IP还是很有必要的。 搞活动就意味着人多,接入SLB,对多台云服务器进行流量分发,可以通过流量分发扩展应用系统对外的服务能力,通过消除单点故障提升应用系统的可用性。 基于SLB价格以及灵活性考虑后面我们接入Nginx做限流分发,来保障后端服务的正常运行。 后端秒杀业务逻辑,基于Redis 或者 Zookeeper 分布式锁,Kafka 或者 Redis 做消息队列,DRDS数据库中间件实现数据的读写分离。 优化思路 分流、分流、分流,重要的事情说三遍,再牛逼的机器也抵挡不住高级别的并发。 限流、限流、限流,毕竟秒杀商品有限,防刷的前提下没有绝对的公平,根据每个服务的负载能力,设定流量极限。 缓存、缓存、缓存、尽量不要让大量请求穿透到DB层,活动开始前商品信息可以推送至分布式缓存。 异步、异步、异步,分析并识别出可以异步处理的逻辑,比如日志,缩短系统响应时间。 主备、主备、主备,如果有条件做好主备容灾方案也是非常有必要的(参考某年锤子的活动被攻击)。 最后,为了支撑更高的并发,追求更好的性能,可以对服务器的部署模型进行优化,部分请求走正常的秒杀流程,部分请求直接返回秒杀失败,缺点是开发部署时需要维护两套逻辑。 分层优化 前端优化:活动开始前生成静态商品页面推送缓存和CDN,静态文件(JS/CSS)请求推送至文件服务器和CDN。 网络优化:如果是全国用户,最好是BGP多线机房,减少网络延迟。 应用服务优化:Nginx最佳配置、Tomcat连接池优化、数据库配置优化、数据库连接池优化。 全链路压测 分析需压测业务场景涉及系统 协调各个压测系统资源并搭建压测环境 压测数据隔离以及监控(响应时间、吞吐量、错误率等数据以图表形式实时显示) 压测结果统计(平均响应时间、平均吞吐量等数据以图表形式在测试结束后显示) 优化单个系统性能、关联流程以及整个业务流程 整个压测优化过程就是一个不断优化不断改进的过程,事先通过测试不断发现问题,优化系统,避免问题,指定应急方案,才能让系统的稳定性和性能都得到质的提升。 代码案例 可能秒杀架构原理大家都懂,网上也有不少实现方式,但大多都是文字的描述,告诉你如何如何,什么加锁、缓存、队列之类。但很少全面有的案例告诉你如何去做,既然是从0到1,希望以下代码案例可以帮助到你。当然最终落实到生产,还有很长的路要走,要根据自己的业务进行编码,实施并部署。 你将会在代码案例中学到以下知识(不定期补充): 如何大家SpringBoot微服务 ThreadPoolExecutor线程池的使用 ReentrantLock和Synchronized的使用场景 数据库锁机制(悲观锁、乐观锁) 分布式锁(RedissLock、Zookeeper) 进程内消息队列(LinkedBlockingQueue、ArrayBlockingQueue、ConcurrentLinkedQueue) 分布式消息队列(Redis、Kafka) 代码结构: ├─src │ ├─main │ │ ├─java │ │ │ └─com │ │ │ └─itstyle │ │ │ └─seckill │ │ │ │ Application.java │ │ │ │ │ │ │ ├─common │ │ │ │ ├─api │ │ │ │ │ SwaggerConfig.java │ │ │ │ │ │ │ │ │ ├─config │ │ │ │ │ IndexController.java │ │ │ │ │ │ │ │ │ ├─dynamicquery │ │ │ │ │ DynamicQuery.java │ │ │ │ │ DynamicQueryImpl.java │ │ │ │ │ NativeQueryResultEntity.java │ │ │ │ │ │ │ │ │ ├─entity │ │ │ │ │ Result.java │ │ │ │ │ Seckill.java │ │ │ │ │ SuccessKilled.java │ │ │ │ │ │ │ │ │ ├─enums │ │ │ │ │ SeckillStatEnum.java │ │ │ │ │ │ │ │ │ ├─interceptor │ │ │ │ │ MyAdapter.java │ │ │ │ │ │ │ │ │ └─redis │ │ │ │ RedisConfig.java │ │ │ │ RedisUtil.java │ │ │ │ │ │ │ ├─distributedlock │ │ │ │ ├─redis │ │ │ │ │ RedissLockDemo.java │ │ │ │ │ RedissLockUtil.java │ │ │ │ │ RedissonAutoConfiguration.java │ │ │ │ │ RedissonProperties.java │ │ │ │ │ │ │ │ │ └─zookeeper │ │ │ │ ZkLockUtil.java │ │ │ │ │ │ │ ├─queue │ │ │ │ ├─jvm │ │ │ │ │ SeckillQueue.java │ │ │ │ │ TaskRunner.java │ │ │ │ │ │ │ │ │ ├─kafka │ │ │ │ │ KafkaConsumer.java │ │ │ │ │ KafkaSender.java │ │ │ │ │ │ │ │ │ └─redis │ │ │ │ RedisConsumer.java │ │ │ │ RedisSender.java │ │ │ │ RedisSubListenerConfig.java │ │ │ │ │ │ │ ├─repository │ │ │ │ SeckillRepository.java │ │ │ │ │ │ │ ├─service │ │ │ │ │ ISeckillDistributedService.java │ │ │ │ │ ISeckillService.java │ │ │ │ │ │ │ │ │ └─impl │ │ │ │ SeckillDistributedServiceImpl.java │ │ │ │ SeckillServiceImpl.java │ │ │ │ │ │ │ └─web │ │ │ SeckillController.java │ │ │ SeckillDistributedController.java │ │ │ │ │ ├─resources │ │ │ │ application.properties │ │ │ │ logback-spring.xml │ │ │ │ │ │ │ ├─sql │ │ │ │ seckill.sql │ │ │ │ │ │ │ ├─static │ │ │ └─templates │ │ └─webapp 思考改进 如何防止单个用户重复秒杀下单? 如何防止恶意调用秒杀接口? 如果用户秒杀成功,一直不支付该怎么办? 消息队列处理完成后,如果异步通知给用户秒杀成功? 如何保障 Redis、Zookeeper 、Kafka 服务的正常运行(高可用)? 高并发下秒杀业务如何做到不影响其他业务(隔离性)? 码云下载:从0到1构建分布式秒杀系统 可供参考 SpringBoot开发案例之整合Kafka实现消息队列 作者: 小柒2012 欢迎关注: https://blog.52itstyle.com
Spring Cloud是一系列框架的有序集合。它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。Spring并没有重复制造轮子,它只是将目前各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过Spring Boot风格进行再封装屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。 目录 ├1 公开课.avi ├10 4.6 Ribbon-3使用配置文件自定义Ribbon Client.avi ├11 4.7 Ribbon-4 Ribbon脱离Eureka使用.avi ├12 4.8 Feign-1 Feign的简介及基础使用.avi ├13 4.9 Feign-2覆写Feign的默认配置.avi ├14 4.10 Fegion-3覆写Fegion的默认配置及Fegion的日志.avi ├15 4.11 Fegion-4解决Fegion第一次请求timeout的问题.avi ├16 4.12 Eureka深入理解.avi ├17 4.13 Eureka常用配置详解.avi ├18 4.14 Eurek Ribbon Feign常见问题及解决.avi ├19 5.1超时机制,断路器模式简介.avi ├2 1.1 微服务架构概述.avi ├20 5.2 Hystrix简介及简单代码示例.avi ├20 5.2Hystrix简介及简单代码事例.avi ├21 Hystrix Health Indicator及Metrics Stream.avi ├22 5.4 Hystrix Health Indicator及Metrics Stream支持.avi ├23 5.5 Fegion的Hystrix支持.avi ├24 5.6如何禁用单个FegionClient的Hystrix的支持.avi ├25 5.7 Feign使用fallbackFactory属性打印fallback异常.avi ├26 5.8 Hystrix Dashboard的使用与常见问题总结.avi ├27 5.9 Turbine-上.avi ├28 5.9 Turbine-下.avi ├29 6.1 API Gateway简介.avi ├3.开始使用Spring Cloud实战微服务.avi ├30 6.2 Zuul简介及代码示例.avi ├31 6.3 Zuul指定path+serviceid.avi ├32 6.4 Zuul指定Path+url以及指定可用的服务节点时如何负载均衡.avi ├33 6.5 Zuul使用正则表达式指定路由规则.avi ├34 6.6 Zuul路由的strip-prefix与order.avi ├35 6.7 Zuul的各种姿势.avi ├36 6.8通过Zuul上传文件,禁用Zuul的Filters.avi ├37 6.9 Zuul的回退.avi ├38 6.10 使用Sidecar支持异构平台的微服务.avi ├39 6.10 Sidecar补充.avi ├4 服务提供者与服务消费者.avi ├4 服务提供者与服务消费者new.avi ├40 6.11-1 Zuul过滤器.avi ├41 6.11-2禁用Zuul的过滤器.avi ├42 7.1 Spring Cloud Config简介.avi ├43 7.2 编写Config Server.avi ├44 7.3 编写Config Client.avi ├45 7.4 Git仓库配置详解.avi ├46 7.5配置属性加解密之对称加密.avi ├47 7.6配置属性加解密之非对称加密.avi ├48 7.7 Spring Cloud Config与Eureka.avi ├49 7.8 Spring Cloud Config 与Eureka配合使用.avi ├5 4.1服务发现与服务注册.avi ├50 7.9 Spring Cloud Config配置属性刷新之手动刷新.avi ├51 7.10 Spring Cloud Config配置属性刷新之自动刷新.avi ├52 7.11 Spring Cloud Config配置属性刷新之自动刷新补充.avi ├53 7.12 Config Server的高可用.avi ├6 4.2Eureka简介与Eureka Server上.avi ├7 4.3将微服务注册到Eureka Server上.avi ├8 4.4 Ribbon-1 Ribbon的基本使用.avi ├9 4.5 Ribbon-2通过代码自定义配置ribbon.avi ├<课件> │ ├1.1微服务概述.ppt │ ├2.1开始使用SpringCloud实战微服务.zip │ ├3.1服务提供者与服务消费者.zip │ ├4.1服务发现与服务注册.zip │ ├5.1超时机制、断路器模式简介.zip │ ├Eureka.zip │ ├Ribbon.zip │ ├常见问题总结文档 本章代码.zip │ ├第五章-断路器配套源码.zip │ └使用SpringCloud实战微服务-公开课-课件.mmap.zip 公号内回复微服务,就可以免费领取(一个有温度的微信公众号,期待与你共同进步,分享美文,分享各种Java学习资源)。
企业场景 一般在企业内部(科帮网),开发、测试以及预生产都会有一套供开发以及测试人员使用的网络环境。运维人员会为每套环境的相关项目配置单独的Tomcat,然后开放一个端口,以 IP+Port 的形式访问。然而随着项目的增多,对于开发和测试人员记住如此多的内网地址,无疑是一件头疼的事情(当然你也可以使用浏览器书签管理器或者记录在某个地方)。但是你不永远不会确定,那天由于升级突然改了IP,我们可能又要重新撸一遍配置,所以内网域名还是非常有必要的。 内网域名具体有哪些优点: 方便记忆 变更IP,只需要修改DNS即可 服务器环境 192.168.1.170(开发)192.168.1.180(测试)192.168.1.190(预生产)192.168.1.125(DNS+Nginx) DNS安装 安装容器 为了方便,我们使用docker环境手动搭建一个DNS服务器。 选择andyshinn/dnsmasq的docker镜像,2.75版本,执行命令: docker run -d -p 53:53/tcp -p 53:53/udp --cap-add=NET_ADMIN --name dns-server andyshinn/dnsmasq:2.75 执行完毕以后,通过命令查看是否创建并运行成功: [root@test125 ~]# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 38ae71377ef1 andyshinn/dnsmasq:2.75 "dnsmasq -k" 22 hours ago Up About an hour 0.0.0.0:53->53/tcp, 0.0.0.0:53->53/udp dns-server 配置DNS 进入容器: docker exec -it dns-server /bin/sh 创建代理文件: vi /etc/resolv.dnsmasq 添加内容: nameserver 114.114.114.114 nameserver 8.8.8.8 新建本地解析规则配置: vi /etc/dnsmasqhosts 添加解析规则: 192.168.1.125 dev.52itstyle.com test.52itstyle.com sit.52itstyle.com 修改dnsmasq配置文件,指定使用上述两个我们自定义的配置文件: vi /etc/dnsmasq.conf 追加下述两个配置 resolv-file=/etc/resolv.dnsmasq addn-hosts=/etc/dnsmasqhosts 退出容器: exit 重启容器: docker restart dns-server Nginx安装 安装OpenResty之前需要下载一些必备的依赖: yum install readline-devel pcre-devel openssl-devel -y yum install wget perl gcc -y 下载最新版本: wget https://openresty.org/download/openresty-1.13.6.1.tar.gz 解压: tar -xvf openresty-1.13.6.1.tar.gz 安装配置: ./configure 您可以使用下面的命令来编译安装: make && make install 如果您的电脑支持多核 make 工作的特性, 您可以这样编译安装: make && make install -j2 为了方便启动,建立软连接: ln -s /usr/local/openresty/nginx/sbin/nginx /usr/sbin/nginx 在/usr/local/openresty/nginx/conf文件夹下创建vhosts目录,然后依次创建一下文件(演示文件,正式环境中会有多个项目转发)。 dev.52itstyle.com.conf: server{ listen 80; server_name dev.52itstyle.com; proxy_set_header Host $host; location /{ proxy_pass http://192.168.1.170:8080; } } test.52itstyle.com.conf: server{ listen 80; server_name test.52itstyle.com; proxy_set_header Host $host; location /{ proxy_pass http://192.168.1.180:8080; } } sit.52itstyle.com.conf: server{ listen 80; server_name sit.52itstyle.com; proxy_set_header Host $host; location /{ proxy_pass http://192.168.1.190:8080; } } 配置文件: vi /usr/local/openresty/nginx/conf/nginx.conf worker_processes 2; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; sendfile on; keepalive_timeout 65; #导入各个环境 include vhosts/*.conf; } 启动服务:执行 nginx命令即可。 本机验证 那么如何验证这些域名可以解析到我们的内网项目,只需要修改本机dns服务器地址即可: 配置完成后,我们就可以通过dev.52itstyle.com等相关域名访问我们的内网项目了。 当然,最好是可以直接修改路由器的DNS,这样就不用每个电脑都配置DNS了。 作者: 小柒2012 欢迎关注: https://blog.52itstyle.com
过节之前来一发,又是许久没整理笔记了,今天跟大家聊聊Docker如何搭建私有仓库的几种方式。首先我们来回顾一下之前讲到的Doker 平台的基本构成。 Doker 平台的基本构成 Docker 平台基本上由三部分组成: 客户端:用户使用 Docker 提供的工具(CLI 以及 API 等)来构建,上传镜像并发布命令来创建和启动容器 Docker 主机:从 Docker registry 上下载镜像并启动容器 Docker registry:Docker 镜像仓库,用于保存镜像,并提供镜像上传和下载 后面的文章会具体分析。 搭建方式 与Mavan的管理一样,Docker不仅提供了一个中央仓库,同时也允许我们使用registry搭建本地私有镜像仓库。 使用私有仓库有许多优点: 节省网络带宽,针对于每个镜像不用每个人都去中央仓库上面去下载,只需要从私有仓库中下载即可; 提供镜像资源利用,针对于公司内部使用的镜像,推送到本地的私有仓库中,以供公司内部相关人员使用。 方式一(registry镜像) 环境:为了测试安装方便,这里准备了一台装有Docker的云服务器。 搭建私有仓库: # 下载registry镜像 $ sudo docker pull registry # 通过该镜像启动一个容器 $ sudo docker run -d -p 8082:8082 registry # 映射镜像路径至宿机器、放置容器删除、镜像丢失: $ sudo docker run -d -p 8082:8082 -v /opt/data/registry:/tmp/registry registry 修改配置并重启Docker vi /etc/docker/daemon.json { "registry-mirrors": ["172.17.120.102:8082"], "insecure-registries":["172.17.120.102:8082"] } # 重启 docker 服务 systemctl restart docker 测试仓库Push/Pull: # 首先pull一个比较小的镜像(busybox)来测试 docker pull busybox # 修改一下该镜像的tag $ docker tag busybox 172.17.120.102:8080/busybox # 上传镜像到私有仓库。 $ docker push 172.17.120.102:8082/busybox 到此就搭建好了Docker私有仓库,但是如上搭建的仓库是不需要加密认证的,当然你可以通过证书或者Nginx实现认证访问。下面介绍一下基于Nexus 3搭建的Docker私有仓库。 方式二(Nexus 3) Nexus简介 Nexus是一个多功能的仓库管理系统,是企业常用的私有仓库服务器软件。目前常被用来作为Maven私服、Docker私服。 优点 安装简单,并且有官方Docker镜像 用户界面,并提供REST API 支持浏览、检索以及检查机制 支持npm与bower以及Raw repositories、NuGet repositories 总之Nexus物美价廉,又提供功能全面的oss版,加之支持种类众多的依赖管理,又可以统一管理docker镜像。 安装 参考之前写的一篇博客:本地私服仓库nexus3.3.1使用手册 。当然,这里我们有更简洁的安装方式,由于nexus3+依赖于JDK1.8,可能有不少企业系统上安装的还是1.7甚至是1.6版本,这里我们选择使用Docker镜像安装。 下载安装: # 下载nexus3镜像(pull前请更换镜像加速器,否则可能无法下载) $ sudo docker pull sonatype/nexus3 # 通过该镜像启动一个容器 $ sudo docker run -d -p 8081:8081 -p 8082:8082 --name nexus sonatype/nexus3 # 可能需要一些时间(2-3分钟)才能在新容器中启动该服务。一旦Nexus准备就绪,您可以确定日志以确定结果: $ sudo docker logs -f nexus # 测试 如果出现pong说明启动成功 $ curl -u admin:admin123 http://localhost:8081/service/metrics/ping 注意事项: 可能会出现无法启动的问题,由于云服务器只有1G内存,剩余也有几十MB的样式,显然是无法跑起来的。 Nexus的安装是/opt/sonatype/nexus。 持久目录,/nexus-data用于配置,日志和存储。该目录需要由作为UID 200运行的Nexus进程写入。 环境变量用于将JVM参数传递给启动脚本 $ docker run -d -p 8081:8081 --name nexus -e INSTALL4J_ADD_VM_PARAMS="-Xms2g -Xmx2g -XX:MaxDirectMemorySize=3g -Djava.util.prefs.userRoot=/some-other-dir" sonatype/nexus3 控制Nexus访问目录,NEXUS_CONTEXT,默认为/ docker run -d -p 8081:8081 --name nexus -e NEXUS_CONTEXT=nexus sonatype/nexus3 持久数据 $ mkdir /opt/data/nexus-data && chown -R 200 /opt/data/nexus-data $ docker run -d -p 8081:8081 --name nexus -v /opt/data/nexus-data:/nexus-data sonatype/nexus3 创建本地仓库: Nexus配置: 项目 地址端口 Nexus UI 8081 private repo 8082 URL http://192.168.1.180:8081/ 作者: 小柒2012 欢迎关注: https://blog.52itstyle.com
前言 最近在做一个原始成绩统计的功能,用户通过前台设置相关参数,后台实时统计并返回数据。相对来说统计功能点还是比较多的,这里大体罗列一下。 个人排名 本次测试的优良线、及格线、低分线 各个班级的排名人数(1-25、26-50 类比等等) 各个班级的前X名人数统计(前10、前20 类比等等) 各个班级的分数段学生人数统计(150-140、139-130 类比等等) 最好的用户体验,就是每一个操作都可以实时的展示数据,3秒之内应该是用户的忍受范围之内的了,所以做一款产品不仅要考虑用户交互设计,后端的优化也是比不可少的。 大家可以简单的看下以上这5项统计数据,总体来说,统计量还是不少的。最主要的还是要实时、实时、实时(重要的事情说三遍),显然定时任务是不现实的。 改造前 程序逻辑 改造后 程序逻辑 代码实现 StatsDemo伪代码: /** * 多任务并行统计 * 创建者 科帮网 * 创建时间 2018年4月16日 */ public class StatsDemo { final static SimpleDateFormat sdf = new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss"); final static String startTime = sdf.format(new Date()); public static void main(String[] args) throws InterruptedException { CountDownLatch latch = new CountDownLatch(5);// 两个赛跑者 Stats stats1 = new Stats("任务A", 1000, latch); Stats stats2 = new Stats("任务B", 2000, latch); Stats stats3 = new Stats("任务C", 2000, latch); Stats stats4 = new Stats("任务D", 2000, latch); Stats stats5 = new Stats("任务E", 2000, latch); stats1.start();//任务A开始执行 stats2.start();//任务B开始执行 stats3.start();//任务C开始执行 stats4.start();//任务D开始执行 stats5.start();//任务E开始执行 latch.await();// 等待所有人任务结束 System.out.println("所有的统计任务执行完成:" + sdf.format(new Date())); } static class Stats extends Thread { String statsName; int runTime; CountDownLatch latch; public Stats(String statsName, int runTime, CountDownLatch latch) { this.statsName = statsName; this.runTime = runTime; this.latch = latch; } public void run() { try { System.out.println(statsName+ " do stats begin at "+ startTime); //模拟任务执行时间 Thread.sleep(runTime); System.out.println(statsName + " do stats complete at "+ sdf.format(new Date())); latch.countDown();//单次任务结束,计数器减一 } catch (InterruptedException e) { e.printStackTrace(); } } } } 由于要同步返回统计数据,这里我们使用到了CountDownLatch类,它是Java5中新增的一个并发工具类,其使用非常简单,参考上面的伪代码给出了详细的使用步骤。 CountDownLatch用于同步一个或多个任务,强制他们等待由其他任务执行的一组操作完成。CountDownLatch典型的用法是将一个程序分为N个互相独立的可解决任务,并创建值为N的CountDownLatch。当每一个任务完成时,都会在这个锁存器上调用countDown,等待问题被解决的任务调用这个锁存器的await,将他们自己拦住,直至锁存器计数结束。 具体的源码解读,大家可以参考:【JUC】JDK1.8源码分析之CountDownLatch 项目源码:https://gitee.com/52itstyle/spring-data-jpa 作者: 小柒2012 欢迎关注: https://blog.52itstyle.com
前言 很多初学者,甚至是工作1-3年的小伙伴们都可能弄不明白?servlet Struts1 Struts2 springmvc 哪些是单例,哪些是多例,哪些是线程安全? 在谈这个话题之前,我们先了解一下Java中相关的变量类型以及内存模型JMM。 变量类型 类变量:独立于方法之外的变量,用 static 修饰。 局部变量:类的方法中的变量。 实例变量(全局变量):独立于方法之外的变量,不过没有 static 修饰。 JAVA的局部变量 局部变量声明在方法、构造方法或者语句块中; 局部变量在方法、构造方法、或者语句块被执行的时候创建,当它们执行完成后,变量将会被销毁; 访问修饰符不能用于局部变量; 局部变量只在声明它的方法、构造方法或者语句块中可见; 局部变量是在栈上分配的。 局部变量没有默认值,所以局部变量被声明后,必须经过初始化,才可以使用。 JAVA的实例变量 实例变量声明在一个类中,但在方法、构造方法和语句块之外; 当一个对象被实例化之后,每个实例变量的值就跟着确定; 实例变量在对象创建的时候创建,在对象被销毁的时候销毁; 实例变量的值应该至少被一个方法、构造方法或者语句块引用,使得外部能够通过这些方式获取实例变量信息; 实例变量可以声明在使用前或者使用后; 访问修饰符可以修饰实例变量; 实例变量对于类中的方法、构造方法或者语句块是可见的。一般情况下应该把实例变量设为私有。通过使用访问修饰符可以使实例变量对子类可见; 实例变量具有默认值。数值型变量的默认值是0,布尔型变量的默认值是false,引用类型变量的默认值是null。变量的值可以在声明时指定,也可以在构造方法中指定;实例变量可以直接通过变量名访问。但在静态方法以及其他类中,就应该使用完全限定名:ObejectReference.VariableName。 JAVA的类变量(静态变量) 类变量也称为静态变量,在类中以static关键字声明,但必须在方法构造方法和语句块之外。 无论一个类创建了多少个对象,类只拥有类变量的一份拷贝。 静态变量除了被声明为常量外很少使用。常量是指声明为public/private,final和static类型的变量。常量初始化后不可改变。 静态变量储存在静态存储区。经常被声明为常量,很少单独使用static声明变量。 静态变量在程序开始时创建,在程序结束时销毁。 与实例变量具有相似的可见性。但为了对类的使用者可见,大多数静态变量声明为public类型。 默认值和实例变量相似。数值型变量默认值是0,布尔型默认值是false,引用类型默认值是null。变量的值可以在声明的时候指定,也可以在构造方法中指定。此外,静态变量还可以在静态语句块中初始化。 静态变量可以通过:ClassName.VariableName的方式访问。 类变量被声明为public static final类型时,类变量名称一般建议使用大写字母。如果静态变量不是public和final类型,其命名方式与实例变量以及局部变量的命名方式一致。 Java的内存模型JMM Java的内存模型JMM(Java Memory Model)JMM主要是为了规定了线程和内存之间的一些关系。根据JMM的设计,系统存在一个主内存(Main Memory),Java中所有实例变量都储存在主存中,对于所有线程都是共享的。每条线程都有自己的工作内存(Working Memory),工作内存由缓存和堆栈两部分组成,缓存中保存的是主存中变量的拷贝,缓存可能并不总和主存同步,也就是缓存中变量的修改可能没有立刻写到主存中;堆栈中保存的是线程的局部变量,线程之间无法相互直接访问堆栈中的变量。根据JMM,我们可以将论文中所讨论的Servlet实例的内存模型抽象为下图所示的模型。 线程安全 Servlet Servlet/JSP技术和ASP、PHP等相比,由于其多线程运行而具有很高的执行效率。由于Servlet/JSP默认是以多线程模式执行的,所以,在编写代码时需要非常细致地考虑多线程的安全性问题。然而,很多人编写Servlet/JSP程序时并没有注意到多线程安全性的问题,这往往造成编写的程序在少量用户访问时没有任何问题,而在并发用户上升到一定值时,就会经常出现一些莫明其妙的问题。 Servlet的多线程机制 Servlet体系结构是建立在Java多线程机制之上的,它的生命周期是由Web容器负责的。当客户端第一次请求某个Servlet 时,Servlet容器将会根据web.xml配置文件实例化这个Servlet类。当有新的客户端请求该Servlet时,一般不会再实例化该 Servlet类,也就是有多个线程在使用这个实例。Servlet容器会自动使用线程池等技术来支持系统的运行,如下图所示。 这样,当两个或多个线程同时访问同一个Servlet时,可能会发生多个线程同时访问同一资源的情况,数据可能会变得不一致。所以在用Servlet构建的Web应用时如果不注意线程安全的问题,会使所写的Servlet程序有难以发现的错误。 Servlet的线程安全问题 Servlet的线程安全问题主要是由于实例变量使用不当而引起的,这里以一个现实的例子来说明。 /** * 模拟用户AB在同时执行不同的动作 * 先执行 http://localhost:8080/concurrent?username=A&action=play * 稍后执行 http://localhost:8080/concurrent?username=B&action=eat */ public class Concurrent extends HttpServlet { private static final long serialVersionUID = 1L; private String action = "";//动作 public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { try { String username = request.getParameter("username"); action = request.getParameter("username"); Thread.sleep(5000); //为了突出并发问题,在这设置一个延时 //如果不出意外,应该用户AB都在吃饭 System.out.println("用户:"+username+"在"+action); } catch (Exception e) { } } } Struts1 首先,明确一点Sturts1 action是单例模式,线程是不安全的。Struts1使用的ActionServlet是单例的,既然是单例,当使用实例变量的时候就会有线程安全的问题。所有一般在开发中试禁止使用实例变量的。 Struts2 struts2使用的是actionContext,都是使用里面的实例变量,让struts2自动匹配成对象的。每次处理一个请求,struts2就会实例化一个对象,这样就不会有线程安全的问题了。 需要注意的是,如果struts2+spring来管理注入的时候,不要把Action设置成单例,否则会出问题的。当然现在很少有项目使用struts2了。 SpringMVC SpringMVC的controller默认是单例模式的,所以也会有多线程并发的问题。 总结 servlet Struts1 SpringMvc 是线程不安全的,当然如果你不使用实例变量也就不存在线程安全的问题了。 Struts2 是线程安全的,当然前提情况是,Action 不交给 spring管理,并且不设置为单例。 SpringMvc 的 Bean 可以设置成多例变成线程安全,但是一定程度上回影响系统性能。 作者: 小柒 出处: https://blog.52itstyle.com 分享是快乐的,也见证了个人成长历程,文章大多都是工作经验总结以及平时学习积累,基于自身认知不足之处在所难免,也请大家指正,共同进步。
基于spring-boot 2.x + quartz 的CRUD任务管理系统,适用于中小项目。 基于spring-boot +quartz 的CRUD任务管理系统: https://gitee.com/52itstyle/spring-boot-quartz 开发环境 JDK1.8、Maven、Eclipse 技术栈 SpringBoot2.0.1、thymeleaf3.0.9、quartz2.3.0、iview、vue、layer、AdminLTE、bootstrap 启动说明 项目使用的数据库为MySql,选择resources/sql中的tables_mysql_innodb.sql文件初始化数据库信息。 在resources/application.properties文件中替换为自己的数据源。 运行Application main方法启动项目,项目启动会自动创建一个测试任务 见:com.itstyle.quartz.config.TaskRunner.java。 项目访问地址:http://localhost:8080/task 项目截图 版本区别(spring-boot 1.x and 2.x) 这里只是针对这两个项目异同做比较,当然spring-boot 2.x版本升级还有不少需要注意的地方。 项目名称配置: # spring boot 1.x server.context-path=/quartz # spring boot 2.x server.servlet.context-path=/quartz thymeleaf配置: #spring boot 1.x spring.thymeleaf.mode=LEGACYHTML5 #spring boot 2.x spring.thymeleaf.mode=HTML Hibernate配置: # spring boot 2.x JPA 依赖 Hibernate 5 # Hibernate 4 naming strategy fully qualified name. Not supported with Hibernate 5. spring.jpa.hibernate.naming.strategy = org.hibernate.cfg.ImprovedNamingStrategy # stripped before adding them to the entity manager) spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5Dialect # Hibernate 5 spring.jpa.hibernate.naming.implicit-strategy=org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl quartz配置: # spring boot 2.x 已集成Quartz,无需自己配置 spring.quartz.job-store-type=jdbc spring.quartz.properties.org.quartz.scheduler.instanceName=clusteredScheduler spring.quartz.properties.org.quartz.scheduler.instanceId=AUTO spring.quartz.properties.org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX spring.quartz.properties.org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate spring.quartz.properties.org.quartz.jobStore.tablePrefix=QRTZ_ spring.quartz.properties.org.quartz.jobStore.isClustered=true spring.quartz.properties.org.quartz.jobStore.clusterCheckinInterval=10000 spring.quartz.properties.org.quartz.jobStore.useProperties=false spring.quartz.properties.org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool spring.quartz.properties.org.quartz.threadPool.threadCount=10 spring.quartz.properties.org.quartz.threadPool.threadPriority=5 spring.quartz.properties.org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread=true 默认首页配置: /** * 配置首页 spring boot 1.x * 创建者 小柒2012 * 创建时间 2017年9月7日 */ @Configuration public class MyAdapter extends WebMvcConfigurerAdapter{ @Override public void addViewControllers( ViewControllerRegistry registry ) { registry.addViewController( "/" ).setViewName( "forward:/login.shtml" ); registry.setOrder( Ordered.HIGHEST_PRECEDENCE ); super.addViewControllers( registry ); } } /** * 配置首页(在SpringBoot2.0及Spring 5.0 WebMvcConfigurerAdapter以被废弃 * 建议实现WebMvcConfigurer接口) * 创建者 小柒2012 * 创建时间 2018年4月10日 */ @Configuration public class MyAdapter implements WebMvcConfigurer{ @Override public void addViewControllers( ViewControllerRegistry registry ) { registry.addViewController( "/" ).setViewName( "forward:/login.shtml" ); registry.setOrder( Ordered.HIGHEST_PRECEDENCE ); } } 待解决问题: /** * Set a strategy for handling the query results. This can be used to change * "shape" of the query result. * * @param transformer The transformer to apply * * @return this (for method chaining) * * @deprecated (since 5.2) * @todo develop a new approach to result transformers */ @Deprecated Query<R> setResultTransformer(ResultTransformer transformer); hibernate 5.2 废弃了 setResultTransformer,说是要开发一种新的获取集合方法,显然目前还没实现,处于TODO状态。 项目源码: https://gitee.com/52itstyle/spring-boot-task 作者: 小柒2012 欢迎关注: https://blog.52itstyle.com
前段时间做了一个基于SpringBoot和Quartz任务管理系统(脚手架而已),很多功能不是特别完善,由于工作原因,断断续续一直在更新中,码云上有个小伙伴提问说:Job中service自动注入报错怎么解决?正好之前做的项目中有使用到注入相关的功能,顺便也集成进去。 缘由 简单来说就是quartz中的Job是在quartz中实例化出来的,不受spring的管理,所以就导致注入不进去了。 解决 定义SpringJobFactory类: /** * 解决spring bean注入Job的问题 */ @Component public class SpringJobFactory extends AdaptableJobFactory { @Autowired private AutowireCapableBeanFactory capableBeanFactory; @Override protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception { // 调用父类的方法 Object jobInstance = super.createJobInstance(bundle); // 进行注入 capableBeanFactory.autowireBean(jobInstance); return jobInstance; } } quartz配置: /** * quartz配置 * 创建者 科帮网 * 创建时间 2018年4月3日 */ @Configuration public class SchedulerConfig { @Autowired private SpringJobFactory springJobFactory; @Bean(name="SchedulerFactory") public SchedulerFactoryBean schedulerFactoryBean() throws IOException { SchedulerFactoryBean factory = new SchedulerFactoryBean(); factory.setAutoStartup(true); factory.setStartupDelay(5);//延时5秒启动 factory.setQuartzProperties(quartzProperties()); //注意这里是重点 factory.setJobFactory(springJobFactory); return factory; } @Bean public Properties quartzProperties() throws IOException { PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean(); propertiesFactoryBean.setLocation(new ClassPathResource("/quartz.properties")); propertiesFactoryBean.afterPropertiesSet(); return propertiesFactoryBean.getObject(); } /* * quartz初始化监听器 */ @Bean public QuartzInitializerListener executorListener() { return new QuartzInitializerListener(); } /* * 通过SchedulerFactoryBean获取Scheduler的实例 */ @Bean(name="Scheduler") public Scheduler scheduler() throws IOException { return schedulerFactoryBean().getScheduler(); } } 测试任务案例TestJob: /** * 实现序列化接口、防止重启应用出现quartz Couldn't retrieve job because a required class was not found 的问题 */ public class TestJob implements Job,Serializable { private static final long serialVersionUID = 1L; @Autowired private IJobService jobService; @Override public void execute(JobExecutionContext context) throws JobExecutionException { System.out.println(jobService);//注入jobService 执行相关业务操作 System.out.println("任务执行成功"); } } 项目源码: https://gitee.com/52itstyle/spring-boot-quartz
基于spring-boot+quartz的CRUD动态任务管理系统,适用于中小项目。 开发环境 JDK1.7、Maven、Eclipse 技术栈 SpringBoot1.5.2、thymeleaf、quartz2.3.0、iview、vue、layer、AdminLTE、bootstrap 启动说明 项目使用的数据库为MySql,选择resources/sql中的tables_mysql_innodb.sql文件初始化数据库信息。 在resources/application.properties文件中替换为自己的数据源。 运行Application main方法,启动项目。 项目访问地址:http://localhost:8080/quartz API接口地址:http://localhost:8080/quartz/swagger-ui.html 友情提示 由于工作原因,项目正在完善中(仅供参考),随时更新日志。 项目截图 已实现功能 任务列表 任务新增和修改 任务执行 待集成功能 系统登录以及权限管理 任务停止和开启 任务列表搜索以及分页 项目源码: https://gitee.com/52itstyle/spring-boot-quartz
前言 有人说 从 jdbc->jdbctemplate->hibernation/mybatis 再到 jpa,真当开发人员的学习时间不要钱?我觉得到 h/m 这一级的封装已经有点过了,再往深处走就有病了。 还有人说JPA 很反人类(一个面试官),还举了一个很简单举了例子说:一个数据库如果有 50 个字段,那你写各种条件查询不是要写很多?就是应该用类似 SQL 的方式来查询啊? 其实在我看来,存在即合理,人们总是向着好的方向去发展,学习什么不需要成本,底层语言牛逼倒是去学啊,不还是看不懂,弄不明白。很多知识对于程序员来说,都是一通百通,查询文档就是了,最主要的是能方便以后的开发即可。 对于反人类这一说,只能说 to young to simple,JPA的初衷肯定也不会是让你写一个几十个字段的查询,顶多一到两个而已,非要这么极端?再说JPA也是提供了EntityManager来实现SQL或者HQL语句查询的不是,JPA本质上还是集成了Hibernate的很多优点的。 进阶查询 需求: 学生表(app_student)、班级表(app_class)、当然表结构比较简单,比如这时候我们需要查询学生列表,但是需要同时查询班级表的一些数据,并以JSON或者实体的方式返回给调用者。 本次需求,主要实现JPA的以下几个特性: 封装EntityManager基类 多表查询返回一个List 多表查询返回一个Map 多表查询返回一个实体 Entitymanager的核心概念图: 实现 班级表: @Entity @Table(name = "app_class") public class AppClass { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "id", nullable = false) private Integer id; private String className; private String teacherName; //忽略部分代码 } 学生表: @Entity @Table(name = "app_student") public class AppStudent { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "id", nullable = false) private Integer id; private Integer classId; private String name; private Integer age; //忽略部分代码 } 封装接口 DynamicQuery: /** * 扩展SpringDataJpa, 支持动态jpql/nativesql查询并支持分页查询 * 使用方法:注入ServiceImpl * 创建者 张志朋 * 创建时间 2018年3月8日 */ public interface DynamicQuery { public void save(Object entity); public void update(Object entity); public <T> void delete(Class<T> entityClass, Object entityid); public <T> void delete(Class<T> entityClass, Object[] entityids); /** * 查询对象列表,返回List * @param resultClass * @param nativeSql * @param params * @return List<T> * @Date 2018年3月15日 * 更新日志 * 2018年3月15日 张志朋 首次创建 * */ <T> List<T> nativeQueryList(String nativeSql, Object... params); /** * 查询对象列表,返回List<Map<key,value>> * @param nativeSql * @param params * @return List<T> * @Date 2018年3月15日 * 更新日志 * 2018年3月15日 张志朋 首次创建 * */ <T> List<T> nativeQueryListMap(String nativeSql,Object... params); /** * 查询对象列表,返回List<组合对象> * @param resultClass * @param nativeSql * @param params * @return List<T> * @Date 2018年3月15日 * 更新日志 * 2018年3月15日 张志朋 首次创建 * */ <T> List<T> nativeQueryListModel(Class<T> resultClass, String nativeSql, Object... params); } 封装实现 DynamicQueryImpl: /** * 动态jpql/nativesql查询的实现类 * 创建者 张志朋 * 创建时间 2018年3月8日 */ @Repository public class DynamicQueryImpl implements DynamicQuery { Logger logger = LoggerFactory.getLogger(DynamicQueryImpl.class); @PersistenceContext private EntityManager em; public EntityManager getEntityManager() { return em; } @Override public void save(Object entity) { em.persist(entity); } @Override public void update(Object entity) { em.merge(entity); } @Override public <T> void delete(Class<T> entityClass, Object entityid) { delete(entityClass, new Object[] { entityid }); } @Override public <T> void delete(Class<T> entityClass, Object[] entityids) { for (Object id : entityids) { em.remove(em.getReference(entityClass, id)); } } private Query createNativeQuery(String sql, Object... params) { Query q = em.createNativeQuery(sql); if (params != null && params.length > 0) { for (int i = 0; i < params.length; i++) { q.setParameter(i + 1, params[i]); // 与Hiberante不同,jpa // query从位置1开始 } } return q; } @SuppressWarnings("unchecked") @Override public <T> List<T> nativeQueryList(String nativeSql, Object... params) { Query q = createNativeQuery(nativeSql, params); q.unwrap(SQLQuery.class).setResultTransformer(Transformers.TO_LIST); return q.getResultList(); } @SuppressWarnings("unchecked") @Override public <T> List<T> nativeQueryListModel(Class<T> resultClass, String nativeSql, Object... params) { Query q = createNativeQuery(nativeSql, params);; q.unwrap(SQLQuery.class).setResultTransformer(Transformers.aliasToBean(resultClass)); return q.getResultList(); } @SuppressWarnings("unchecked") @Override public <T> List<T> nativeQueryListMap(String nativeSql, Object... params) { Query q = createNativeQuery(nativeSql, params); q.unwrap(SQLQuery.class).setResultTransformer(Transformers.ALIAS_TO_ENTITY_MAP); return q.getResultList(); } } 业务 IStudentService: public interface IStudentService { /** * 返回List<Object[]> * @Author 科帮网 * @return List<Object[]> * @Date 2018年3月28日 * 更新日志 * 2018年3月28日 科帮网 首次创建 * */ List<Object[]> listStudent(); /** * 返回List<Student> * @Author 科帮网 * @return List<Student> * @Date 2018年3月28日 * 更新日志 * 2018年3月28日 科帮网 首次创建 * */ List<Student> listStudentModel(); /** * List<Map<Object, Object>> * @Author 科帮网 * @return List<Map<Object,Object>> * @Date 2018年3月28日 * 更新日志 * 2018年3月28日 科帮网 首次创建 * */ List<Map<Object, Object>> listStudentMap(); } 业务实现 StudentServiceImpl: @Service public class StudentServiceImpl implements IStudentService { @Autowired private DynamicQuery dynamicQuery; @Override public List<Object[]> listStudent() { String nativeSql = "SELECT s.id AS studentId,c.id AS classId,c.class_name AS className,c.teacher_name AS teacherName,s.name,s.age FROM app_student s,app_class c"; List<Object[]> list = dynamicQuery.nativeQueryList(nativeSql, new Object[]{}); return list; } @Override public List<Student> listStudentModel() { String nativeSql = "SELECT s.id AS studentId,c.id AS classId,c.class_name AS className,c.teacher_name AS teacherName,s.name,s.age FROM app_student s,app_class c"; List<Student> list = dynamicQuery.nativeQueryListModel(Student.class, nativeSql, new Object[]{}); return list; } @Override public List<Map<Object,Object>> listStudentMap() { String nativeSql = "SELECT s.id AS studentId,c.id AS classId,c.class_name AS className,c.teacher_name AS teacherName,s.name,s.age FROM app_student s,app_class c"; List<Map<Object,Object>> list = dynamicQuery.nativeQueryListMap(nativeSql, new Object[]{}); return list; } } 接口测试: @Api(tags ="测试接口") @RestController @RequestMapping("/test") public class StudentController { private final static Logger LOGGER = LoggerFactory.getLogger(StudentController.class); @Autowired private IStudentService studentService; @ApiOperation(value="学生List") @PostMapping("/list") public Result list(HttpServletRequest request){ LOGGER.info("学生List"); List<Object[]> list = studentService.listStudent(); return Result.ok(list); } @ApiOperation(value="学生Map") @PostMapping("/listMap") public Result listMap(HttpServletRequest request){ LOGGER.info("学生Map"); List<Map<Object, Object>> list = studentService.listStudentMap(); return Result.ok(list); } @ApiOperation(value="学生Model") @PostMapping("/listModel") public Result listModel(HttpServletRequest request){ LOGGER.info("学生Model"); List<Student> list = studentService.listStudentModel(); return Result.ok(list); } } Swagger2测试 返回List< Object[] >: 返回List< Map< Object, Object > >: 返回List< Student >: 源码:https://gitee.com/52itstyle/spring-data-jpa 作者: 小柒 出处: https://blog.52itstyle.com 分享是快乐的,也见证了个人成长历程,文章大多都是工作经验总结以及平时学习积累,基于自身认知不足之处在所难免,也请大家指正,共同进步。
描述 MySQL 5.6 SQL数据库服务器Docker镜像,此容器映像包含用于OpenShift的MySQL 5.6 SQL数据库服务器和一般用法。用户可以选择RHEL和基于CentOS的图像。然后CentOS镜像可以在Docker Hub上以centos / mysql-56-centos7的形式获得。 用法 查找镜像: docker search mysql 获取镜像: docker pull docker.io/centos/mysql-56-centos7 如果您只想设置必需的环境变量而不将数据库存储在主机目录中,请执行以下命令: docker run -d --name app_mysql -p 3307:3306 -e MYSQL_ROOT_PASSWORD=123456 docker.io/centos/mysql-56-centos7 如果你希望你的数据库在容器执行过程中保持持久性,请执行以下命令: # 创建数据存储目录 和配置文件目录 mkdir -p ~/home/mysql/data ~/home/mysql/cnf.d # 分别赋予读写权限 chmod +766 data/ chmod +766 cnf.d/ # 创建并运行容器 docker run -d --name app_mysql -p 3307:3306 -v /home/mysql/cnf.d:/etc/my.cnf.d -v /home/mysql/data:/var/lib/mysql/data -e MYSQL_ROOT_PASSWORD=123456 docker.io/centos/mysql-56-centos7 命令说明: -p 3307:3306:将容器的3306端口映射到主机的3307端口 -v /home/mysql/cnf.d:/etc/my.cnf.d:主机目录:容器目录 -v /home/mysql/data:/var/lib/mysql/data:主机目录:容器目录 -e MYSQL_ROOT_PASSWORD=123456:初始化root用户的密码 查看容器运行情况: docker ps 进入容器: docker exec -it app_mysql bash 命令说明: -d :分离模式: 在后台运行 -i :即使没有附加也保持STDIN 打开 -t :分配一个伪终端 作者: 小柒 出处: https://blog.52itstyle.com 分享是快乐的,也见证了个人成长历程,文章大多都是工作经验总结以及平时学习积累,基于自身认知不足之处在所难免,也请大家指正,共同进步。
前言 书接上回的《SpringBoot开发案例之微信小程序文件上传》,正常的业务流程是,口语测评需要学生通过前端微信小程序录入一段音频,通过调用第三方音频处理服务商进行评分,然后服务端对原始录音、标准录音以及评分信息进行存储,最终呈现给学生并用于复看以及复读。 微信端 index.wxml: <button bindtap="start" class='btn'>开始录音</button> <button bindtap="pause" class='btn'>暂停录音</button> <button bindtap="stop" class='btn'>停止录音</button> <button bindtap="play" class='btn'>播放录音</button> <button bindtap="upload" class='btn'>上传录音</button> index.wxss: .btn{ margin-top: 10rpx; } index.js: //录音管理 const recorderManager = wx.getRecorderManager() //音频组件控制 const innerAudioContext = wx.createInnerAudioContext() var tempFilePath; Page({ data: { }, //开始录音的时候 start: function () { const options = { duration: 10000,//指定录音的时长,单位 ms sampleRate: 16000,//采样率 numberOfChannels: 1,//录音通道数 encodeBitRate: 96000,//编码码率 format: 'mp3',//音频格式,有效值 aac/mp3 frameSize: 50,//指定帧大小,单位 KB } //开始录音 recorderManager.start(options); recorderManager.onStart(() => { console.log('recorder start') }); //错误回调 recorderManager.onError((res) => { console.log(res); }) }, //暂停录音 pause: function () { recorderManager.onPause(); console.log('暂停录音') }, //停止录音 stop: function () { recorderManager.stop(); recorderManager.onStop((res) => { this.tempFilePath = res.tempFilePath; console.log('停止录音', res.tempFilePath) const { tempFilePath } = res }) }, //播放声音 play: function () { innerAudioContext.autoplay = true innerAudioContext.src = this.tempFilePath, innerAudioContext.onPlay(() => { console.log('开始播放') }) innerAudioContext.onError((res) => { console.log(res.errMsg) console.log(res.errCode) }) }, //上传录音 upload:function(){ wx.uploadFile({ url: "https://xxx.com/fileUpload",//演示域名、自行配置 filePath: this.tempFilePath, name: 'file', header: { "Content-Type": "multipart/form-data" }, formData: { userId: 12345678 //附加信息为用户ID }, success: function (res) { console.log(res); wx.showToast({ title: '上传成功', icon: 'success', duration: 2000 }) }, fail: function (res) { console.log(res); }, complete: function (res) { } }) }, onLoad: function () { }, }) 上传服务 /** * 口语测试 * 创建者 柒 * 创建时间 2018年3月13日 */ @Api(tags ="口语测试接口") @RestController @RequestMapping("/test") public class TestController { private final static Logger LOGGER = LoggerFactory.getLogger(WechatController.class); @Value("${web.upload.path}") private String uploadPath; @ApiOperation(value="上传文件(小程序)") @PostMapping("/fileUpload") public String upload(HttpServletRequest request, @RequestParam("file")MultipartFile[] files){ LOGGER.info("上传测试"); //多文件上传 if(files!=null && files.length>=1) { BufferedOutputStream bw = null; try { String fileName = files[0].getOriginalFilename(); //判断是否有文件(实际生产中要判断是否是音频文件) if(StringUtils.isNoneBlank(fileName)) { //创建输出文件对象 File outFile = new File(uploadPath + UUID.randomUUID().toString()+ FileUtil.getFileType(fileName)); //拷贝文件到输出文件对象 FileUtils.copyInputStreamToFile(files[0].getInputStream(), outFile); } } catch (Exception e) { e.printStackTrace(); } finally { try { if(bw!=null) {bw.close();} } catch (IOException e) { e.printStackTrace(); } } } return "success"; } }
前言 在这个营销的时代,短链接和二维码是企业进行营销中非常重要的工具,不仅仅是缩短了链接,而且还可以通过扩展获得更多的数据,诸如点击数、下载量、来源以及时间等等。 网上搜寻了一下比较有名有U.NU和0x3.me,但前者只能统计点击次数,而且不能修改链接,后者功能丰富,但确是收费商业网站。 环境搭建 本安装指南将帮助您安装Polr 2.0的最新版本Polr 2.0。Polr 是一个开源软件、世界上最好的语言,功能还算强大。 功能包括 修改缩短的域名 统计功能(来源,时间) API支持 二维码生成 服务器要求 Apache, nginx, IIS, or lighttpd (Apache preferred) PHP >= 5.5.9 MariaDB or MySQL >= 5.5, SQLite alternatively composer PHP requirements:OpenSSL PHP Extension PDO PHP ExtensionPDO MySQL Driver (php5-mysql on Debian & Ubuntu, php5x-pdo_mysql on FreeBSD)Mbstring PHP ExtensionTokenizer PHP ExtensionJSON PHP ExtensionPHP curl extension 安装PHP PHP http://php.net/downloads.php wget http://ba1.php.net/get/php-5.6.34.tar.gz/from/this/mirror 安装libxml2和libxml2-devel yum -y install libxml2 yum -y install libxml2-devel 因为不同的操作系统环境,系统安装开发环境包的完整程度也不相同,所以建议安装操作系统的时候做必要选择,也可以统一执行一遍所有的命令,将没有安装的组件安装好,如果已经安装了可能会进行升级,版本完全一致则不会进行任何操作,命令除上面2个之外,汇总如下: yum -y install libxml2 yum -y install libxml2-devel yum -y install openssl yum -y install openssl-devel yum -y install curl yum -y install curl-devel yum -y install libjpeg yum -y install libjpeg-devel yum -y install libpng yum -y install libpng-devel yum -y install freetype yum -y install freetype-devel yum -y install pcre yum -y install pcre-devel yum -y install libxslt yum -y install libxslt-devel yum -y install bzip2 yum -y install bzip2-devel yum -y install libjpeg libjpeg-devel libpng libpng-devel freetype freetype-devel mysql pcre-devel 安装完成之后,执行配置: ./configure --prefix=/usr/local/php --with-curl --with-freetype-dir --with-gd --with-gettext --with-iconv-dir --with-kerberos --with-libdir=lib64 --with-libxml-dir --with-mysqli --with-openssl --with-pcre-regex --with-pdo-mysql --with-pdo-sqlite --with-pear --with-png-dir --with-jpeg-dir --with-xmlrpc --with-xsl --with-zlib --with-bz2 --with-mhash --enable-fpm --enable-bcmath --enable-libxml --enable-inline-optimization --enable-gd-native-ttf --enable-mbregex --enable-mbstring --enable-opcache --enable-pcntl --enable-shmop --enable-soap --enable-sockets --enable-sysvsem --enable-sysvshm --enable-xml --enable-zip 然后执行编译: make 编译时间可能会有点长,编译完成之后,执行安装: make install php的默认安装位置上面已经指定为/usr/local/php,接下来配置相应的文件: cp php.ini-development /usr/local/php/lib/php.ini cp /usr/local/php/etc/php-fpm.conf.default /usr/local/php/etc/php-fpm.conf cp sapi/fpm/php-fpm /usr/local/bin 然后设置php.ini,使用: vi /usr/local/php/lib/php.ini 打开php配置文件找到cgi.fix_pathinfo配置项,这一项默认被注释并且值为1,根据官方文档的说明,这里为了当文件不存在时,阻止Nginx将请求发送到后端的PHP-FPM模块,从而避免恶意脚本注入的攻击,所以此项应该去掉注释并设置为0 创建web用户: groupadd www-data useradd -g www-data www-data 修改php-fpm.conf添加以上创建的用户和组,这时候使用 vi /usr/local/etc/php-fpm.conf 打开文件后找到官方所提示的位置: user = www-data group = www-data 执行以下命令启动php-fpm服务: php-fpm 启动完毕之后,php-fpm服务默认使用9000端口,使用 netstat -tln | grep 9000 可以查看端口使用情况。你也可以使用 ps -ef|grep php 命令查看进程。 停止 php-fpm killall php-fpm 下载源代码 如果你想下载一个稳定版本的Polr,你可以查看发布页面。 $ cd /var/www $ git clone https://github.com/cydrobolt/polr.git --depth=1 #你也可以下载码云中国汉化版 $ git clone https://gitee.com/skywalker512/polr.git $ chmod -R 755 polr $ chown -R www-data polr $ cd polr $ cp .env.setup .env Composer 安装 切换到 polr目录下 # download composer package curl -sS https://getcomposer.org/installer | php # update/install dependencies php composer.phar install --no-dev -o 如果由于PHP版本的原因,编写器无法安装适当的依赖项,请删除composer.lock 并重新尝试安装依赖项。 rm composer.lock php composer.phar install --no-dev -o Nginx 安装 推荐您使用yum安装以下的开发库: yum install readline-devel pcre-devel openssl-devel -y Docker容器还可能要安装: yum install wget perl gcc -y 下载最新版本: wget https://openresty.org/download/openresty-1.11.2.4.tar.gz 解压并重命名: tar -xvf openresty-1.11.2.4.tar.gz mv openresty-1.11.2.4 openresty 安装配置: ./configure 您可以使用下面的命令来编译安装: make && make install 如果您的电脑支持多核 make 工作的特性, 您可以这样编译安装: make && make install -j2 为了方便启动,建立软连接: ln -s /usr/local/openresty/nginx/sbin/nginx /usr/sbin/nginx 配置文件 vi /usr/local/openresty/nginx/conf/nginx.conf server { listen 80; listen 443 ssl; server_name polr.52itstyle.com; index index.php; #证书路径 ssl_certificate /usr/local/openresty/nginx/cert/214545352540632.pem; #私钥路径 ssl_certificate_key /usr/local/openresty/nginx/cert/214545352540632.key; #缓存有效期 ssl_session_timeout 5m; #可选的加密算法,顺序很重要,越靠前的优先级越高. ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; #安全链接可选的加密协议 ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_prefer_server_ciphers on; root /var/www/polr/public; location / { try_files $uri $uri/ /index.php$is_args$args; } location ~ \.php$ { try_files $uri =404; include fastcgi_params; fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param HTTP_HOST $server_name; } error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } } 安装运行 进入https://polr.52itstyle.com/setup 进行安装,设置一些相关选项即可 安装成功,首页: 后台管理: 短链接演示网址:https://polr.52itstyle.com/ Polr API文档 API密钥 要将用户认证为Polr,您需要提供一个API密钥以及对Polr API的每个请求,作为GET或POST参数。(例如?key=API_KEY_HERE) 分配API密钥 要分配API密钥,请从管理员帐户登录,转到“管理员”选项卡,然后滚动到所需的用户。从那里,您可以打开API按钮下拉菜单来重置,创建或删除用户的API密钥。您还将被提示设置所需的API配额。这被定义为每分钟的请求。您可以通过使配额成为负数来允许无限制的请求。一旦用户收到API密钥,他们将能够在其用户面板中看到一个“API”选项卡,该选项卡提供了与API进行交互所需的信息。 操作 操作作为网址中的细分受众群传递。目前有两项行动得到执行: shorten - 缩短网址 lookup - 查找缩短的URL的目的地 演示 码云API代码:https://gitee.com/52itstyle/short_url 作者: 小柒 出处: https://blog.52itstyle.com 分享是快乐的,也见证了个人成长历程,文章大多都是工作经验总结以及平时学习积累,基于自身认知不足之处在所难免,也请大家指正,共同进步。
前言 最近在做一个口语测评的小程序服务端,小程序涉及到了音频文件的上传,按理说应该统一封装一个第三方上传接口服务提供给前段调用,但是开发没有那么多道理,暂且为了省事就封装到后端服务中去了。 这篇文章需要用到前面所讲的知识点《SpringBoot开发案例之配置静态资源文件路径》,请仔细阅读如何自定义静态资源路径,最好做到服务跟文件分离。 文件上传 前端小程序代码 wx.uploadFile({ url: 'https://example.weixin.qq.com/upload', //示例,非真实的接口地址 filePath: '/static/itstyle.mp3',//默认小程序内音频路径,也可以自己上传 name: 'file', header: { "Content-Type": "multipart/form-data" }, formData: { userId: 12 //附加信息 }, success: function (res) { console.log(res); }, fail: function (res) { console.log(res); }, complete: function (res) { } }) }, 后端上传代码 /** * 口语测试 * 创建者 柒 * 创建时间 2018年3月13日 */ @Api(tags ="口语测试接口") @RestController @RequestMapping("/test") public class TestController { private final static Logger LOGGER = LoggerFactory.getLogger(WechatController.class); @Value("${web.upload.path}") private String uploadPath; @ApiOperation(value="上传文件(小程序)") @PostMapping("/fileUpload") public String upload(HttpServletRequest request, @RequestParam("file")MultipartFile[] files){ LOGGER.info("上传测试"); //多文件上传 if(files!=null && files.length>=1) { BufferedOutputStream bw = null; try { String fileName = files[0].getOriginalFilename(); //判断是否有文件(实际生产中要判断是否是音频文件) if(StringUtils.isNoneBlank(fileName)) { //创建输出文件对象 File outFile = new File(uploadPath + UUID.randomUUID().toString()+ FileUtil.getFileType(fileName)); //拷贝文件到输出文件对象 FileUtils.copyInputStreamToFile(files[0].getInputStream(), outFile); } } catch (Exception e) { e.printStackTrace(); } finally { try { if(bw!=null) {bw.close();} } catch (IOException e) { e.printStackTrace(); } } } return "success"; } } 测试服务 小程序服务端请求必须HTTPS,如何配置,可以参考《阿里云证书服务》配置。 启动服务,执行小程序上传方法,监控前台返回参数,如果没有错误(显然没错误),查看服务器目录/home/file 下是否有相应的文件。 作者: 小柒 出处: https://blog.52itstyle.com 分享是快乐的,也见证了个人成长历程,文章大多都是工作经验总结以及平时学习积累,基于自身认知不足之处在所难免,也请大家指正,共同进步。
前言 SpringBoot本质上是为微服务而生的,以JAR的形式启动运行,但是有时候静态资源的访问是必不可少的,比如:image、js、css 等资源的访问。 默认静态资源路径 Spring Boot 对静态资源映射提供了默认配置,静态资源路径都是在classpath中: classpath:/static classpath:/public classpath:/resources classpath:/META-INF/resources 我们在src/main/resources目录下新建 public、resources、static 三个目录,并分别放入 1.jpg 2.jpg 3.jpg 三张图片。然后通过浏览器分别访问: http://localhost:8080/1.jpg http://localhost:8080/2.jpg http://localhost:8080/3.jpg 地址均可以正常访问,Spring Boot 默认会从 public resources static 三个目录里面查找是否存在相应的资源。 新增静态资源路径 我们在spring.resources.static-locations后面追加一个配置classpath:/itstyle/: # 静态文件请求匹配方式 spring.mvc.static-path-pattern=/** # 修改默认的静态寻址资源目录 多个使用逗号分隔 spring.resources.static-locations = classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/public/,classpath:/itstyle/ 自定义静态资源映射 在实际开发中,我们可能需要自定义静态资源访问以及上传路径,特别是文件上传,不可能上传的运行的JAR服务中,那么可以通过继承WebMvcConfigurerAdapter来实现自定义路径映射。 application.properties 文件配置: # 图片音频上传路径配置(win系统自行变更本地路径) web.upload.path=/home/file/ WechatApplication.java 启动配置: /** * 语音测评后台服务 * 创建者 柒 * 创建时间 2018年3月8日 */ @SpringBootApplication public class WechatApplication extends WebMvcConfigurerAdapter { private final static Logger LOGGER = LoggerFactory.getLogger(WechatApplication.class); @Value("${web.upload.path}") private String uploadPath; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { super.addResourceHandlers(registry); registry.addResourceHandler("/uploads/**").addResourceLocations( "file:"+uploadPath); LOGGER.info("自定义静态资源目录、此处功能用于文件映射"); } public static void main(String[] args) { SpringApplication.run(WechatApplication.class); LOGGER.info("语音测评后台服务启动成功"); } } 我们可以访问以下路径: http://localhost:8080/uploads/1.jpg 作者: 小柒 出处: https://blog.52itstyle.com 分享是快乐的,也见证了个人成长历程,文章大多都是工作经验总结以及平时学习积累,基于自身认知不足之处在所难免,也请大家指正,共同进步。
前言 好久没有更新Spring Boot系列文章,你说忙么?也可能是,前段时间的关注点也许在其他方面了,最近项目中需要开发小程序,正好采用Spring Boot实现一个后端服务,后面会把相关的代码案例分享出来,不至于大家做小程序后端服务的时候一头雾水。 在Spring Boot下默认提供了若干种可用的连接池(dbcp,dbcp2, tomcat, hikari),当然并不支持Druid,Druid来自于阿里系的一个开源连接池,它提供了非常优秀的监控功能,下面跟大家分享一下如何与Spring Boot集成。 版本环境 Spring Boot 1.5.2.RELEASE、Druid 1.1.6、JDK1.7 系统集成 添加pom.xml依赖: <!-- Jpa --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!-- MySql --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- druid --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.6</version> </dependency> 配置application.properties: #数据源 spring.datasource.url=jdbc:mysql://192.168.1.66:3306/spring_boot?characterEncoding=utf-8&useSSL=false spring.datasource.username=root spring.datasource.password=root spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.datasource.type=com.alibaba.druid.pool.DruidDataSource # 初始化大小,最小,最大 spring.datasource.initialSize=1 spring.datasource.minIdle=3 spring.datasource.maxActive=20 # 配置获取连接等待超时的时间 spring.datasource.maxWait=60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 spring.datasource.timeBetweenEvictionRunsMillis=60000 # 配置一个连接在池中最小生存的时间,单位是毫秒 spring.datasource.minEvictableIdleTimeMillis=30000 spring.datasource.validationQuery=select 'x' spring.datasource.testWhileIdle=true spring.datasource.testOnBorrow=false spring.datasource.testOnReturn=false # 打开PSCache,并且指定每个连接上PSCache的大小 spring.datasource.poolPreparedStatements=true spring.datasource.maxPoolPreparedStatementPerConnectionSize=20 # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙 spring.datasource.filters=stat,wall,slf4j # 通过connectProperties属性来打开mergeSql功能;慢SQL记录 spring.datasource.connectionProperties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 配置yml文件(与上二选一) spring: datasource: url: jdbc:mysql://192.168.1.66:3306/spring-boot?useUnicode=true&characterEncoding=utf-8&useSSL=false username: root password: root driver-class-name: com.mysql.jdbc.Driver platform: mysql type: com.alibaba.druid.pool.DruidDataSource # 下面为连接池的补充设置,应用到上面所有数据源中 # 初始化大小,最小,最大 initialSize: 1 minIdle: 3 maxActive: 20 # 配置获取连接等待超时的时间 maxWait: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 timeBetweenEvictionRunsMillis: 60000 # 配置一个连接在池中最小生存的时间,单位是毫秒 minEvictableIdleTimeMillis: 30000 validationQuery: select 'x' testWhileIdle: true testOnBorrow: false testOnReturn: false # 打开PSCache,并且指定每个连接上PSCache的大小 poolPreparedStatements: true maxPoolPreparedStatementPerConnectionSize: 20 # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙 filters: stat,wall,slf4j # 通过connectProperties属性来打开mergeSql功能;慢SQL记录 connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 配置Druid的监控统计功能 import java.sql.SQLException; import javax.sql.DataSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import com.alibaba.druid.pool.DruidDataSource; import com.alibaba.druid.support.http.StatViewServlet; import com.alibaba.druid.support.http.WebStatFilter; /** * 阿里数据库连接池 Druid配置 * 创建者 柒 * 创建时间 2018年3月15日 */ @Configuration public class DruidConfiguration { private static final Logger logger = LoggerFactory.getLogger(DruidConfiguration.class); private static final String DB_PREFIX = "spring.datasource"; @Bean public ServletRegistrationBean druidServlet() { logger.info("init Druid Servlet Configuration "); ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*"); // IP白名单 (没有配置或者为空,则允许所有访问) servletRegistrationBean.addInitParameter("allow", ""); // IP黑名单(共同存在时,deny优先于allow) //servletRegistrationBean.addInitParameter("deny", "192.168.1.100"); //控制台管理用户 servletRegistrationBean.addInitParameter("loginUsername", "admin"); servletRegistrationBean.addInitParameter("loginPassword", "admin"); //是否能够重置数据 禁用HTML页面上的“Reset All”功能 servletRegistrationBean.addInitParameter("resetEnable", "false"); return servletRegistrationBean; } @Bean public FilterRegistrationBean filterRegistrationBean() { FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new WebStatFilter()); filterRegistrationBean.addUrlPatterns("/*"); filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"); return filterRegistrationBean; } @ConfigurationProperties(prefix = DB_PREFIX) class IDataSourceProperties { private String url; private String username; private String password; private String driverClassName; private int initialSize; private int minIdle; private int maxActive; private int maxWait; private int timeBetweenEvictionRunsMillis; private int minEvictableIdleTimeMillis; private String validationQuery; private boolean testWhileIdle; private boolean testOnBorrow; private boolean testOnReturn; private boolean poolPreparedStatements; private int maxPoolPreparedStatementPerConnectionSize; private String filters; private String connectionProperties; @Bean public DataSource dataSource() { DruidDataSource datasource = new DruidDataSource(); datasource.setUrl(url); datasource.setUsername(username); datasource.setPassword(password); datasource.setDriverClassName(driverClassName); //configuration datasource.setInitialSize(initialSize); datasource.setMinIdle(minIdle); datasource.setMaxActive(maxActive); datasource.setMaxWait(maxWait); datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis); datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis); datasource.setValidationQuery(validationQuery); datasource.setTestWhileIdle(testWhileIdle); datasource.setTestOnBorrow(testOnBorrow); datasource.setTestOnReturn(testOnReturn); datasource.setPoolPreparedStatements(poolPreparedStatements); datasource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize); try { datasource.setFilters(filters); } catch (SQLException e) { System.err.println("druid configuration initialization filter: " + e); } datasource.setConnectionProperties(connectionProperties); return datasource; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getDriverClassName() { return driverClassName; } public void setDriverClassName(String driverClassName) { this.driverClassName = driverClassName; } public int getInitialSize() { return initialSize; } public void setInitialSize(int initialSize) { this.initialSize = initialSize; } public int getMinIdle() { return minIdle; } public void setMinIdle(int minIdle) { this.minIdle = minIdle; } public int getMaxActive() { return maxActive; } public void setMaxActive(int maxActive) { this.maxActive = maxActive; } public int getMaxWait() { return maxWait; } public void setMaxWait(int maxWait) { this.maxWait = maxWait; } public int getTimeBetweenEvictionRunsMillis() { return timeBetweenEvictionRunsMillis; } public void setTimeBetweenEvictionRunsMillis(int timeBetweenEvictionRunsMillis) { this.timeBetweenEvictionRunsMillis = timeBetweenEvictionRunsMillis; } public int getMinEvictableIdleTimeMillis() { return minEvictableIdleTimeMillis; } public void setMinEvictableIdleTimeMillis(int minEvictableIdleTimeMillis) { this.minEvictableIdleTimeMillis = minEvictableIdleTimeMillis; } public String getValidationQuery() { return validationQuery; } public void setValidationQuery(String validationQuery) { this.validationQuery = validationQuery; } public boolean isTestWhileIdle() { return testWhileIdle; } public void setTestWhileIdle(boolean testWhileIdle) { this.testWhileIdle = testWhileIdle; } public boolean isTestOnBorrow() { return testOnBorrow; } public void setTestOnBorrow(boolean testOnBorrow) { this.testOnBorrow = testOnBorrow; } public boolean isTestOnReturn() { return testOnReturn; } public void setTestOnReturn(boolean testOnReturn) { this.testOnReturn = testOnReturn; } public boolean isPoolPreparedStatements() { return poolPreparedStatements; } public void setPoolPreparedStatements(boolean poolPreparedStatements) { this.poolPreparedStatements = poolPreparedStatements; } public int getMaxPoolPreparedStatementPerConnectionSize() { return maxPoolPreparedStatementPerConnectionSize; } public void setMaxPoolPreparedStatementPerConnectionSize(int maxPoolPreparedStatementPerConnectionSize) { this.maxPoolPreparedStatementPerConnectionSize = maxPoolPreparedStatementPerConnectionSize; } public String getFilters() { return filters; } public void setFilters(String filters) { this.filters = filters; } public String getConnectionProperties() { return connectionProperties; } public void setConnectionProperties(String connectionProperties) { this.connectionProperties = connectionProperties; } } } 启动应用,访问地址:http://localhost:8080/druid/, 输入配置的账号密码登录之后,即可查看数据源及SQL统计等监控。效果图如下: 当然,阿里巴巴也提供了Druid的SpringBoot集成版(druid-spring-boot-starter),可参考以下链接。 参考: https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter https://github.com/alibaba/druid/wiki 作者: 小柒 出处: https://blog.52itstyle.com 分享是快乐的,也见证了个人成长历程,文章大多都是工作经验总结以及平时学习积累,基于自身认知不足之处在所难免,也请大家指正,共同进步。
上一篇文章中,简单了学习了一下在Docker环境中搭建JavaWeb环境,其实这时候已经是一个全新的镜像了,就是我们的Ghost系统一样,装配了各式各样的软件一样,以后使用的时候直接安装镜像即可。这时候我们需要做的就是把配置完成JavaWeb环境Docker打包一下,封装成一个更新的镜像环境。 打包镜像 首先我们进入之前启动的容器: [root@iZ2ze74fkxrls31tr2ia2fZ ~]# docker attach centos [root@b5a21b26c111 ROOT] b5a21b26c111是产生的容器ID,然后我们执行以下命令: docker commit b5a21b26c111 centos-java 配置完成以后执行命令docker images,就可以看到REPOSITORY名为centos-java的镜像了。 上传镜像 阿里云官方网站链接(进入后自行创建用户):https://dev.aliyun.com/search.html 然后依次创建命名空间-镜像仓库。 登录阿里云docker registry: docker login --username=admin@52itstyle.com registry.cn-hangzhou.aliyuncs.com 将镜像推送到registry: docker tag <IMAGE ID> registry.cn-hangzhou.aliyuncs.com/itstyle/images:centos-java docker push registry.cn-hangzhou.aliyuncs.com/itstyle/images:centos-java Push成功以后如下显示: 运行容器 查看镜像: docker images 创建并启动容器: docker run -i -t -v /root/software/:/mnt/software/ <IMAGE ID> /bin/bash 如果想启动的时候设置内存: docker run -i -t -m 100m --memory-swap=100m -v /root/software/:/mnt/software/ <IMAGE ID> /bin/bash 创建时指定容器名字: docker run -i -t --name test -m 100m --memory-swap=100m -v /root/software/:/mnt/software/ <IMAGE ID> /bin/bash Docker 1.10提供了对容器资源限制的动态修改能力。例如,我们可以通过下面命令把容器内存限制调整到1GB docker update -m 1024m test docker restart test 查看运行容器: docker ps 重命名容器: docker rename <原容器NAMES> <新容器NAMES> 配置完成以后,我们启动容器中的Tomcat。 负载均衡 openresty配置: server { listen 80; server_name docker.52itstyle.com; charset utf-8; location / { default_type text/html; proxy_pass http://docker; } } upstream docker { server 172.18.0.2:8080 weight=1 max_fails=2 fail_timeout=30s; server 172.18.0.3:8080 weight=1 max_fails=2 fail_timeout=30s; } 最终访问地址(见标题变化):http://docker.52itstyle.com/ 资源配置小知识 内存限制 Docker 提供的内存限制功能有以下几点: 容器能使用的内存和交换分区大小。 容器的核心内存大小。 容器虚拟内存的交换行为。 容器内存的软性限制。 是否杀死占用过多内存的容器。 容器被杀死的优先级 内存限制相关的参数:执行docker run命令时能使用的和内存限制相关的所有选项如下。 -m,--memory 内存限制,格式是数字加单位,单位可以为 b,k,m,g。最小为 4M --memory-swap 内存+交换分区大小总限制。格式同上。必须必-m设置的大 --memory-reservation 内存的软性限制。格式同上 --oom-kill-disable 是否阻止 OOM killer 杀死容器,默认没设置 --oom-score-adj 容器被 OOM killer 杀死的优先级,范围是[-1000, 1000],默认为 0 --memory-swappiness 用于设置容器的虚拟内存控制行为。值为 0~100 之间的整数 --kernel-memory 核心内存限制。格式同上,最小为 4M 作者: 小柒 出处: https://blog.52itstyle.com 分享是快乐的,也见证了个人成长历程,文章大多都是工作经验总结以及平时学习积累,基于自身认知不足之处在所难免,也请大家指正,共同进步。
自上次从北京参加阿里云社区开发者进阶大会回来,就萌发了学习Docker的种子,尽管公司现在的业务并没有什么需求,但学习先进的东西总没有坏处。 2017年,Docker 四周岁啦!尽管之前有所耳闻,但是并没有机会和时间去接触,正好手里躺着两台服务器,趁着年底系统的学习一下,自此接触并认识小鲸鱼。 回顾 上一篇文章大体对Docker有了一定的认识和了解,Docker学习,并从阿里云官方镜像拉取了一个centos镜像。今天,来学下Docker容器如何配置一个JavaWeb环境。 配置 首先查看是否有容器或者在运行,然后启动并进入容器: # 查看所有容器 docker ps -a # 查看运行中的容器 docker ps # 启动容器 docker start 容器名或ID # 进入容器 docker attach 容器名或ID 安装JDK,这里我们直接使用YUM安装(简单方便与官方的基本没什么差别): yum install java -y 安装完成,如果没有错误,执行以下命令检查是否安装成功: java -version 安装Tomcat容器,这里我们下载官方的Tomcat8: # 下载 wget http://mirrors.hust.edu.cn/apache/tomcat/tomcat-8/v8.5.24/bin/apache-tomcat-8.5.24.tar.gz # 解压 tar -zvf apache-tomcat-8.5.24.tar.gz # 重命名 mv apache-tomcat-8.5.24 tomcat8 # 切换的执行目录 cd tomcat8/bin # 启动容器 ./startup.sh 启动后,切换到logs目录查看日志是否启动成功: tail -100f catalina.out 访问 以上配置完成以后,那么我们如何访问容器中的服务呢?由于母鸡中安装配置了OpenResty,我们可以使用OpenResty做代理服务访问我们容器内部的服务。 首先我们命令查看容器的内网IP: # 查询单个容器的IP docker inspect <container id> # 或者查询所有容器的IP docker inspect -f '{{.Name}} - {{.NetworkSettings.IPAddress }}' $(docker ps -aq) 然后通过Nginx代理配置: server { listen 80; server_name docker.52itstyle.com; charset utf-8; location / { default_type text/html; proxy_pass http://172.18.0.2:8080; } } 最终访问地址:http://docker.52itstyle.com/ 快捷 当然,如果你不想一步步配置JavaWeb运行环境,你可以执行执行以下命令获取现成的打包镜像: #阿里镜像 docker login --username=admin@52itstyle.com registry.cn-hangzhou.aliyuncs.com # 获取 tomcat8版本 docker pull tomcat:8 作者: 小柒 出处: https://blog.52itstyle.com 分享是快乐的,也见证了个人成长历程,文章大多都是工作经验总结以及平时学习积累,基于自身认知不足之处在所难免,也请大家指正,共同进步。
Docker 是一个开源工具,它可以让创建和管理 Linux 容器变得简单。容器就像是轻量级的虚拟机,并且可以以毫秒级的速度来启动或停止。Docker 帮助系统管理员和程序员在容器中开发应用程序,并且可以扩展到成千上万的节点。 这是一只鲸鱼,它托着许多集装箱。我们可以把宿主机可当做这只鲸鱼,把相互隔离的容器可看成集装箱,每个集装箱中都包含自己的应用程序。 Docker与传统虚拟区别 传统虚拟化技术的体系架构: Docker技术的体系架构: 容器和 VM(虚拟机)的主要区别是: 容器提供了基于进程的隔离,而虚拟机提供了资源的完全隔离。 虚拟机可能需要一分钟来启动,而容器只需要一秒钟或更短。 容器使用宿主操作系统的内核,而虚拟机使用独立的内核。 Doker 平台的基本构成 Docker 平台基本上由三部分组成: 客户端:用户使用 Docker 提供的工具(CLI 以及 API 等)来构建,上传镜像并发布命令来创建和启动容器 Docker 主机:从 Docker registry 上下载镜像并启动容器 Docker registry:Docker 镜像仓库,用于保存镜像,并提供镜像上传和下载 后面的文章会具体分析。 Docker 容器的状态机 一个容器在某个时刻可能处于以下几种状态之一: created:已经被创建 (使用 docker ps -a 命令可以列出)但是还没有被启动 (使用 docker ps 命令还无法列出) running:运行中 paused:容器的进程被暂停了 restarting:容器的进程正在重启过程中 exited:上图中的 stopped 状态,表示容器之前运行过但是现在处于停止状态(要区别于 created 状态,它是指一个新创出的尚未运行过的容器)。可以通过 start 命令使其重新进入 running 状态 destroyed:容器被删除了,再也不存在了 Docker 的安装 RedHat/CentOS必须要6.6版本以上,或者7.x才能安装docker,建议在RedHat/CentOS 7上使用docker,因为RedHat/CentOS 7的内核升级到了kernel 3.10,对lxc容器支持更好。 查看Linux内核版本(内核版本必须是3.10或者以上): cat /proc/version uname -a lsb_release -a ##无法执行命令安装 yum install -y redhat-lsb 更新YUM源: yum update 安装: yum install docker -y 检查版本: docker -v 安装完成后,使用下面的命令来启动 docker 服务,并将其设置为开机启动: service docker start chkconfig docker on 下载官方的 CentOS 镜像: docker pull centos 检查CentOS 镜像是否被获取: docker images # 删除镜像 docker rmi <image id> # 删除镜像(针对多个相同image id的镜像) docker rmi repository:tag 下载完成后,你应该会看到: [root@iZ2ze74fkxrls31tr2ia2fZ ~]# docker images centos REPOSITORY TAG IMAGE ID CREATED SIZE docker.io/centos latest 3fa822599e10 3weeks ago 203.5 MB 如果看到以上输出,说明你可以使用“docker.io/centos”这个镜像了,或将其称为仓库(Repository),该镜像有一个名为“latest”的标签(Tag),此外还有一个名为“3fa822599e10 ”的镜像 ID(可能您所看到的镜像 ID 与此处的不一致,那是正常现象,因为这个数字是随机生成的)。此外,我们可以看到该镜像只有 203.5 MB,非常小巧,而不像虚拟机的镜像文件那样庞大。 重命名TAG: # ocker tag IMAGEID(镜像id) REPOSITORY:TAG(仓库:标签) docker tag 3fa822599e10 docker.io/centos:centos 启动容器: docker run -i -t -v /root/software/:/mnt/software/ 3fa822599e10 /bin/bash 命令参数说明:docker run <相关参数> <镜像 ID> <初始命令> -i:表示以“交互模式”运行容器 -t:表示容器启动后会进入其命令行 -v:表示需要将本地哪个目录挂载到容器中,格式:-v <宿主机目录>:<容器目录> 更多参数详解: Usage: docker run [OPTIONS] IMAGE [COMMAND] [ARG...] -d, --detach=false 指定容器运行于前台还是后台,默认为false -i, --interactive=false 打开STDIN,用于控制台交互 -t, --tty=false 分配tty设备,该可以支持终端登录,默认为false -u, --user="" 指定容器的用户 -a, --attach=[] 登录容器(必须是以docker run -d启动的容器) -w, --workdir="" 指定容器的工作目录 -c, --cpu-shares=0 设置容器CPU权重,在CPU共享场景使用 -e, --env=[] 指定环境变量,容器中可以使用该环境变量 -m, --memory="" 指定容器的内存上限 -P, --publish-all=false 指定容器暴露的端口 -p, --publish=[] 指定容器暴露的端口 -h, --hostname="" 指定容器的主机名 -v, --volume=[] 给容器挂载存储卷,挂载到容器的某个目录 --volumes-from=[] 给容器挂载其他容器上的卷,挂载到容器的某个目录 --cap-add=[] 添加权限,权限清单详见:http://linux.die.net/man/7/capabilities --cap-drop=[] 删除权限,权限清单详见:http://linux.die.net/man/7/capabilities --cidfile="" 运行容器后,在指定文件中写入容器PID值,一种典型的监控系统用法 --cpuset="" 设置容器可以使用哪些CPU,此参数可以用来容器独占CPU --device=[] 添加主机设备给容器,相当于设备直通 --dns=[] 指定容器的dns服务器 --dns-search=[] 指定容器的dns搜索域名,写入到容器的/etc/resolv.conf文件 --entrypoint="" 覆盖image的入口点 --env-file=[] 指定环境变量文件,文件格式为每行一个环境变量 --expose=[] 指定容器暴露的端口,即修改镜像的暴露端口 --link=[] 指定容器间的关联,使用其他容器的IP、env等信息 --lxc-conf=[] 指定容器的配置文件,只有在指定--exec-driver=lxc时使用 --name="" 指定容器名字,后续可以通过名字进行容器管理,links特性需要使用名字 --net="bridge" 容器网络设置: bridge 使用docker daemon指定的网桥 host //容器使用主机的网络 container:NAME_or_ID >//使用其他容器的网路,共享IP和PORT等网络资源 none 容器使用自己的网络(类似--net=bridge),但是不进行配置 --privileged=false 指定容器是否为特权容器,特权容器拥有所有的capabilities --restart="no" 指定容器停止后的重启策略: no:容器退出时不重启 on-failure:容器故障退出(返回值非零)时重启 always:容器退出时总是重启 --rm=false 指定容器停止后自动删除容器(不支持以docker run -d启动的容器) --sig-proxy=true 设置由代理接受并处理信号,但是SIGCHLD、SIGSTOP和SIGKILL不能被代理 Docker命令 我们可以把Docker 的命令大概地分类如下: 镜像操作: build Build an image from a Dockerfile commit Create a new image from a container's changes images List images load Load an image from a tar archive or STDIN pull Pull an image or a repository from a registry push Push an image or a repository to a registry rmi Remove one or more images search Search the Docker Hub for images tag Tag an image into a repository save Save one or more images to a tar archive history 显示某镜像的历史 inspect 获取镜像的详细信息 容器及其中应用的生命周期操作: create 创建一个容器 kill Kill one or more running containers inspect Return low-level information on a container, image or task pause Pause all processes within one or more containers ps List containers rm 删除一个或者多个容器 rename Rename a container restart Restart a container run 创建并启动一个容器 start 启动一个处于停止状态的容器 stats 显示容器实时的资源消耗信息 stop 停止一个处于运行状态的容器 top Display the running processes of a container unpause Unpause all processes within one or more containers update Update configuration of one or more containers wait Block until a container stops, then print its exit code attach Attach to a running container exec Run a command in a running container port List port mappings or a specific mapping for the container logs 获取容器的日志 容器文件系统操作: cp Copy files/folders between a container and the local filesystem diff Inspect changes on a container's filesystem export Export a container's filesystem as a tar archive import Import the contents from a tarball to create a filesystem image Docker registry 操作: login Log in to a Docker registry. logout Log out from a Docker registry. Volume 操作 volume Manage Docker volumes 网络操作 network Manage Docker networks Swarm 相关操作 swarm Manage Docker Swarm service Manage Docker services node Manage Docker Swarm nodes 系统操作: version Show the Docker version information events 持续返回docker 事件 info 显示Docker 主机系统范围内的信息 # 查看运行中的容器 docker ps # 查看所有容器 docker ps -a # 退出容器 按Ctrl+D 即可退出当前容器【但退出后会停止容器】 # 退出不停止容器: 组合键:Ctrl+P+Q # 启动容器 docker start 容器名或ID # 进入容器 docker attach 容器名或ID # 停止容器 docker stop 容器名或ID # 暂停容器 docker pause 容器名或ID #继续容器 docker unpause 容器名或ID # 删除容器 docker rm 容器名或ID # 删除全部容器--慎用 docker stop $(docker ps -q) & docker rm $(docker ps -aq) #保存容器,生成镜像 docker commit 容器ID 镜像名称 #从 host 拷贝文件到 container 里面 docker cp /home/soft centos:/webapp docker run与start的区别 docker run 只在第一次运行时使用,将镜像放到容器中,以后再次启动这个容器时,只需要使用命令docker start 即可。 docker run相当于执行了两步操作:将镜像放入容器中(docker create),然后将容器启动,使之变成运行时容器(docker start)。 而docker start的作用是,重新启动已存在的镜像。也就是说,如果使用这个命令,我们必须事先知道这个容器的ID,或者这个容器的名字,我们可以使用docker ps找到这个容器的信息。 因为容器的ID是随机码,而容器的名字又是看似无意义的命名,我们可以使用命令: docker rename jovial_cori centos 给这个容器命名。这样以后,我们再次启动或停止容器时,就可以直接使用这个名字: docker [stop] [start] new_name 而要显示出所有容器,包括没有启动的,可以使用命令: docker ps -a Docker配置 更改存储目录: #复制docker存储目录 rsync -aXS /var/lib/docker/. /home/docker #更改 docker 存储文件目录 ln -s /home/docker /var/lib/docker 获取IP: docker inspect <container id> 要获取所有容器名称及其IP地址只需一个命令: docker inspect -f '{{.Name}} - {{.NetworkSettings.IPAddress }}' $(docker ps -aq) docker inspect --format='{{.Name}} - {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $(docker ps -aq) Docker 镜像加速器 注册个帐号 https://dev.aliyun.com/search.html 阿里云会自动为用户分配一个镜像加速器的地址,登录后进入"管理中心"-->"加速器",里面有分配给你的镜像加速器的地址以及各个环境的使用说明。 镜像加速器地址:https://xxxxx.mirror.aliyuncs.com 如何配置镜像加速器 针对Docker客户端版本大于1.10.0的用户您可以通过修改daemon配置文件/etc/docker/daemon.json来使用加速器: { "registry-mirrors": ["<your accelerate address>"] } 重启Docker Daemon: sudo systemctl daemon-reload sudo systemctl restart docker 作者: 小柒 出处: https://blog.52itstyle.com 分享是快乐的,也见证了个人成长历程,文章大多都是工作经验总结以及平时学习积累,基于自身认知不足之处在所难免,也请大家指正,共同进步。
在线语音合成 将文字信息转化为声音信息,给应用配上“嘴巴”。我们提供了众多极具特色的发音人(音库)供您选择。其合成音在音色、自然度等方面的表现均接近甚至超过了人声。这种语音合成体验,达到了真正可商用的标准 讯飞的语音合成还是很牛P的,不但有基础发音人,还有精品发音人、特色发音人、明星发音人,当然你如果有特殊要求还可以定制。 这里我们选择基础发音人做简单的JavaWeb集成测试,因为其他选项还要申请,想想还是算了,等流程走通再说。 平台环境 JDK1.7、Tomcat8、Eclipse、讯飞JDK、win+ffmpeg(测试)、Linux+Docker+ffmpeg(生产) 说明:讲真,Win平台下ffmpeg安装使用还是很轻松的,直接下载压缩包免安装,JAVA直接调用执行命令即可。Linux下各种依赖编译能把你的小机器跑死,并且还各种编译错误,然后就果断使用了Docker,唯一头疼的是,这个环境真干净,各种命令不支持,当然这也是Docker的优点。 流程图 Web集成 讯飞为我们提供了简单的SDK,科大讯飞MSC开发指南-Java。当然,前提你要有一个讯飞的账号,注册、创建应用什么的这里就不赘述了,只要最后能获取到一个APP_ID就可以。 Win+ffmpeg(测试) 讯飞语音合成需要动态链接库支持,根据自己的系统把msc64.dll或者msc32.dll放到指定的目录,可以使用System.getProperty("java.library.path")查看,放置到任意目录即可。 根据自己的系统下载对应的ffmpeg,解压即可,直接调用bin目录下的ffmpeg.exe即可。- Linux+Docker+ffmpeg(生产) 获取ffmpeg镜像 docker pull jrottenberg/ffmpeg 创建并运行容器 docker run -it --name app_ffmpeg -p 8080:8080 -v /home/app_ffmpeg/:/mnt/app/ --entrypoint='bash' jrottenberg/ffmpeg 注意:Docker容器中,各种yum、wget以及vim是不存在的,所以大都数配置通过宿机获取然后同步复制到容器中。 安装配置JDK 甲骨文给弄的必须认证下载了,这里我们自行下载并手动上传到/home/app_ffmpeg目录下。 # 复制配置文件到宿机 docker cp 4f131c866092:/etc/profile /home/app_ffmpeg/ 编辑profile,追加以下配置 #set java environment JAVA_HOME=/mnt/app/jdk1.7.0_80 JRE_HOME=/mnt/app/jdk1.7.0_80/jre CLASS_PATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$JRE_HOME/lib PATH=$PATH:$JAVA_HOME/bin:$JRE_HOME/bin export JAVA_HOME JRE_HOME CLASS_PATH PATH # 复制配置文件到容器 docker cp /home/app_ffmpeg/profile 4f131c866092:/etc/ 进入容器,生效配置 # 进入容器 docker exec -it app_ffmpeg bash # 使配置生效 source /etc/profile # 检查JDK是否安装成功 java -version 安装配置Tomcat 如果tomcat启动卡主不动 找到jdk1.x.x_xx/jre/lib/security/java.security文件,在文件中找到securerandom.source这个设置项,将其改为: securerandom.source=file:/dev/./urandom 如果tomcat输出中文乱码 locale locale -a LANG=C.UTF-8 (有的是zh_CN.UTF-8,不过我在本地没发现这种编码) source /etc/profile 配置讯飞动态库 根据自己的系统版本,分别把libmsc32.so 或者 libmsc64.so 上传到/lib/ 和 /lib64/ 目录。 开源项目 https://gitee.com/52itstyle/xufei_msc 演示地址 http://xunfei.52itstyle.com/xufei_msc/
概述 分布式文件系统:Distributed file system, DFS,又叫做网络文件系统:Network File System。一种允许文件通过网络在多台主机上分享的文件系统,可让多机器上的多用户分享文件和存储空间。 FastDFS是用c语言编写的一款开源的分布式文件系统,充分考虑了冗余备份、负载均衡、线性扩容等机制,并注重高可用、高性能等指标,功能包括:文件存储、文件同步、文件访问(文件上传、文件下载)等,解决了大容量存储和负载均衡的问题。特别适合中小文件(建议范围:4KB < file_size <500MB),对以文件为载体的在线服务,如相册网站、视频网站等。 FastDFS 架构 FastDFS架构包括Tracker server和Storage server。客户端请求Tracker server进行文件上传、下载,通过Tracker server调度最终由Storage server完成文件上传和下载。 跟踪服务器Tracker Server 主要做调度工作,起到均衡的作用;负责管理所有的 storage server和 group,每个 storage 在启动后会连接 Tracker,告知自己所属 group 等信息,并保持周期性心跳。tracker根据storage的心跳信息,建立group==>[storage serverlist]的映射表。 Tracker需要管理的元信息很少,会全部存储在内存中;另外tracker上的元信息都是由storage汇报的信息生成的,本身不需要持久化任何数据,这样使得tracker非常容易扩展,直接增加tracker机器即可扩展为tracker cluster来服务,cluster里每个tracker之间是完全对等的,所有的tracker都接受stroage的心跳信息,生成元数据信息来提供读写服务。 存储服务器Storage Server 主要提供容量和备份服务;以 group 为单位,每个 group 内可以有多台 storage server,数据互为备份。以group为单位组织存储能方便的进行应用隔离、负载均衡、副本数定制(group内storage server数量即为该group的副本数),比如将不同应用数据存到不同的group就能隔离应用数据,同时还可根据应用的访问特性来将应用分配到不同的group来做负载均衡;缺点是group的容量受单机存储容量的限制,同时当group内有机器坏掉时,数据恢复只能依赖group内地其他机器,使得恢复时间会很长。 group内每个storage的存储依赖于本地文件系统,storage可配置多个数据存储目录,比如有10块磁盘,分别挂载在/data/disk1-/data/disk10,则可将这10个目录都配置为storage的数据存储目录。storage接受到写文件请求时,会根据配置好的规则选择其中一个存储目录来存储文件。为了避免单个目录下的文件数太多,在storage第一次启动时,会在每个数据存储目录里创建2级子目录,每级256个,总共65536个文件,新写的文件会以hash的方式被路由到其中某个子目录下,然后将文件数据作为本地文件存储到该目录中。 FastDFS的存储策略 为了支持大容量,存储节点(服务器)采用了分卷(或分组)的组织方式。存储系统由一个或多个卷组成,卷与卷之间的文件是相互独立的,所有卷的文件容量累加就是整个存储系统中的文件容量。一个卷可以由一台或多台存储服务器组成,一个卷下的存储服务器中的文件都是相同的,卷中的多台存储服务器起到了冗余备份和负载均衡的作用。 在卷中增加服务器时,同步已有的文件由系统自动完成,同步完成后,系统自动将新增服务器切换到线上提供服务。当存储空间不足或即将耗尽时,可以动态添加卷。只需要增加一台或多台服务器,并将它们配置为一个新的卷,这样就扩大了存储系统的容量。 FastDFS的上传过程 FastDFS向使用者提供基本文件访问接口,比如upload、download、append、delete等,以客户端库的方式提供给用户使用。 Storage Server会定期的向Tracker Server发送自己的存储信息。当Tracker Server Cluster中的Tracker Server不止一个时,各个Tracker之间的关系是对等的,所以客户端上传时可以选择任意一个Tracker。 当Tracker收到客户端上传文件的请求时,会为该文件分配一个可以存储文件的group,当选定了group后就要决定给客户端分配group中的哪一个storage server。当分配好storage server后,客户端向storage发送写文件请求,storage将会为文件分配一个数据存储目录。然后为文件分配一个fileid,最后根据以上的信息生成文件名存储文件。 选择tracker server 当集群中不止一个tracker server时,由于tracker之间是完全对等的关系,客户端在upload文件时可以任意选择一个trakcer。 选择存储的group 当tracker接收到upload file的请求时,会为该文件分配一个可以存储该文件的group,支持如下选择group的规则: 1. Round robin,所有的group间轮询 2. Specified group,指定某一个确定的group 3. Load balance,剩余存储空间多多group优先 选择storage server 当选定group后,tracker会在group内选择一个storage server给客户端,支持如下选择storage的规则: 1. Round robin,在group内的所有storage间轮询 2. First server ordered by ip,按ip排序 3. First server ordered by priority,按优先级排序(优先级在storage上配置) 选择storage path 当分配好storage server后,客户端将向storage发送写文件请求,storage将会为文件分配一个数据存储目录,支持如下规则: 1. Round robin,多个存储目录间轮询 2. 剩余存储空间最多的优先 生成Fileid 选定存储目录之后,storage会为文件生一个Fileid,由storage server ip、文件创建时间、文件大小、文件crc32和一个随机数拼接而成,然后将这个二进制串进行base64编码,转换为可打印的字符串。 选择两级目录 当选定存储目录之后,storage会为文件分配一个fileid,每个存储目录下有两级256*256的子目录,storage会按文件fileid进行两次hash(猜测),路由到其中一个子目录,然后将文件以fileid为文件名存储到该子目录下。 生成文件名 当文件存储到某个子目录后,即认为该文件存储成功,接下来会为该文件生成一个文件名,文件名由group、存储目录、两级子目录、fileid、文件后缀名(由客户端指定,主要用于区分文件类型)拼接而成。 FastDFS的文件同步 写文件时,客户端将文件写至group内一个storage server即认为写文件成功,storage server写完文件后,会由后台线程将文件同步至同group内其他的storage server。 每个storage写文件后,同时会写一份binlog,binlog里不包含文件数据,只包含文件名等元信息,这份binlog用于后台同步,storage会记录向group内其他storage同步的进度,以便重启后能接上次的进度继续同步;进度以时间戳的方式进行记录,所以最好能保证集群内所有server的时钟保持同步。 storage的同步进度会作为元数据的一部分汇报到tracker上,tracke在选择读storage的时候会以同步进度作为参考。 比如一个group内有A、B、C三个storage server,A向C同步到进度为T1 (T1以前写的文件都已经同步到B上了),B向C同步到时间戳为T2(T2 > T1),tracker接收到这些同步进度信息时,就会进行整理,将最小的那个做为C的同步时间戳,本例中T1即为C的同步时间戳为T1(即所有T1以前写的数据都已经同步到C上了);同理,根据上述规则,tracker会为A、B生成一个同步时间戳。 FastDFS的文件下载 客户端uploadfile成功后,会拿到一个storage生成的文件名,接下来客户端根据这个文件名即可访问到该文件。 跟upload file一样,在downloadfile时客户端可以选择任意tracker server。tracker发送download请求给某个tracker,必须带上文件名信息,tracke从文件名中解析出文件的group、大小、创建时间等信息,然后为该请求选择一个storage用来服务读请求。 FastDFS性能方案 FastDFS 安装 软件包 版本 FastDFS v5.05 libfastcommon v1.0.7 下载安装libfastcommon 下载 wget https://github.com/happyfish100/libfastcommon/archive/V1.0.7.tar.gz 解压 tar -xvf V1.0.7.tar.gz cd libfastcommon-1.0.7 编译、安装 ./make.sh ./make.sh install 创建软链接 ln -s /usr/lib64/libfastcommon.so /usr/local/lib/libfastcommon.so ln -s /usr/lib64/libfastcommon.so /usr/lib/libfastcommon.so ln -s /usr/lib64/libfdfsclient.so /usr/local/lib/libfdfsclient.so ln -s /usr/lib64/libfdfsclient.so /usr/lib/libfdfsclient.so 下载安装FastDFS 下载FastDFS wget https://github.com/happyfish100/fastdfs/archive/V5.05.tar.gz 解压 tar -xvf V5.05.tar.gz cd fastdfs-5.05 编译、安装 ./make.sh ./make.sh install 配置 Tracker 服务 上述安装成功后,在/etc/目录下会有一个fdfs的目录,进入它。会看到三个.sample后缀的文件,这是作者给我们的示例文件,我们需要把其中的tracker.conf.sample文件改为tracker.conf配置文件并修改它: cp tracker.conf.sample tracker.conf vi tracker.conf 编辑tracker.conf # 配置文件是否不生效,false 为生效 disabled=false # 提供服务的端口 port=22122 # Tracker 数据和日志目录地址 base_path=//home/data/fastdfs # HTTP 服务端口 http.server_port=80 创建tracker基础数据目录,即base_path对应的目录 mkdir -p /home/data/fastdfs 使用ln -s 建立软链接 ln -s /usr/bin/fdfs_trackerd /usr/local/bin ln -s /usr/bin/stop.sh /usr/local/bin ln -s /usr/bin/restart.sh /usr/local/bin 启动服务 service fdfs_trackerd start 查看监听 netstat -unltp|grep fdfs 如果看到22122端口正常被监听后,这时候说明Tracker服务启动成功啦! tracker server 目录及文件结构Tracker服务启动成功后,会在base_path下创建data、logs两个目录。目录结构如下: ${base_path} |__data | |__storage_groups.dat:存储分组信息 | |__storage_servers.dat:存储服务器列表 |__logs | |__trackerd.log: tracker server 日志文件 配置 Storage 服务 进入 /etc/fdfs 目录,复制 FastDFS 存储器样例配置文件 storage.conf.sample,并重命名为 storage.conf # cd /etc/fdfs # cp storage.conf.sample storage.conf # vi storage.conf 编辑storage.conf # 配置文件是否不生效,false 为生效 disabled=false # 指定此 storage server 所在 组(卷) group_name=group1 # storage server 服务端口 port=23000 # 心跳间隔时间,单位为秒 (这里是指主动向 tracker server 发送心跳) heart_beat_interval=30 # Storage 数据和日志目录地址(根目录必须存在,子目录会自动生成) base_path=/home/data/fastdfs/storage # 存放文件时 storage server 支持多个路径。这里配置存放文件的基路径数目,通常只配一个目录。 store_path_count=1 # 逐一配置 store_path_count 个路径,索引号基于 0。 # 如果不配置 store_path0,那它就和 base_path 对应的路径一样。 store_path0=/home/data/fastdfs/storage # FastDFS 存储文件时,采用了两级目录。这里配置存放文件的目录个数。 # 如果本参数只为 N(如: 256),那么 storage server 在初次运行时,会在 store_path 下自动创建 N * N 个存放文件的子目录。 subdir_count_per_path=256 # tracker_server 的列表 ,会主动连接 tracker_server # 有多个 tracker server 时,每个 tracker server 写一行 tracker_server=192.168.1.190:22122 # 允许系统同步的时间段 (默认是全天) 。一般用于避免高峰同步产生一些问题而设定。 sync_start_time=00:00 sync_end_time=23:59 使用ln -s 建立软链接 ln -s /usr/bin/fdfs_storaged /usr/local/bin 启动服务 service fdfs_storaged start 查看监听 netstat -unltp|grep fdfs 启动Storage前确保Tracker是启动的。初次启动成功,会在 /home/data/fastdfs/storage 目录下创建 data、 logs 两个目录。如果看到23000端口正常被监听后,这时候说明Storage服务启动成功啦! 查看Storage和Tracker是否在通信 /usr/bin/fdfs_monitor /etc/fdfs/storage.conf FastDFS 配置 Nginx 模块 软件包 版本 openresty v1.13.6.1 fastdfs-nginx-module v1.1.6 FastDFS 通过 Tracker 服务器,将文件放在 Storage 服务器存储, 但是同组存储服务器之间需要进行文件复制,有同步延迟的问题。 假设 Tracker 服务器将文件上传到了 192.168.1.190,上传成功后文件 ID已经返回给客户端。此时 FastDFS 存储集群机制会将这个文件同步到同组存192.168.1.190,在文件还没有复制完成的情况下,客户端如果用这个文件 ID 在 192.168.1.190 上取文件,就会出现文件无法访问的错误。而 fastdfs-nginx-module 可以重定向文件链接到源服务器取文件,避免客户端由于复制延迟导致的文件无法访问错误。 下载 安装 Nginx 和 fastdfs-nginx-module: 推荐您使用yum安装以下的开发库: yum install readline-devel pcre-devel openssl-devel -y 下载最新版本并解压: wget https://openresty.org/download/openresty-1.13.6.1.tar.gz tar -xvf openresty-1.13.6.1.tar.gz wget https://github.com/happyfish100/fastdfs-nginx-module/archive/master.zip unzip master.zip 配置 nginx 安装,加入fastdfs-nginx-module模块: ./configure --add-module=../fastdfs-nginx-module-master/src/ 编译、安装: make && make install 查看Nginx的模块: /usr/local/openresty/nginx/sbin/nginx -v 有下面这个就说明添加模块成功 复制 fastdfs-nginx-module 源码中的配置文件到/etc/fdfs 目录, 并修改: cp /fastdfs-nginx-module/src/mod_fastdfs.conf /etc/fdfs/ # 连接超时时间 connect_timeout=10 # Tracker Server tracker_server=192.168.1.190:22122 # StorageServer 默认端口 storage_server_port=23000 # 如果文件ID的uri中包含/group**,则要设置为true url_have_group_name = true # Storage 配置的store_path0路径,必须和storage.conf中的一致 store_path0=/home/data/fastdfs/storage 复制 FastDFS 的部分配置文件到/etc/fdfs 目录: cp /fastdfs-nginx-module/src/http.conf /etc/fdfs/ cp /fastdfs-nginx-module/src/mime.types /etc/fdfs/ 配置nginx,修改nginx.conf: location ~/group([0-9])/M00 { ngx_fastdfs_module; } 启动Nginx: [root@iz2ze7tgu9zb2gr6av1tysz sbin]# ./nginx ngx_http_fastdfs_set pid=9236 测试上传: [root@iz2ze7tgu9zb2gr6av1tysz fdfs]# /usr/bin/fdfs_upload_file /etc/fdfs/client.conf /etc/fdfs/4.jpg group1/M00/00/00/rBD8EFqVACuAI9mcAAC_ornlYSU088.jpg 部署结构图: JAVA 客户端集成 pom.xml引入: <!-- fastdfs --> <dependency> <groupId>org.csource</groupId> <artifactId>fastdfs-client-java</artifactId> <version>1.27</version> </dependency> fdfs_client.conf配置: #连接tracker服务器超时时长 connect_timeout = 2 #socket连接超时时长 network_timeout = 30 #文件内容编码 charset = UTF-8 #tracker服务器端口 http.tracker_http_port = 8080 http.anti_steal_token = no http.secret_key = FastDFS1234567890 #tracker服务器IP和端口(可以写多个) tracker_server = 192.168.1.190:22122 FastDFSClient上传类: public class FastDFSClient{ private static final String CONFIG_FILENAME = "D:\\itstyle\\src\\main\\resources\\fdfs_client.conf"; private static final String GROUP_NAME = "market1"; private TrackerClient trackerClient = null; private TrackerServer trackerServer = null; private StorageServer storageServer = null; private StorageClient storageClient = null; static{ try { ClientGlobal.init(CONFIG_FILENAME); } catch (IOException e) { e.printStackTrace(); } catch (MyException e) { e.printStackTrace(); } } public FastDFSClient() throws Exception { trackerClient = new TrackerClient(ClientGlobal.g_tracker_group); trackerServer = trackerClient.getConnection(); storageServer = trackerClient.getStoreStorage(trackerServer);; storageClient = new StorageClient(trackerServer, storageServer); } /** * 上传文件 * @param file 文件对象 * @param fileName 文件名 * @return */ public String[] uploadFile(File file, String fileName) { return uploadFile(file,fileName,null); } /** * 上传文件 * @param file 文件对象 * @param fileName 文件名 * @param metaList 文件元数据 * @return */ public String[] uploadFile(File file, String fileName, Map<String,String> metaList) { try { byte[] buff = IOUtils.toByteArray(new FileInputStream(file)); NameValuePair[] nameValuePairs = null; if (metaList != null) { nameValuePairs = new NameValuePair[metaList.size()]; int index = 0; for (Iterator<Map.Entry<String,String>> iterator = metaList.entrySet().iterator(); iterator.hasNext();) { Map.Entry<String,String> entry = iterator.next(); String name = entry.getKey(); String value = entry.getValue(); nameValuePairs[index++] = new NameValuePair(name,value); } } return storageClient.upload_file(GROUP_NAME,buff,fileName,nameValuePairs); } catch (Exception e) { e.printStackTrace(); } return null; } /** * 获取文件元数据 * @param fileId 文件ID * @return */ public Map<String,String> getFileMetadata(String groupname,String fileId) { try { NameValuePair[] metaList = storageClient.get_metadata(groupname,fileId); if (metaList != null) { HashMap<String,String> map = new HashMap<String, String>(); for (NameValuePair metaItem : metaList) { map.put(metaItem.getName(),metaItem.getValue()); } return map; } } catch (Exception e) { e.printStackTrace(); } return null; } /** * 删除文件 * @param fileId 文件ID * @return 删除失败返回-1,否则返回0 */ public int deleteFile(String groupname,String fileId) { try { return storageClient.delete_file(groupname,fileId); } catch (Exception e) { e.printStackTrace(); } return -1; } /** * 下载文件 * @param fileId 文件ID(上传文件成功后返回的ID) * @param outFile 文件下载保存位置 * @return */ public int downloadFile(String groupName,String fileId, File outFile) { FileOutputStream fos = null; try { byte[] content = storageClient.download_file(groupName,fileId); fos = new FileOutputStream(outFile); InputStream ips = new ByteArrayInputStream(content); IOUtils.copy(ips,fos); return 0; } catch (Exception e) { e.printStackTrace(); } finally { if (fos != null) { try { fos.close(); } catch (IOException e) { e.printStackTrace(); } } } return -1; } public static void main(String[] args) throws Exception { FastDFSClient client = new FastDFSClient(); File file = new File("D:\\23456.png"); String[] result = client.uploadFile(file, "png"); System.out.println(result.length); System.out.println(result[0]); System.out.println(result[1]); } } 执行main方法测试返回: 2 group1 M00/00/00/rBD8EFqTrNyAWyAkAAKCRJfpzAQ227.png 源码:https://gitee.com/52itstyle/spring-boot-fastdfs 作者: 小柒 出处: https://blog.52itstyle.com 分享是快乐的,也见证了个人成长历程,文章大多都是工作经验总结以及平时学习积累,基于自身认知不足之处在所难免,也请大家指正,共同进步。
NFS简介 NFS(Network File System)即网络文件系统。 主要功能:通过网络(局域网)让不同的主机系统之间可以共享文件或目录。 主要用途:NFS网络文件系统一般被用来存储共享视频,图片,附件等静态资源文件。 NFS存储服务 无NFS文件共享存储 当用户A通过互联网上传文件时,经过负载均衡,随机或者定向分配到某个节点。但是当用户B去下载这个文件的时候,并不确定会向哪个节点发送请求,这样会导致用户存在一定几率下载不到的情况。 有NFS文件共享存储 当用户A通过互联网上传文件时,经过负载均衡,无论发送到哪个节点都会被存储到NFS文件服务器。但是当用户B去下载这个文件的时候,任何节点都可以读取NFS文件服务器的文件。 NFS服务的优缺点 优点 简单容易上手 方便部署非常快速,维护十分简单 节省本地存储空间将常用的数据存放在一台服务器可以通过网络访问 缺点 在高并发下NFS效率/性能有限 NFS的数据是明文的,对数据完整性不做验证 多台机器挂载NFS服务器时,连接管理维护麻烦 容易发生单点故障,如果服务端宕机,所有客户端将不能访问 客户端没用用户认证机制,且数据是通过明文传送,安全性一般(一般建议在局域网内使用) RPC工作流程 NFS支持的功能非常多,不同的功能会有不同的服务来完成,很多服务都需要监听在一些端口,其中的很多端口并不是固定的。这些服务在启动时,都需要向rpcbind服务注册一个端口,rpcbind服务随机选取一个未被使用的端口予以分配。rpcbind服务监听在111端口,所以rpcbind的主要功能就是指定每个RPC service对应的port number,并且通知给客户端,让客户端连接到正确的端口上去。 客户端向NFS服务器端请求的步骤: 首先用户访问网站程序,由程序在NFS客户端上发出存取NFS文件的请求,这是NFS客户端的RPC服务就不通过网络向NFS服务器端的RPC服务的111端口发出NFS文件存取功能的查询请求,包括要实现的什么功能。 NFS服务器端的RPC服务找到对应的已注册的NFS端口,通知NFS客户端的RPC服务。 此时NFS客户端获取到正确的端口,并与NFS联机存取数据。 NFS客户端把数据存取成功后,返回给客户端程序,告知用户存取结果。 注意:由于rpc service在启动时需要向rpcbind注册端口,所以rpcbind要先启动。另外若rpcbind重新启动,原来注册的数据也会不见,因此一但rpcbind重新启动,让所管理的服务因为需要重新启动以重新向rpcbind注册。 NFS服务器端配置 NFS服务器:192.168.1.180 检查并安装NFS [root@iZ2ze74fkxrls31tr2ia2fZ ~]# rpm -qa rpcbind nfs-utils nfs-utils-1.3.0-0.48.el7_4.1.x86_64 rpcbind-0.2.0-42.el7.x86_64 如果没有,安装 NFS 服务器所需的软件包,实际上需要安装两个包nfs-utils(nfs服务主程序)和rpcbind(rpc主程序), 不过当使用yum安装nfs-utils时会把rpcbind一起安装上。 yum install -y nfs-utils 配置说明 /etc/sysconfig/nfs #NFS的主配置文件 /etc/exports #配置共享目录的文件 /etc/exports的配置格式: nfs共享目录 nfs客户端地址1(参1,参2.....) 客户端地址2(参1,参2...) 说明: nfs共享目录:为nfs服务器要共享的实际目录,绝对目录。注意权限问题。 nfs客户端地址:为nfs服务器授权可以访问的客户端的地址,可以是单独的ip地址或主机名,域名。也可以是整个网段。 授权整个网段:eg:10.0.0.0/24 文件配置实例说明: /data/fileserver 192.168.1.190(rw,sync,no_root_squash) 若服务器端对/etc/exports文件进行了修改,可以通过exportfs命令重新加载服务而不需要重启服务。若重启服务需要重新向prcbind注册,而且对客户端的影响也很大,所以尽量使用exportfs命令来使配置文件生效。 exportfs: exportfs -ar #重新导出所有的文件系统 exportfs -r #导出某个文件系统 exportfs -au #关闭导出的所有文件系统 exportfs -u #关闭指定的导出的文件系统 相关参数 (man exports) A. 选项:选项用来设置输出目录的访问权限、用户映射等。 设置输出目录只读:ro 设置输出目录读写:rw B. 用户映射选项 all_squash:将远程访问的所有普通用户及所属组都映射为匿名用户或用户组(nfsnobody); no_all_squash:与all_squash取反(默认设置); root_squash:将root用户及所属组都映射为匿名用户或用户组(默认设置); no_root_squash:与rootsquash取反; anonuid=xxx:将远程访问的所有用户都映射为匿名用户,并指定该用户为本地用户(UID=xxx); anongid=xxx:将远程访问的所有用户组都映射为匿名用户组账户,并指定该匿名用户组账户为本地用户组账户(GID=xxx); C. 其它选项 secure:限制客户端只能从小于1024的tcp/ip端口连接nfs服务器(默认设置); insecure:允许客户端从大于1024的tcp/ip端口连接服务器; sync:将数据同步写入内存缓冲区与磁盘中,效率低,但可以保证数据的一致性; async:将数据先保存在内存缓冲区中,必要时才写入磁盘; wdelay:检查是否有相关的写操作,如果有则将这些写操作一起执行,这样可以提高效率(默认设置); no_wdelay:若有写操作则立即执行,应与sync配合使用; subtree:若输出目录是一个子目录,则nfs服务器将检查其父目录的权限(默认设置); no_subtree:即使输出目录是一个子目录,nfs服务器也不检查其父目录的权限,这样可以提高效率; 启动NFS服务端上nfs服务 1、先为rpcbind和nfs做开机启动: systemctl enable rpcbind.service systemctl enable nfs-server.service 2、然后分别启动rpcbind和nfs服务: systemctl start rpcbind.service systemctl start nfs-server.service 查看服务是否启动 [root@iZ2ze74fkxrls31tr2ia2fZ ~]# rpcinfo -p program vers proto port service 100000 4 tcp 111 portmapper 100000 3 tcp 111 portmapper 100000 2 tcp 111 portmapper 100000 4 udp 111 portmapper 100000 3 udp 111 portmapper 100000 2 udp 111 portmapper 100024 1 udp 47426 status 100024 1 tcp 35379 status 100005 1 udp 20048 mountd 100005 1 tcp 20048 mountd 100005 2 udp 20048 mountd 100005 2 tcp 20048 mountd 100005 3 udp 20048 mountd 100005 3 tcp 20048 mountd 100003 3 tcp 2049 nfs 100003 4 tcp 2049 nfs 100227 3 tcp 2049 nfs_acl 100003 3 udp 2049 nfs 100003 4 udp 2049 nfs 100227 3 udp 2049 nfs_acl 100021 1 udp 53046 nlockmgr 100021 3 udp 53046 nlockmgr 100021 4 udp 53046 nlockmgr 100021 1 tcp 38280 nlockmgr 100021 3 tcp 38280 nlockmgr 100021 4 tcp 38280 nlockmgr 使用exportfs查看本机上已经共享的目录: exportfs NFS客户端配置 NFS客户端:192.168.1.190 安装nfs,并启动服务。 yum install -y nfs-utils systemctl enable rpcbind.service systemctl start rpcbind.service 客户端不需要启动nfs服务,只需要启动rpcbind服务。 检查 NFS 服务器端是否有目录共享 showmount -e 192.168.1.180 挂载远程服务 mount -t nfs 192.168.1.180:/data/fileserver /data/itstyle 查看挂载 df -h 开机挂载,编辑/etc/fstab vim /etc/fstab 加入以下内容: # 设备文件 挂载点 文件系统类型 mount参数 dump参数 fsck顺序 192.168.1.180:/data/fileserver /data/itstyle nfs defaults,_netdev 0 0 _netdev明确说明这是网络文件系统,避免网络启动前挂载出现错误。 保存后,重新挂载 /etc/fstab 里面的内容。mount -a 作者: 小柒 出处: https://blog.52itstyle.com 分享是快乐的,也见证了个人成长历程,文章大多都是工作经验总结以及平时学习积累,基于自身认知不足之处在所难免,也请大家指正,共同进步。
架构、分布式、日志队列,标题自己都看着唬人,其实就是一个日志收集的功能,只不过中间加了一个Kafka做消息队列罢了。 kafka介绍 Kafka是由Apache软件基金会开发的一个开源流处理平台,由Scala和Java编写。Kafka是一种高吞吐量的分布式发布订阅消息系统,它可以处理消费者规模的网站中的所有动作流数据。 这种动作(网页浏览,搜索和其他用户的行动)是在现代网络上的许多社会功能的一个关键因素。 这些数据通常是由于吞吐量的要求而通过处理日志和日志聚合来解决。 特性 Kafka是一种高吞吐量的分布式发布订阅消息系统,有如下特性: 通过O(1)的磁盘数据结构提供消息的持久化,这种结构对于即使数以TB的消息存储也能够保持长时间的稳定性能。 高吞吐量:即使是非常普通的硬件Kafka也可以支持每秒数百万的消息。 支持通过Kafka服务器和消费机集群来分区消息。 支持Hadoop并行数据加载。 主要功能 发布和订阅消息流,这个功能类似于消息队列,这也是kafka归类为消息队列框架的原因 以容错的方式记录消息流,kafka以文件的方式来存储消息流 可以再消息发布的时候进行处理 使用场景 在系统或应用程序之间构建可靠的用于传输实时数据的管道,消息队列功能 构建实时的流数据处理程序来变换或处理数据流,数据处理功能 消息传输流程 相关术语介绍 Broker Kafka集群包含一个或多个服务器,这种服务器被称为broker Topic每条发布到Kafka集群的消息都有一个类别,这个类别被称为Topic。(物理上不同Topic的消息分开存储,逻辑上一个Topic的消息虽然保存于一个或多个broker上但用户只需指定消息的Topic即可生产或消费数据而不必关心数据存于何处) PartitionPartition是物理上的概念,每个Topic包含一个或多个Partition. Producer负责发布消息到Kafka broker Consumer消息消费者,向Kafka broker读取消息的客户端。 Consumer Group每个Consumer属于一个特定的Consumer Group(可为每个Consumer指定group name,若不指定group name则属于默认的group) Kafka安装 环境 Linux、JDK、Zookeeper 下载二进制程序 wget https://archive.apache.org/dist/kafka/0.10.0.1/kafka_2.11-0.10.0.1.tgz 安装 tar -zxvf kafka_2.11-0.10.0.1.tgz cd kafka_2.11-0.10.0.1 目录说明 bin 启动,停止等命令 config 配置文件 libs 类库 参数说明 #########################参数解释############################## broker.id=0 #当前机器在集群中的唯一标识,和zookeeper的myid性质一样 port=9092 #当前kafka对外提供服务的端口默认是9092 host.name=192.168.1.170 #这个参数默认是关闭的 num.network.threads=3 #这个是borker进行网络处理的线程数 num.io.threads=8 #这个是borker进行I/O处理的线程数 log.dirs=/opt/kafka/kafkalogs/ #消息存放的目录,这个目录可以配置为“,”逗号分割的表达式,上面的num.io.threads要大于这个目录的个数这个目录,如果配置多个目录,新创建的topic他把消息持久化的地方是,当前以逗号分割的目录中,那个分区数最少就放那一个 socket.send.buffer.bytes=102400 #发送缓冲区buffer大小,数据不是一下子就发送的,先回存储到缓冲区了到达一定的大小后在发送,能提高性能 socket.receive.buffer.bytes=102400 #kafka接收缓冲区大小,当数据到达一定大小后在序列化到磁盘 socket.request.max.bytes=104857600 #这个参数是向kafka请求消息或者向kafka发送消息的请请求的最大数,这个值不能超过java的堆栈大小 num.partitions=1 #默认的分区数,一个topic默认1个分区数 log.retention.hours=168 #默认消息的最大持久化时间,168小时,7天 message.max.byte=5242880 #消息保存的最大值5M default.replication.factor=2 #kafka保存消息的副本数,如果一个副本失效了,另一个还可以继续提供服务 replica.fetch.max.bytes=5242880 #取消息的最大直接数 log.segment.bytes=1073741824 #这个参数是:因为kafka的消息是以追加的形式落地到文件,当超过这个值的时候,kafka会新起一个文件 log.retention.check.interval.ms=300000 #每隔300000毫秒去检查上面配置的log失效时间(log.retention.hours=168 ),到目录查看是否有过期的消息如果有,删除 log.cleaner.enable=false #是否启用log压缩,一般不用启用,启用的话可以提高性能 zookeeper.connect=192.168.1.180:12181,192.168.1.181:12181,192.168.1.182:1218 #设置zookeeper的连接端口、如果非集群配置一个地址即可 #########################参数解释############################## 启动kafka 启动kafka之前要启动相应的zookeeper集群、自行安装,这里不做说明。 #进入到kafka的bin目录 ./kafka-server-start.sh -daemon ../config/server.properties Kafka集成 环境 spring-boot、elasticsearch、kafka pom.xml引入: <!-- kafka 消息队列 --> <dependency> <groupId>org.springframework.kafka</groupId> <artifactId>spring-kafka</artifactId> <version>1.1.1.RELEASE</version> </dependency> 生产者 import java.util.HashMap; import java.util.Map; import org.apache.kafka.clients.producer.ProducerConfig; import org.apache.kafka.common.serialization.StringSerializer; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.kafka.annotation.EnableKafka; import org.springframework.kafka.core.DefaultKafkaProducerFactory; import org.springframework.kafka.core.KafkaTemplate; import org.springframework.kafka.core.ProducerFactory; /** * 生产者 * 创建者 科帮网 * 创建时间 2018年2月4日 */ @Configuration @EnableKafka public class KafkaProducerConfig { @Value("${kafka.producer.servers}") private String servers; @Value("${kafka.producer.retries}") private int retries; @Value("${kafka.producer.batch.size}") private int batchSize; @Value("${kafka.producer.linger}") private int linger; @Value("${kafka.producer.buffer.memory}") private int bufferMemory; public Map<String, Object> producerConfigs() { Map<String, Object> props = new HashMap<>(); props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, servers); props.put(ProducerConfig.RETRIES_CONFIG, retries); props.put(ProducerConfig.BATCH_SIZE_CONFIG, batchSize); props.put(ProducerConfig.LINGER_MS_CONFIG, linger); props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, bufferMemory); props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); return props; } public ProducerFactory<String, String> producerFactory() { return new DefaultKafkaProducerFactory<>(producerConfigs()); } @Bean public KafkaTemplate<String, String> kafkaTemplate() { return new KafkaTemplate<String, String>(producerFactory()); } } 消费者 mport java.util.HashMap; import java.util.Map; import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.common.serialization.StringDeserializer; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.kafka.annotation.EnableKafka; import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory; import org.springframework.kafka.config.KafkaListenerContainerFactory; import org.springframework.kafka.core.ConsumerFactory; import org.springframework.kafka.core.DefaultKafkaConsumerFactory; import org.springframework.kafka.listener.ConcurrentMessageListenerContainer; /** * 消费者 * 创建者 科帮网 * 创建时间 2018年2月4日 */ @Configuration @EnableKafka public class KafkaConsumerConfig { @Value("${kafka.consumer.servers}") private String servers; @Value("${kafka.consumer.enable.auto.commit}") private boolean enableAutoCommit; @Value("${kafka.consumer.session.timeout}") private String sessionTimeout; @Value("${kafka.consumer.auto.commit.interval}") private String autoCommitInterval; @Value("${kafka.consumer.group.id}") private String groupId; @Value("${kafka.consumer.auto.offset.reset}") private String autoOffsetReset; @Value("${kafka.consumer.concurrency}") private int concurrency; @Bean public KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<String, String>> kafkaListenerContainerFactory() { ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>(); factory.setConsumerFactory(consumerFactory()); factory.setConcurrency(concurrency); factory.getContainerProperties().setPollTimeout(1500); return factory; } public ConsumerFactory<String, String> consumerFactory() { return new DefaultKafkaConsumerFactory<>(consumerConfigs()); } public Map<String, Object> consumerConfigs() { Map<String, Object> propsMap = new HashMap<>(); propsMap.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, servers); propsMap.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, enableAutoCommit); propsMap.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, autoCommitInterval); propsMap.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, sessionTimeout); propsMap.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); propsMap.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); propsMap.put(ConsumerConfig.GROUP_ID_CONFIG, groupId); propsMap.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, autoOffsetReset); return propsMap; } @Bean public Listener listener() { return new Listener(); } } 日志监听 import org.apache.kafka.clients.consumer.ConsumerRecord; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.kafka.annotation.KafkaListener; import org.springframework.stereotype.Component; import com.itstyle.es.common.utils.JsonMapper; import com.itstyle.es.log.entity.SysLogs; import com.itstyle.es.log.repository.ElasticLogRepository; /** * 扫描监听 * 创建者 科帮网 * 创建时间 2018年2月4日 */ @Component public class Listener { protected final Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired private ElasticLogRepository elasticLogRepository; @KafkaListener(topics = {"itstyle"}) public void listen(ConsumerRecord<?, ?> record) { logger.info("kafka的key: " + record.key()); logger.info("kafka的value: " + record.value()); if(record.key().equals("itstyle_log")){ try { SysLogs log = JsonMapper.fromJsonString(record.value().toString(), SysLogs.class); logger.info("kafka保存日志: " + log.getUsername()); elasticLogRepository.save(log); } catch (Exception e) { e.printStackTrace(); } } } } 测试日志传输 /** * kafka 日志队列测试接口 */ @GetMapping(value="kafkaLog") public @ResponseBody String kafkaLog() { SysLogs log = new SysLogs(); log.setUsername("红薯"); log.setOperation("开源中国社区"); log.setMethod("com.itstyle.es.log.controller.kafkaLog()"); log.setIp("192.168.1.80"); log.setGmtCreate(new Timestamp(new Date().getTime())); log.setExceptionDetail("开源中国社区"); log.setParams("{'name':'码云','type':'开源'}"); log.setDeviceType((short)1); log.setPlatFrom((short)1); log.setLogType((short)1); log.setDeviceType((short)1); log.setId((long)200000); log.setUserId((long)1); log.setTime((long)1); //模拟日志队列实现 String json = JsonMapper.toJsonString(log); kafkaTemplate.send("itstyle", "itstyle_log",json); return "success"; } Kafka与Redis 之前简单的介绍过,JavaWeb项目架构之Redis分布式日志队列,有小伙伴们聊到, Redis PUB/SUB没有任何可靠性保障,也不会持久化。当然了,原项目中仅仅是记录日志,并不是十分重要的信息,可以有一定程度上的丢失 Kafka与Redis PUB/SUB之间最大的区别在于Kafka是一个完整的分布式发布订阅消息系统,而Redis PUB/SUB只是一个组件而已。 使用场景 Redis PUB/SUB 消息持久性需求不高、吞吐量要求不高、可以忍受数据丢失 Kafka高可用、高吞吐、持久性、多样化的消费处理模型 开源项目源码(参考):https://gitee.com/52itstyle/spring-boot-elasticsearch 作者: 小柒 出处: https://blog.52itstyle.com 分享是快乐的,也见证了个人成长历程,文章大多都是工作经验总结以及平时学习积累,基于自身认知不足之处在所难免,也请大家指正,共同进步。
前几天有个买链接的,顺手查了下站的权重,果然又回到1了,尽管不是太在意这个东西,但是总归越高越好了。 然而重点是在爱站的最下面居然发现了居然没有开启Gizp,由于论坛开启CDN一直有问题,就放着没弄, 虽然是2MB的带宽,也不能这么玩不是。 新建 gizp.conf #开启gzip压缩 gzip on; #设置允许压缩的页面最小字节数 gzip_min_length 1k; #申请4个单位为16K的内存作为压缩结果流缓存 gzip_buffers 4 16k; #设置识别http协议的版本,默认为1.1 gzip_http_version 1.1; #指定gzip压缩比,1-9数字越小,压缩比越小,速度越快 gzip_comp_level 2; #指定压缩的类型 gzip_types text/plain application/javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png; #让前端的缓存服务器进过gzip压缩的页面 gzip_vary on; #IE6对Gzip不怎么友好,不给它Gzip了 gzip_disable "MSIE [1-6]\."; 导入 nginx.onf http { include mime.types; default_type application/octet-stream; #开启高效文件传输模式 sendfile on; #设置客户端连接保存活动的超时时间 keepalive_timeout 65; #压缩配置 include gizp.conf; #导入项目网站 include vhosts/*.conf; } 重启Nginx服务 nginx -s reload 用curl测试Gzip是否成功开启 [root@itstyle nginx]# curl -I -H "Accept-Encoding: gzip, deflate" "http://www.52itstyle.com/data/cache/common.js" HTTP/1.1 200 OK Server: nginx/1.10.3 Date: Mon, 05 Feb 2018 01:49:24 GMT Content-Type: application/javascript Last-Modified: Wed, 13 Dec 2017 07:45:08 GMT Connection: keep-alive ETag: W/"5a30da84-d95d" Content-Encoding: gzip 作者: 小柒 出处: https://blog.52itstyle.com 分享是快乐的,也见证了个人成长历程,文章大多都是工作经验总结以及平时学习积累,基于自身认知不足之处在所难免,也请大家指正,共同进步。
Elasticsearch (ES)是一个基于 Lucene 的开源搜索引擎,它不但稳定、可靠、快速,而且也具有良好的水平扩展能力,是专门为分布式环境设计的。 特性 安装方便:没有其他依赖,下载后安装非常方便;只用修改几个参数就可以搭建起来一个集群 JSON:输入/输出格式为 JSON,意味着不需要定义 Schema,快捷方便 RESTful:基本所有操作(索引、查询、甚至是配置)都可以通过 HTTP 接口进行 分布式:节点对外表现对等(每个节点都可以用来做入口);加入节点自动均衡 多租户:可根据不同的用途分索引;可以同时操作多个索引 集群 其中一个节点就是一个 ES 进程,多个节点组成一个集群。一般每个节点都运行在不同的操作系统上,配置好集群相关参数后 ES 会自动组成集群(节点发现方式也可以配置)。集群内部通过 ES 的选主算法选出主节点,而集群外部则是可以通过任何节点进行操作,无主从节点之分(对外表现对等/去中心化,有利于客户端编程,例如故障重连)。 分片 ES 是一个分布式系统,我们一开始就应该以集群的方式来使用它。它保存索引时会选择适合的“主分片”(Primary Shard),把索引保存到其中(我们可以把分片理解为一块物理存储区域)。分片的分法是固定的,而且是安装时候就必须要决定好的(默认是 5),后面就不能改变了。 既然有主分片,那肯定是有“从”分片的,在 ES 里称之为“副本分片”(Replica Shard)。副本分片主要有两个作用: 高可用:某分片节点挂了的话可走其他副本分片节点,节点恢复后上面的分片数据可通过其他节点恢复 负载均衡:ES 会自动根据负载情况控制搜索路由,副本分片可以将负载均摊 索引 RESTful 这个特性非常方便,最关键的是 ES 的 HTTP 接口不只是可以进行业务操作(索引/搜索),还可以进行配置,甚至是关闭 ES 集群。下面我们介绍几个很常用的接口: /_cat/nodes?v:查集群状态 /_cat/shards?v:查看分片状态 /${index}/${type}/_search?pretty:搜索 v 是 verbose 的意思,这样可以更可读(有表头,有对齐),_cat 是监测相关的 APIs,/_cat?help 来获取所有接口。${index} 和 ${type} 分别是具体的某一索引某一类型,是分层次的。我们也可以直接在所有索引所有类型上进行搜索:/_search。 日志处理 前面介绍了那么多Elasticsearch简介和特性,大多源自官方介绍和百度,其实写这篇文章的目的就是如何基于Elasticsearch构建网站日志处理系统,通过数据同步工具等一些列开源组件来快速构建一个日志处理系统,后面附项目Demo雏形,并不完善,初步成型中。 开发环境 JDK1.7、Maven、Eclipse、SpringBoot1.5.9、elasticsearch2.4.6、Dubbox2.8.4、zookeeper3.4.6、Vue、Iview 版本介绍 spring-boot-starter-parent-1.5.9.RELEASE、spring-data-elasticsearch-2.1.9.RELEAS、elasticsearch-2.4.6(5.0+以上需要依赖JDK8) 截止2018年1月22日,ElasticSearch目前最新的已到6.1.2,但是spring-boot的更新速度远远跟不上ElasticSearch更新的速度,目前spring-boot支持的最新版本是elasticsearch-2.4.6。 服务说明 使用本地ElasticSearch服务(application-dev.properties) spring.data.elasticsearch.cluster-name=elasticsearch #默认就是本机,如果要使用远程服务器,或者局域网服务器,那就需要在这里配置ip:prot;可以配置多个,以逗号分隔,相当于集群。 #Java客户端:通过9300端口与集群进行交互 #其他所有程序语言:都可以使用RESTful API,通过9200端口的与Elasticsearch进行通信。 #spring.data.elasticsearch.cluster-nodes=192.168.1.180:9300 使用远程ElasticSearch服务(application-dev.properties) 需要自行安装ElasticSearch,注意ElasticSearch版本尽量要与JAR包一致。 下载地址:https://www.elastic.co/downloads/past-releases/elasticsearch-2-4-6 安装说明:http://www.52itstyle.com/thread-20114-1-1.html 新版本不建议使用root用户启动,需要自建ElasticSearch用户,也可以使用以下命令启动 elasticsearch -Des.insecure.allow.root=true -d 或者在elasticsearch中加入ES_JAVA_OPTS="-Des.insecure.allow.root=true"。 项目结构 ├─src │ ├─main │ │ ├─java │ │ │ └─com │ │ │ └─itstyle │ │ │ └─es │ │ │ │ Application.java │ │ │ │ │ │ │ ├─common │ │ │ │ ├─constant │ │ │ │ │ PageConstant.java │ │ │ │ │ │ │ │ │ └─interceptor │ │ │ │ MyAdapter.java │ │ │ │ │ │ │ └─log │ │ │ ├─controller │ │ │ │ LogController.java │ │ │ │ │ │ │ ├─entity │ │ │ │ Pages.java │ │ │ │ SysLogs.java │ │ │ │ │ │ │ ├─repository │ │ │ │ ElasticLogRepository.java │ │ │ │ │ │ │ └─service │ │ │ │ LogService.java │ │ │ │ │ │ │ └─impl │ │ │ LogServiceImpl.java │ │ │ │ │ ├─resources │ │ │ │ application-dev.properties │ │ │ │ application-prod.properties │ │ │ │ application-test.properties │ │ │ │ application.yml │ │ │ │ spring-context-dubbo.xml │ │ │ │ │ │ │ ├─static │ │ │ │ ├─iview │ │ │ │ │ │ iview.css │ │ │ │ │ │ iview.min.js │ │ │ │ │ │ │ │ │ │ │ └─fonts │ │ │ │ │ ionicons.eot │ │ │ │ │ ionicons.svg │ │ │ │ │ ionicons.ttf │ │ │ │ │ ionicons.woff │ │ │ │ │ │ │ │ │ ├─jquery │ │ │ │ │ jquery-3.2.1.min.js │ │ │ │ │ │ │ │ │ └─vue │ │ │ │ vue.min.js │ │ │ │ │ │ │ └─templates │ │ │ └─log │ │ │ index.html │ │ │ │ │ └─webapp │ │ │ index.jsp │ │ │ │ │ └─WEB-INF │ │ web.xml │ │ │ └─test │ └─java │ └─com │ └─itstyle │ └─es │ └─test │ Logs.java │ 项目演示 演示网址:http://es.52itstyle.com 项目截图 分页查询 使用ElasticsearchTemplate模板插入了20万条数据,本地向外网服务器(1核1G),用时60s+,一分钟左右的时间。虽然索引库容量有增加,但是等了大约 10分钟左右的时间才能搜索出来。 分页查询到10000+的时候系统报错,Result window is too large,修改config下的elasticsearch.yml 追加以下代码即可: # 自行定义数量 index.max_result_window : '10000000' 参考:https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules.html Java API Elasticsearch为Java用户提供了两种内置客户端: 节点客户端(node client):节点客户端,顾名思义,其本身也是Elasticsearch集群的一个组成部分。以无数据节点(none data node)身份加入集群,换言之,它自己不存储任何数据,但是它知道数据在集群中的具体位置,并且能够直接转发请求到对应的节点上。 传输客户端(Transport client):这个更轻量的传输客户端能够发送请求到远程集群。它自己不加入集群,只是简单转发请求给集群中的节点。两个Java客户端都通过9300端口与集群交互,使用Elasticsearch传输协议(Elasticsearch Transport Protocol)。集群中的节点之间也通过9300端口进行通信。如果此端口未开放,你的节点将不能组成集群。 安装Elasticsearch-Head elasticsearch-head是一个界面化的集群操作和管理工具,可以对集群进行傻瓜式操作。你可以通过插件把它集成到es(首选方式),也可以安装成一个独立webapp。 es-head主要有三个方面的操作: 显示集群的拓扑,并且能够执行索引和节点级别操作 搜索接口能够查询集群中原始json或表格格式的检索数据 能够快速访问并显示集群的状态 插件安装方式、参考:https://github.com/mobz/elasticsearch-head for Elasticsearch 5.x: site plugins are not supported. Run as a standalone server for Elasticsearch 2.x: sudo elasticsearch/bin/plugin install mobz/elasticsearch-head for Elasticsearch 1.x: sudo elasticsearch/bin/plugin -install mobz/elasticsearch-head/1.x for Elasticsearch 0.x: sudo elasticsearch/bin/plugin -install mobz/elasticsearch-head/0.9 安装成功以后会在plugins目录下出现一个head目录,表明安装已经成功。 浏览截图: x-pack监控 Elasticsearch、Logstash 随着 Kibana 的命名升级直接从2.4跳跃到了5.0,5.x版本的 ELK 在版本对应上要求相对较高,不再支持5.x和2.x的混搭,同时 Elastic 做了一个 package ,对原本的 marvel、watch、alert 做了一个封装,形成了 x-pack 。 安装:https://www.elastic.co/guide/en/elasticsearch/reference/6.1/installing-xpack-es.html 用户管理x-pack安装之后有一个超级用户elastic ,其默认的密码是changeme,拥有对所有索引和数据的控制权,可以使用该用户创建和修改其他用户,当然这里可以通过kibana的web界面进行用户和用户组的管理。 修改elastic用户的密码: curl -XPUT -u elastic 'localhost:9200/_xpack/security/user/elastic/_password' -d '{ "password" : "123456" }' IK Analysis for Elasticsearch 下载安装: 方式一 - download pre-build package from here: https://github.com/medcl/elasticsearch-analysis-ik/releases unzip plugin to folder your-es-root/plugins/ 方式一二 - use elasticsearch-plugin to install ( version > v5.5.1 ): ./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v6.0.0/elasticsearch-analysis-ik-6.0.0.zip 由于Elasticsearch版本是2.4.6,这里选择IK版本为1.10.6 wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v1.10.6/elasticsearch-analysis-ik-1.10.6.zip 下载解压以后在 Elasticsearch 的config下的elasticsearch.yml文件中,添加如下代码(2.0以上可以不设置)。 index: analysis: analyzer: ik: alias: [ik_analyzer] type: org.elasticsearch.index.analysis.IkAnalyzerProvider ik_max_word: type: ik use_smart: false ik_smart: type: ik use_smart: true 或者 index.analysis.analyzer.ik.type : “ik” 安装前: http://192.168.1.180:9200/_analyze?analyzer=standard&pretty=true&text=我爱你中国 { "tokens" : [ { "token" : "我", "start_offset" : 0, "end_offset" : 1, "type" : "<IDEOGRAPHIC>", "position" : 0 }, { "token" : "爱", "start_offset" : 1, "end_offset" : 2, "type" : "<IDEOGRAPHIC>", "position" : 1 }, { "token" : "你", "start_offset" : 2, "end_offset" : 3, "type" : "<IDEOGRAPHIC>", "position" : 2 }, { "token" : "中", "start_offset" : 3, "end_offset" : 4, "type" : "<IDEOGRAPHIC>", "position" : 3 }, { "token" : "国", "start_offset" : 4, "end_offset" : 5, "type" : "<IDEOGRAPHIC>", "position" : 4 } ] } 安装后: http://121.42.155.213:9200/_analyze?analyzer=ik&pretty=true&text=我爱你中国 { "tokens" : [ { "token" : "我爱你", "start_offset" : 0, "end_offset" : 3, "type" : "CN_WORD", "position" : 0 }, { "token" : "爱你", "start_offset" : 1, "end_offset" : 3, "type" : "CN_WORD", "position" : 1 }, { "token" : "中国", "start_offset" : 3, "end_offset" : 5, "type" : "CN_WORD", "position" : 2 } ] } 数据同步 使用第三方工具类库elasticsearch-jdbc实现MySql到elasticsearch的同步。 运行环境:centos7.5、JDK8、elasticsearch-jdbc-2.3.2.0 安装步骤: 这里是列表文本第一步:下载(可能很卡、请耐心等待) wget http://xbib.org/repository/org/xbib/elasticsearch/importer/elasticsearch-jdbc/2.3.2.0/elasticsearch-jdbc-2.3.2.0-dist.zip 这里是列表文本第二步:解压 unzip elasticsearch-jdbc-2.3.2.0-dist.zip 这里是列表文本第三步:配置脚本mysql_import_es.sh #!/bin/sh # elasticsearch-jdbc 安装路径 bin=/home/elasticsearch-jdbc-2.3.2.0/bin lib=/home/elasticsearch-jdbc-2.3.2.0/lib echo '{ "type" : "jdbc", "jdbc": { # 如果数据库中存在Json文件 这里设置成false,否则会同步出错 "detect_json":false, "url":"jdbc:mysql://127.0.0.1:3306/itstyle_log??useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true", "user":"root", "password":"root", # 如果想自动生成_id,去掉第一个获取字段即可;如果想Id作为主键,把id设置为_id即可 "sql":"SELECT id AS _id,id,user_id AS userId ,username,operation,time,method,params,ip,device_type AS deviceType,log_type AS logType,exception_detail AS exceptionDetail, gmt_create AS gmtCreate,plat_from AS platFrom FROM sys_log", "elasticsearch" : { "host" : "127.0.0.1",#elasticsearch服务地址 "port" : "9300" #远程elasticsearch服务 此端口一定要开放 }, "index" : "elasticsearch",# 索引名相当于库 "type" : "sysLog" # 类型名相当于表 } }' | java \ -cp "${lib}/*" \ -Dlog4j.configurationFile=${bin}/log4j2.xml \ org.xbib.tools.Runner \ org.xbib.tools.JDBCImporter 这里是列表文本第四部:授权并执行 chmod +x mysql_import_es.sh ./mysql_import_es.sh Repository和Template Spring-data-elasticsearch是Spring提供的操作ElasticSearch的数据层,封装了大量的基础操作,通过它可以很方便的操作ElasticSearch的数据。 ElasticSearchRepository的基本使用 @NoRepositoryBean public interface ElasticsearchRepository<T, ID extends Serializable> extends ElasticsearchCrudRepository<T, ID> { <S extends T> S index(S entity); Iterable<T> search(QueryBuilder query); Page<T> search(QueryBuilder query, Pageable pageable); Page<T> search(SearchQuery searchQuery); Page<T> searchSimilar(T entity, String[] fields, Pageable pageable); void refresh(); Class<T> getEntityClass(); } ElasticsearchRepository里面有几个特殊的search方法,这些是ES特有的,和普通的JPA区别的地方,用来构建一些ES查询的。 主要是看QueryBuilder和SearchQuery两个参数,要完成一些特殊查询就主要看构建这两个参数。 一般情况下,我们不是直接是new NativeSearchQuery,而是使用NativeSearchQueryBuilder。 通过NativeSearchQueryBuilder.withQuery(QueryBuilder1).withFilter(QueryBuilder2).withSort(SortBuilder1).withXXXX().build();这样的方式来完成NativeSearchQuery的构建。 ElasticSearchTemplate的使用 ElasticSearchTemplate更多是对ESRepository的补充,里面提供了一些更底层的方法。 这里我们主要实现快读批量插入的功能,插入20万条数据,本地向外网服务器(1核1G),用时60s+,一分钟左右的时间。虽然索引库容量有增加,但是等了大约10分钟左右的时间才能搜索出来。 //批量同步或者插入数据 public void bulkIndex(List<SysLogs> logList) { long start = System.currentTimeMillis(); int counter = 0; try { List<IndexQuery> queries = new ArrayList<>(); for (SysLogs log : logList) { IndexQuery indexQuery = new IndexQuery(); indexQuery.setId(log.getId()+ ""); indexQuery.setObject(log); indexQuery.setIndexName("elasticsearch"); indexQuery.setType("sysLog"); //也可以使用IndexQueryBuilder来构建 //IndexQuery index = new IndexQueryBuilder().withId(person.getId() + "").withObject(person).build(); queries.add(indexQuery); if (counter % 1000 == 0) { elasticSearchTemplate.bulkIndex(queries); queries.clear(); System.out.println("bulkIndex counter : " + counter); } counter++; } if (queries.size() > 0) { elasticSearchTemplate.bulkIndex(queries); } long end = System.currentTimeMillis(); System.out.println("bulkIndex completed use time:"+ (end-start)); } catch (Exception e) { System.out.println("IndexerService.bulkIndex e;" + e.getMessage()); throw e; } } 开源项目源码(参考):https://gitee.com/52itstyle/spring-boot-elasticsearch 作者: 小柒 出处: https://blog.52itstyle.com 分享是快乐的,也见证了个人成长历程,文章大多都是工作经验总结以及平时学习积累,基于自身认知不足之处在所难免,也请大家指正,共同进步。
-------------------------