-
Notifications
You must be signed in to change notification settings - Fork 0
/
content.json
1 lines (1 loc) · 215 KB
/
content.json
1
[{"title":"【Spring】缓存注解@Cacheable、@CacheEvict、@CachePut使用","date":"2024-05-25T05:59:20.000Z","path":"article/20240525.html","text":"【Spring】缓存注解@Cacheable、@CacheEvict、@CachePut使用Spring Cache 通过封装统一的缓存抽象层,支持多种缓存实现(如Ehcache、Redis、Caffeine等)。可以使用注解@Cacheable、@CacheEvict、@CachePut等优雅的进行使用,无需修改业务代码。 @Cacheable@Cacheable注解用于在方法执行前检查缓存,如果缓存中有数据则返回缓存中的数据,否则执行方法并将结果缓存。 工作原理: 方法调用前: AOP代理拦截方法调用。 从指定的缓存区(value属性指定)中根据缓存键(key属性指定)查找缓存条目。 如果找到缓存条目,则返回缓存的值,不执行目标方法。 方法调用后: 如果缓存中没有找到条目,则执行目标方法。 将方法返回值存储到缓存中,使用指定的缓存区和缓存键。 示例: 1234@Cacheable(value = "users", key = "#userId")public User getUserById(Long userId) { return userRepository.findById(userId).orElse(null);} @CacheEvict@CacheEvict注解用于从缓存中移除一个或多个条目,通常在数据修改或删除操作时使用。 工作原理: 方法调用后: AOP代理拦截方法调用。 执行目标方法。 从指定的缓存区(value属性指定)中根据缓存键(key属性指定)移除缓存条目。 如果设置了allEntries = true,则清空整个缓存区。 示例: 1234@CacheEvict(value = "users", key = "#userId")public void deleteUserById(Long userId) { userRepository.deleteById(userId);} @CachePut@CachePut注解用于在方法执行后将结果更新到缓存中。与@Cacheable不同,它总是会执行目标方法。 工作原理: 方法调用后: AOP代理拦截方法调用。 执行目标方法。 将方法返回值存储到缓存中,使用指定的缓存区(value属性指定)和缓存键(key属性指定)。 示例: 1234@CachePut(value = "users", key = "#user.id")public User updateUser(User user) { return userRepository.save(user);} @CacheConfig注解用于在类级别配置缓存的公共设置,简化方法级别的缓存配置。如果类中的多个方法共享相同的缓存配置(例如相同的缓存名称),使用@CacheConfig可以避免在每个方法上重复配置。 @CacheConfig的作用 缓存名称:设置默认的缓存名称,应用于该类中的所有缓存操作。 缓存管理器:指定默认的缓存管理器,应用于该类中的所有缓存操作。 示例假设我们有一个UserService类,其中多个方法需要使用相同的缓存配置。 不使用@CacheConfig1234567891011121314151617181920212223import org.springframework.cache.annotation.Cacheable;import org.springframework.cache.annotation.CacheEvict;import org.springframework.cache.annotation.CachePut;import org.springframework.stereotype.Service;@Servicepublic class UserService { @Cacheable(value = "users", key = "#userId") public User getUserById(Long userId) { return userRepository.findById(userId).orElse(null); } @CacheEvict(value = "users", key = "#userId") public void deleteUserById(Long userId) { userRepository.deleteById(userId); } @CachePut(value = "users", key = "#user.id") public User updateUser(User user) { return userRepository.save(user); }} 在这个例子中,每个缓存注解都需要指定value = "users",如果有多个方法共享相同的缓存名称,这样会显得冗余。 使用@CacheConfig12345678910111213141516171819202122232425import org.springframework.cache.annotation.CacheConfig;import org.springframework.cache.annotation.Cacheable;import org.springframework.cache.annotation.CacheEvict;import org.springframework.cache.annotation.CachePut;import org.springframework.stereotype.Service;@Service@CacheConfig(cacheNames = "users")public class UserService { @Cacheable(key = "#userId") public User getUserById(Long userId) { return userRepository.findById(userId).orElse(null); } @CacheEvict(key = "#userId") public void deleteUserById(Long userId) { userRepository.deleteById(userId); } @CachePut(key = "#user.id") public User updateUser(User user) { return userRepository.save(user); }} 在这个例子中: @CacheConfig(cacheNames = "users"):在类级别配置默认的缓存名称为“users”。 各个方法上不再需要重复指定缓存名称,只需要配置key属性。 @CacheConfig 的使用场景 统一缓存配置:当一个类中多个方法使用相同的缓存名称或缓存管理器时,可以通过@CacheConfig统一配置,减少代码冗余。 简化配置:避免在每个方法的缓存注解中重复指定相同的缓存名称或其他配置。 进一步示例假设我们在同一个类中使用多个缓存名称,可以通过@CacheConfig配置默认的缓存名称,同时在特定方法上覆盖默认配置。 123456789101112131415161718192021222324252627282930import org.springframework.cache.annotation.CacheConfig;import org.springframework.cache.annotation.Cacheable;import org.springframework.cache.annotation.CacheEvict;import org.springframework.cache.annotation.CachePut;import org.springframework.stereotype.Service;@Service@CacheConfig(cacheNames = "users")public class UserService { @Cacheable(key = "#userId") public User getUserById(Long userId) { return userRepository.findById(userId).orElse(null); } @CacheEvict(key = "#userId") public void deleteUserById(Long userId) { userRepository.deleteById(userId); } @CachePut(key = "#user.id") public User updateUser(User user) { return userRepository.save(user); } @Cacheable(value = "admins", key = "#adminId") public Admin getAdminById(Long adminId) { return adminRepository.findById(adminId).orElse(null); }} 在这个例子中: @CacheConfig(cacheNames = "users"):在类级别设置默认缓存名称为“users”。 getAdminById方法通过@Cacheable(value = "admins", key = "#adminId")覆盖了默认的缓存名称,使用“admins”缓存。 通过使用@CacheConfig,可以使代码更清晰,避免重复配置,提高可维护性。 实现细节 Spring AOP代理: Spring AOP通过JDK动态代理或CGLIB创建代理对象。 代理对象拦截方法调用,在调用目标方法之前或之后执行缓存逻辑。 CacheManager和Cache接口: CacheManager:Spring缓存抽象中的核心接口,用于管理不同的缓存实现。 Cache:表示具体的缓存,提供基本的缓存操作方法,如get、put、evict等。 缓存配置: 在Spring Boot项目中,通过@EnableCaching注解启用缓存功能。 在配置文件中指定具体的缓存实现(如Ehcache、Redis等)的配置。 示例项目配置引入依赖: 在pom.xml中添加依赖: 123456789<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> 配置文件Yaml: 123456789101112spring: application: name: buzz-chat data: redis: host: 127.0.0.1 port: 6379 database: 0 cache: type: REDIS 主配置类: 1234567891011import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cache.annotation.EnableCaching;@SpringBootApplication@EnableCachingpublic class CacheApplication { public static void main(String[] args) { SpringApplication.run(CacheApplication.class, args); }} 通过上述配置和代码,Spring Cache和缓存注解可以在Spring应用中无缝工作。AOP和缓存抽象机制使得这些缓存操作透明地集成到业务逻辑中,大大简化了开发和维护工作。","tags":[{"name":"Spring","slug":"Spring","permalink":"https://alexhuihui.github.io/tags/Spring/"},{"name":"缓存","slug":"缓存","permalink":"https://alexhuihui.github.io/tags/%E7%BC%93%E5%AD%98/"},{"name":"Cache","slug":"Cache","permalink":"https://alexhuihui.github.io/tags/Cache/"}]},{"title":"【MySQL】索引失效的场景","date":"2024-05-22T00:48:09.000Z","path":"article/20240522.html","text":"今天我们讨论一下数据库索引在什么情况下会失效,总的来说有以下几种场景 在索引列上加函数运算 组合索引中,不符合最左匹配原则 当索引列存在隐式转化的时候 使用like通配符匹配后缀%xxx的时候 使用or连接查询的时候,or语句前后没有同时使用索引 设计 SQL 表假设我们设计一个员工表 employees,包含以下字段: employee_id:员工ID(字符串类型,作为索引列) name:员工姓名 department_id:部门ID(字符串类型,作为索引列) age:员工年龄 创建表的 SQL 语句12345678CREATE TABLE employees ( employee_id VARCHAR(20) NOT NULL, name VARCHAR(100), department_id VARCHAR(20), age INT, PRIMARY KEY (employee_id), KEY idx_department_id (department_id)); 插入示例数据123456INSERT INTO employees (employee_id, name, department_id, age) VALUES('E001', 'Alice', 'D001', 30),('E002', 'Bob', 'D002', 25),('E003', 'Charlie', 'D001', 28),('E004', 'David', 'D003', 35),('E005', 'Eve', 'D002', 22); 查询语句及其 EXPLAIN 分析1. 隐式类型转换导致索引失效查询员工ID为123的员工信息: 1SELECT * FROM employees WHERE employee_id = 123; 分析: 1EXPLAIN SELECT * FROM employees WHERE employee_id = 123; 2. LIKE 通配符匹配后缀导致索引失效查询名字以 ‘lice’ 结尾的员工: 1SELECT * FROM employees WHERE name LIKE '%lice'; 分析: 1EXPLAIN SELECT * FROM employees WHERE name LIKE '%lice'; 3. OR 连接查询索引失效查询员工ID为’E001’或部门ID为’D003’的员工信息: 1SELECT * FROM employees WHERE employee_id = 'E001' OR department_id = 'D003'; 分析: 1EXPLAIN SELECT * FROM employees WHERE employee_id = 'E001' OR department_id = 'D003'; 4. 正确使用索引的查询查询名字以 ‘A’ 开头的员工: 1SELECT * FROM employees WHERE name LIKE 'A%'; 分析: 1EXPLAIN SELECT * FROM employees WHERE name LIKE 'A%'; EXPLAIN 结果分析通过 EXPLAIN 语句,可以分析每个查询的执行计划。以下是可能的解释: 1. 隐式类型转换导致索引失效12345+----+-------------+-----------+------+---------------+------+---------+------+------+-------------+| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |+----+-------------+-----------+------+---------------+------+---------+------+------+-------------+| 1 | SIMPLE | employees | ALL | PRIMARY | NULL | NULL | NULL | 5 | Using where |+----+-------------+-----------+------+---------------+------+---------+------+------+-------------+ 解释:由于隐式类型转换,MySQL 进行了全表扫描(type=ALL),未能使用索引。 2. LIKE 通配符匹配后缀导致索引失效12345+----+-------------+-----------+------+---------------+------+---------+------+------+-------------+| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |+----+-------------+-----------+------+---------------+------+---------+------+------+-------------+| 1 | SIMPLE | employees | ALL | NULL | NULL | NULL | NULL | 5 | Using where |+----+-------------+-----------+------+---------------+------+---------+------+------+-------------+ 解释:由于通配符在前,MySQL 进行了全表扫描(type=ALL),未能使用索引。 3. OR 连接查询索引失效12345+----+-------------+-----------+------+--------------------+------+---------+------+------+-------------+| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |+----+-------------+-----------+------+--------------------+------+---------+------+------+-------------+| 1 | SIMPLE | employees | ALL | PRIMARY,idx_department_id | NULL | NULL | NULL | 5 | Using where |+----+-------------+-----------+------+--------------------+------+---------+------+------+-------------+ 解释:由于 OR 语句使得查询需要考虑多个条件,MySQL 进行了全表扫描(type=ALL),未能使用索引。 4. 正确使用索引的查询12345+----+-------------+-----------+-------+---------------+---------------+---------+------+------+-------------+| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |+----+-------------+-----------+-------+---------------+---------------+---------+------+------+-------------+| 1 | SIMPLE | employees | range | NULL | idx_name | 1024 | NULL | 2 | Using where |+----+-------------+-----------+-------+---------------+---------------+---------+------+------+-------------+ 解释:由于通配符在后,符合最左匹配原则,MySQL 使用了索引(type=range)。 通过以上查询和分析,可以更好地理解 B+树索引在不同情况下的使用和失效原因,从而优化数据库查询性能。","tags":[{"name":"MySQL","slug":"MySQL","permalink":"https://alexhuihui.github.io/tags/MySQL/"},{"name":"索引","slug":"索引","permalink":"https://alexhuihui.github.io/tags/%E7%B4%A2%E5%BC%95/"}]},{"title":"【Docker】容器的本质","date":"2024-04-30T01:57:57.000Z","path":"article/20240430.html","text":"一个正在运行的 Docker 容器,其实就是一个启用了多个 Linux Namespace 的应用进程,而这个进程能够使用的资源量,则受 Cgroups 配置的限制。 下面通过一个小实验证明这句话 验证在 Linux 中,Cgroups 给用户暴露出来的操作接口是文件系统,即它以文件和目录的方式组织在操作系统的 /sys/fs/cgroup 路径下。 1mount -t cgroup 进入到 /sys/fs/cgroup/cpu 目录下,并创建一个目录,操作系统会在新创建的 container 目录下,自动生成该子系统对应的资源限制文件。 1234root@ubuntu:/sys/fs/cgroup/cpu$ mkdir containerroot@ubuntu:/sys/fs/cgroup/cpu$ ls container/cgroup.clone_children cpu.cfs_period_us cpu.rt_period_us cpu.shares notify_on_releasecgroup.procs cpu.cfs_quota_us cpu.rt_runtime_us cpu.stat tasks 执行一个脚本,打满CPU 1while : ; do : ; done & 修改container目录中的CPU资源限制并把刚刚创建的进程加入到任务组中 123echo 20000 > /sys/fs/cgroup/cpu/container/cpu.cfs_quota_usecho PID > /sys/fs/cgroup/cpu/container/tasks 这时候再通过TOP去查看CPU使用就会发现它的使用率从100%降到了20% 缺点docker使用namespace进行隔离但是隔离的不彻底,它本质上还是一个跑在宿主机上的进程。 docker使用cgroups进行使用资源的限制,但是Cgroups 对资源的限制能力也有很多不完善的地方。","tags":[{"name":"Docker","slug":"Docker","permalink":"https://alexhuihui.github.io/tags/Docker/"}]},{"title":"【事务处理】分布式事务","date":"2024-01-27T03:35:29.000Z","path":"article/20240127.html","text":"可以按照服务的数量以及对应的数据源的使用数量来划分分布式事务,例如单个服务使用多数据源称作全局事务可以使用2、3段提交,多个服务使用单数据源则称作共享事务可以使用共享数据库连接,还有一种就是多服务多数据源,这种一般称作在分布式服务环境下的事务处理机制。 全局事务 准备阶段:又叫作投票阶段,在这一阶段,协调者询问事务的所有参与者是否准备好提交,参与者如果已经准备好提交则回复 Prepared,否则回复 Non-Prepared。这里所说的准备操作跟人类语言中通常理解的准备并不相同,对于数据库来说,准备操作是在重做日志中记录全部事务提交操作所要做的内容,它与本地事务中真正提交的区别只是暂不写入最后一条 Commit Record 而已,这意味着在做完数据持久化后并不立即释放隔离性,即仍继续持有锁,维持数据对其他非事务内观察者的隔离状态。 提交阶段:又叫作执行阶段,协调者如果在上一阶段收到所有事务参与者回复的 Prepared 消息,则先自己在本地持久化事务状态为 Commit,在此操作完成后向所有参与者发送 Commit 指令,所有参与者立即执行提交操作;否则,任意一个参与者回复了 Non-Prepared 消息,或任意一个参与者超时未回复,协调者将自己的事务状态持久化为 Abort 之后,向所有参与者发送 Abort 指令,参与者立即执行回滚操作。对于数据库来说,这个阶段的提交操作应是很轻量的,仅仅是持久化一条 Commit Record 而已,通常能够快速完成,只有收到 Abort 指令时,才需要根据回滚日志清理已提交的数据,这可能是相对重负载的操作。 缺点很明显,容易单点、性能问题、一致性风险 共享事务 理论可行,因为该方案是与实际生产系统中的压力方向相悖的,一个服务集群里数据库才是压力最大而又最不容易伸缩拓展的重灾区。 分布式事务","tags":[{"name":"事务","slug":"事务","permalink":"https://alexhuihui.github.io/tags/%E4%BA%8B%E5%8A%A1/"},{"name":"分布式事务","slug":"分布式事务","permalink":"https://alexhuihui.github.io/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/"}]},{"title":"【技术分享】Spring声明式事务的正确使用","date":"2024-01-18T02:49:54.000Z","path":"article/20240118.html","text":"大家肯定对@Transactional这个注解很熟悉,也对事务有着详细的了解,也知道多个数据库操作需要通过事务来保证一致性和原子性。但是很少会关注事务是否生效、有没有出错。这类问题也比较难在测试阶段发现,当出现线上问题的时候不可避免的会产生大量脏数据。所以这次我分享的内容就是帮助大家理清楚使用@Transactional的思路,避免使用不当产生bug。 事务为什么不生效很多同学以为只要加了这个注解就不需要管它了,其实不尽然。可以看看下述例子: 12345678910111213141516171819202122232425262728293031@Service@Slf4jpublic class UserService { @Autowired private UserRepository userRepository; //一个公共方法供Controller调用,内部调用事务性的私有方法 public int createUserWrong1(String name) { try { this.createUserPrivate(new UserEntity(name)); } catch (Exception ex) { log.error("create user failed because {}", ex.getMessage()); } return userRepository.findByName(name).size(); } //标记了@Transactional的private方法 @Transactional private void createUserPrivate(UserEntity entity) { userRepository.save(entity); if (entity.getName().contains("test")) { throw new RuntimeException("invalid username!"); } } //根据用户名查询用户数 public int getUserCount(String name) { return userRepository.findByName(name).size(); }} 只有定义在public方法上的 @Transactional 才能生效 1234567891011121314151617public int createUserWrong2(String name) { try { this.createUserPublic(new UserEntity(name)); } catch (Exception ex) { log.error("create user failed because {}", ex.getMessage()); } return userRepository.findByName(name).size();}//标记了@Transactional的public方法@Transactionalpublic void createUserPublic(UserEntity entity) { userRepository.save(entity); if (entity.getName().contains("test")) { throw new RuntimeException("invalid username!"); }} 必须通过代理过的类从外部调用目标方法才能生效 1234567891011121314151617181920212223242526@GetMapping("right2")public int right2(@RequestParam("name") String name) { try { userService.createUserPublic(new UserEntity(name)); } catch (Exception ex) { log.error("create user failed because {}", ex.getMessage()); } return userService.getUserCount(name);}try { // This is an around advice: Invoke the next interceptor in the chain. // This will normally result in a target object being invoked. retVal = invocation.proceedWithInvocation();}catch (Throwable ex) { // target invocation exception completeTransactionAfterThrowing(txInfo, ex); throw ex;}finally { cleanupTransactionInfo(txInfo);} 只有异常传播出了标记了 @Transactional 注解的方法,事务才能回滚 1234567891011121314151617181920/** * The default behavior is as with EJB: rollback on unchecked exception * ({@link RuntimeException}), assuming an unexpected outcome outside of any * business rules. Additionally, we also attempt to rollback on {@link Error} which * is clearly an unexpected outcome as well. By contrast, a checked exception is * considered a business exception and therefore a regular expected outcome of the * transactional business method, i.e. a kind of alternative return value which * still allows for regular completion of resource operations. * <p>This is largely consistent with TransactionTemplate's default behavior, * except that TransactionTemplate also rolls back on undeclared checked exceptions * (a corner case). For declarative transactions, we expect checked exceptions to be * intentionally declared as business exceptions, leading to a commit by default. * @see org.springframework.transaction.support.TransactionTemplate#execute */@Overridepublic boolean rollbackOn(Throwable ex) { return (ex instanceof RuntimeException || ex instanceof Error);} 默认出现 RuntimeException(非受检异常)或 Error 的时候,Spring 才会回滚事务(可以指定回滚异常) 事务传播有现在这个场景,注册主会员和注册子用户,要求注册子用户失败不影响主会员的注册,下述伪代码,可以猜一下结果 123456789101112131415@Autowiredprivate UserRepository userRepository;@Autowiredprivate SubUserService subUserService;@Transactionalpublic void createUserWrong(UserEntity entity) { createMainUser(entity); subUserService.createSubUserWithExceptionWrong(entity);}private void createMainUser(UserEntity entity) { userRepository.save(entity); log.info("createMainUser finish");} 上述代码异常跑出了@Transactional 注解标记的 createUserWrong 方法,Spring 会回滚事务 修改之后如下: 123456789@Transactionalpublic void createUserWrong2(UserEntity entity) { createMainUser(entity); try { subUserService.createSubUserWithExceptionWrong(entity); } catch (Exception ex) { log.error("create sub user error:{}", ex.getMessage()); }} 虽然捕获了异常,但是因为没有开启新事务,而当前事务因为异常已经被标记为rollback了,所以最终还是会回滚。 看到这里就清楚了,只能让处理子用户的逻辑运行在单独的事务中,这就用到了SPRING的事务传播机制 1234567891011121314151617@Transactional(propagation = Propagation.REQUIRES_NEW)public void createSubUserWithExceptionRight(UserEntity entity) { log.info("createSubUserWithExceptionRight start"); userRepository.save(entity); throw new RuntimeException("invalid status");}@Transactionalpublic void createUserRight(UserEntity entity) { createMainUser(entity); try { subUserService.createSubUserWithExceptionRight(entity); } catch (Exception ex) { // 捕获异常,防止主方法回滚 log.error("create sub user error:{}", ex.getMessage()); }} 希望这次分享能帮助大家正确处理业务代码中的事务。 实际使用踩的坑多数据源中不能使用事务事务和分布式锁使用的坑现象:业务中加了分布式锁,也做了幂等处理如果业务ID存在相同的数据则直接返回。但还是会生成2条一样的数据。 12345678910111213141516171819202122@Around("@annotation(redisLock)")public Object around(ProceedingJoinPoint joinPoint, RedisLock redisLock) throws Throwable { String spel = redisLock.key(); String lockName = redisLock.lockName(); String redisLockKey = getRedisKey(joinPoint, lockName, spel); log.info("生成的 redisKey 是 -> {}", redisLockKey); RLock rLock = redissonClient.getLock(redisLockKey); Object result; try { rLock.lock(redisLock.expire(), redisLock.timeUnit()); //执行方法 result = joinPoint.proceed(); } catch (InterruptedException interruptedException) { log.error("获取分布式锁失败, ", interruptedException); throw new RuntimeException("获取分布式锁失败"); } finally { if (rLock.isLocked()) { rLock.unlock(); } } return result;} 1234567891011121314@Transactional(rollbackFor = RuntimeException.class)@RedisLock(lockName = "test", key = "test")public void test() { // 根据手机号查询用户 User user = bizService.queryByPhone("17770848782"); log.info("user==={}", user);}@Transactional(rollbackFor = RuntimeException.class)public void test2() { // 根据手机号查询用户 User user = bizService.queryByPhone("17770848782"); log.info("user2==={}", user);} 1234567891011121314@GetMapping@Operation(summary = "test2")@RedisLock(lockName = "test2", key = "test2")public ObjectResponse<String> test2() { userAppService.test2(); return ObjectResponse.success();}@PostMapping@Operation(summary = "test")public ObjectResponse<String> test() { userAppService.test(); return ObjectResponse.success();} 结论:当使用分布式锁和事务时,因为切面处理是用的先进后出的栈,又因为事务处理的优先级是默认最低的,所以如果没有知道分布式锁的优先级就会导致锁先于事务释放,导致可能出现事务的隔离性被破坏,产生脏数据。","tags":[{"name":"事务","slug":"事务","permalink":"https://alexhuihui.github.io/tags/%E4%BA%8B%E5%8A%A1/"}]},{"title":"【ELK】springboot结合elk搭建日志平台","date":"2023-12-06T06:57:51.000Z","path":"article/20231206.html","text":"本文介绍如果通过搭建ELK收集springboot的日志。 部署ELK通过docker compose安装 安装docker compose 12yum install -y yum-utils device-mapper-persistent-data lvm2yum install docker-compose 创建以下目录以及对应的配置文件 12345678910111213141516.├── docker-compose.yml├── elasticsearch│ ├── config│ │ └── elasticsearch.yml│ ├── data│ └── logs├── kibana│ └── config│ └── kibana.yml└── logstash ├── config │ ├── logstash.yml │ └── small-tools │ └── dev-enlightrn-hub.config └── data 1234567891011# elasticsearch.ymlcluster.name: "docker-cluster"network.host: 0.0.0.0http.port: 9200# 开启es跨域http.cors.enabled: truehttp.cors.allow-origin: "*"http.cors.allow-headers: Authorization# # 开启安全控制xpack.security.enabled: truexpack.security.transport.ssl.enabled: true 123456789# kibana.ymlserver.name: kibanaserver.host: "0.0.0.0"server.publicBaseUrl: "http://kibana:5601"elasticsearch.hosts: [ "http://elasticsearch:9200" ] xpack.monitoring.ui.container.elasticsearch.enabled: trueelasticsearch.username: "elastic"elasticsearch.password: "123456"i18n.locale: zh-CN 123456# logstash.ymlhttp.host: "0.0.0.0"xpack.monitoring.enabled: truexpack.monitoring.elasticsearch.hosts: [ "http://elasticsearch:9200" ]xpack.monitoring.elasticsearch.username: "elastic"xpack.monitoring.elasticsearch.password: "123456" 1234567891011121314151617181920212223242526272829303132333435363738394041424344# dev-enlightrn-hub.configinput { #输入 tcp { mode => "server" host => "0.0.0.0" # 允许任意主机发送日志 type => "dev-enlighten-hub" # 设定type以区分每个输入源 port => 9999 codec => json_lines # 数据格式 } } filter { mutate { # 导入之过滤字段 remove_field => ["LOG_MAX_HISTORY_DAY", "LOG_HOME", "APP_NAME"] remove_field => ["@version", "_score", "port", "level_value", "tags", "_type", "host"] }} output { #输出-控制台 stdout{ codec => rubydebug }} output { #输出-es if [type] == "dev-enlighten-hub" { elasticsearch { action => "index" # 输出时创建映射 hosts => "http://elasticsearch:9200" # ES地址和端口 user => "elastic" # ES用户名 password => "123456" # ES密码 index => "dev-enlighten-hub-%{+YYYY.MM.dd}" # 指定索引名-按天 codec => "json" } } } 编写docker-compose.yml 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859version: '3.3'networks: elk: driver: bridgeservices: elasticsearch: image: registry.cn-hangzhou.aliyuncs.com/zhengqing/elasticsearch:7.14.1 container_name: elk_elasticsearch restart: unless-stopped volumes: - "./elasticsearch/data:/usr/share/elasticsearch/data" - "./elasticsearch/logs:/usr/share/elasticsearch/logs" - "./elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml" environment: TZ: Asia/Shanghai LANG: en_US.UTF-8 TAKE_FILE_OWNERSHIP: "true" # 权限 discovery.type: single-node ES_JAVA_OPTS: "-Xmx1g -Xms1g" ELASTIC_PASSWORD: "111111" # elastic账号密码 ports: - "9200:9200" - "9300:9300" networks: - elk kibana: image: registry.cn-hangzhou.aliyuncs.com/zhengqing/kibana:7.14.1 container_name: elk_kibana restart: unless-stopped volumes: - "./kibana/config/kibana.yml:/usr/share/kibana/config/kibana.yml" ports: - "5601:5601" depends_on: - elasticsearch links: - elasticsearch networks: - elk logstash: image: registry.cn-hangzhou.aliyuncs.com/zhengqing/logstash:7.14.1 container_name: elk_logstash restart: unless-stopped environment: LS_JAVA_OPTS: "-Xmx1g -Xms1g" volumes: - "./logstash/data:/usr/share/logstash/data" - "./logstash/config/logstash.yml:/usr/share/logstash/config/logstash.yml" - "./logstash/config/small-tools:/usr/share/logstash/config/small-tools" command: logstash -f /usr/share/logstash/config/small-tools ports: - "9600:9600" - "9999:9999" depends_on: - elasticsearch networks: - elk 执行启动命令 1docker compose up -d 如果启动失败可以通过下述命令进行检查配置项 1docker-compose run --rm logstash logstash -t -f /usr/share/logstash/config/small-tools 修改spring boot项目 添加maven依赖 12345<dependency> <groupId>net.logstash.logback</groupId> <artifactId>logstash-logback-encoder</artifactId> <version>7.3</version></dependency> 添加logback-spring.xml 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283<?xml version="1.0" encoding="UTF-8"?><configuration scan="true" scanPeriod="60 seconds" debug="false"> <!-- 日志存放路径 --> <property name="log.path" value="${user.dir}/logs"/> <!-- 日志输出格式 --> <property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n"/> <!-- 控制台输出 --> <appender name="console" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>${log.pattern}</pattern> </encoder> </appender> <!-- 系统日志输出 --> <appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${log.path}/info.log</file> <!-- 循环政策:基于时间创建日志文件 --> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!-- 日志文件名格式 --> <fileNamePattern>${log.path}/info.%d{yyyy-MM-dd}.log</fileNamePattern> <!-- 日志最大的历史 7天 --> <maxHistory>7</maxHistory> </rollingPolicy> <encoder> <pattern>${log.pattern}</pattern> </encoder> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <!-- 过滤的级别 --> <level>INFO</level> <!-- 匹配时的操作:接收(记录) --> <onMatch>ACCEPT</onMatch> <!-- 不匹配时的操作:拒绝(不记录) --> <onMismatch>DENY</onMismatch> </filter> </appender> <appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${log.path}/error.log</file> <!-- 循环政策:基于时间创建日志文件 --> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!-- 日志文件名格式 --> <fileNamePattern>${log.path}/error.%d{yyyy-MM-dd}.log</fileNamePattern> <!-- 日志最大的历史 60天 --> <maxHistory>60</maxHistory> </rollingPolicy> <encoder> <pattern>${log.pattern}</pattern> </encoder> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <!-- 过滤的级别 --> <level>ERROR</level> <!-- 匹配时的操作:接收(记录) --> <onMatch>ACCEPT</onMatch> <!-- 不匹配时的操作:拒绝(不记录) --> <onMismatch>DENY</onMismatch> </filter> </appender> <!-- 将日志文件输出到Logstash --> <appender name="logstash" class="net.logstash.logback.appender.LogstashTcpSocketAppender"> <!-- 获取logstash地址作为输出的目的地 --> <destination>172.22.0.4:9999</destination> <encoder chatset="UTF-8" class="net.logstash.logback.encoder.LogstashEncoder"/> </appender> <!-- 系统模块日志级别控制 --> <logger name="com.gyhappylife" level="info"/> <!-- Spring日志级别控制 --> <logger name="org.springframework" level="warn"/> <root level="info"> <appender-ref ref="console"/> </root> <!--系统操作日志--> <root level="info"> <appender-ref ref="file_info"/> <appender-ref ref="file_error"/> <appender-ref ref="logstash"/> </root></configuration> 注意事项如果spring应用也是跑在docker中的话 那么它的网段可能会和ELK不一样 。这样就必须在iptables中添加路由规则例如: 12$ iptables -I DOCKER-USER -i br-3bed419583c5 -o br-787e52a81bb4 -j ACCEPT$ iptables -I DOCKER-USER -i br-787e52a81bb4 -o br-3bed419583c5 -j ACCEPT","tags":[{"name":"ElasticSearch","slug":"ElasticSearch","permalink":"https://alexhuihui.github.io/tags/ElasticSearch/"},{"name":"elk","slug":"elk","permalink":"https://alexhuihui.github.io/tags/elk/"},{"name":"Logstash","slug":"Logstash","permalink":"https://alexhuihui.github.io/tags/Logstash/"},{"name":"kibana","slug":"kibana","permalink":"https://alexhuihui.github.io/tags/kibana/"}]},{"title":"【RocketMq】部署&运维","date":"2023-11-06T08:45:02.000Z","path":"article/20231106.html","text":"","tags":[{"name":"RocketMq","slug":"RocketMq","permalink":"https://alexhuihui.github.io/tags/RocketMq/"}]},{"title":"【技术分享】优化延迟队列任务消费时间的技术分享","date":"2023-10-19T07:26:44.000Z","path":"article/20231019.html","text":"标题:优化延迟队列任务消费时间的技术分享 作者:汪永晖 日期:2023-10-19 延迟队列在实际应用中是一项重要的技术,可以用于任务调度、定时提醒、消息重试等多种场景。然而,延迟队列的任务有时会在过期时间之后才得以执行,这可能导致应用程序的性能问题和不确定性。在本技术分享中,我将介绍一个优化延迟队列任务消费时间的解决方案,以及如何实现这一解决方案。 延迟队列实现原理 在我们开始讨论如何优化延迟队列的任务消费时间之前,让我们首先了解一下延迟队列的实现原理。这个延迟队列基于Redis和Redisson库实现,它分为三个主要阶段:启动、任务添加和任务消费。 第一阶段:启动 订阅队列:在这个阶段,我们订阅了一个名为”redisson_delay_queue_channel:{dest_queue1}”的固定队列,用于接收任务到期的通知。 获取过期任务:使用zrangebyscore命令,我们获取了过了当前时间的100条任务数据,以确保没有漏掉上次宕机后未执行的任务。 判断下一页:通过zrange命令,我们检查是否还有下一页的任务需要处理。 移除任务:使用BLPOP命令,我们移除并获取了队列”dest_queue1”中的第一个任务,如果队列为空,该操作会一直阻塞。 第二阶段:添加延时任务 添加任务:使用zadd命令,我们将任务添加到”redisson_delay_queue_timeout:{dest_queue1}”。 同步数据:将任务数据同步到”redisson_delay_queue:{dest_queue1}”。 获取即将触发的任务:通过zrange命令,我们获取了即将触发的任务。 发布通知:使用publish命令,我们向”redisson_delay_queue_channel:{dest_queue1}”发布了任务到期的消息,客户端收到通知后会在自己的进程中执行延时任务。 第三阶段:任务消费 获取到期任务:使用zrangebyscore命令,我们获取了”redisson_delay_queue_timeout:{dest_queue1}”中已到期的任务。 将任务放入队列:通过rpush命令,我们将这些任务放入第一阶段监听的阻塞队列”dest_queue1”。 客户端消费任务:客户端获取这些数据并进行处理。 删除任务:使用lrem和zrem命令,我们删除已经处理过的任务。 检查下一个任务:通过zrange命令,我们检查是否有下一个任务需要处理。 继续监听:使用BLPOP命令,我们继续监听”dest_queue1”这个阻塞队列。 问题分析 上述实现中,客户端订阅了一个关键字(key),并且会在类初始化中执行该订阅操作。然而,在测试环境中,服务频繁升级和重启,只有在调用延迟队列客户端的静态方法时才会执行类初始化。因此,当服务重启后未再次订阅相应的key,导致无法从Redis获取任务数据,延迟队列的任务消费时间被推迟。 优化延迟队列任务消费时间的解决方案 为了优化延迟队列任务的消费时间,我们采用了Redisson库来实现延迟队列。下面是解决方案的主要步骤: 1. 定义接口 首先,我们定义了一个名为Queue的接口,用于表示延迟队列。 2. 实现抽象类 我们实现了一个名为AbstractQueue的抽象类,该类包含了实现延迟队列所需的核心逻辑。以下是该抽象类的关键部分: 使用Redisson客户端:通过Spring配置,我们初始化了Redisson客户端,以便与Redis服务器通信。 定义监听器容器:我们创建了一个名为listenerMap的容器,用于存储与topic相关的队列监听器。 启动监听线程:在构造函数中,我们开启了一个独立的线程来监听队列的消息。这确保了任务能够及时被处理。 实现消费逻辑:我们定义了一个consumer方法,用于在监听到消息时处理队列中的任务。该方法首先获取与消息对应的队列监听器,然后执行任务的消费逻辑。 3. 子类实现 我们创建了一个名为DelayQueue的子类,它继承了AbstractQueue并实现了其中的抽象方法。以下是该子类的关键部分: 创建延迟队列:我们使用Redisson客户端创建了RBlockingDeque和RDelayedQueue,这是延迟队列的基本数据结构。 添加任务:通过offer方法,我们可以向RDelayedQueue中添加延时任务。 消费任务:在take方法中,我们从RBlockingDeque中获取任务数据,然后开启线程执行消费逻辑。 使用示例 最后,我们提供了一个DelayClient类,它用于向延迟队列中添加任务。通过调用offer方法,用户可以方便地将任务加入延迟队列,无需担心类初始化问题。 实现延迟队列的设计具有以下优点: 实时性:采用Redis和Redisson库实现的延迟队列无需轮询,任务可以在到期时立即被处理,提供了更好的实时性。 可扩展性:延迟队列的设计支持应用程序的水平扩展。可以轻松地添加更多的消费者来处理大量的延迟任务,而无需担心性能问题。 容错性:即使在服务升级或重启时,延迟队列仍能够正常工作,不会丢失任何任务。 灵活性:使用抽象类和接口的设计,可以根据自己的需求扩展和定制延迟队列的功能。可以添加不同类型的队列监听器,实现各种任务消费逻辑。 结语 通过使用Redisson和合理的设计,我们成功优化了延迟队列的任务消费时间,提高了系统的性能和稳定性。这种技术对于需要处理延迟任务的应用程序非常有用,如定时任务、消息通知和消息重试等场景。 在实际项目中,可以根据自己的需求进一步扩展和优化这一解决方案,以适应不同的应用场景。同时,还可以考虑添加监控和日志记录,以便更好地跟踪和调试延迟队列中的任务。通过合适的工具和设计,延迟队列可以成为提高应用程序效率和可靠性的重要组成部分。","tags":[{"name":"延迟队列","slug":"延迟队列","permalink":"https://alexhuihui.github.io/tags/%E5%BB%B6%E8%BF%9F%E9%98%9F%E5%88%97/"}]},{"title":"【Kafka】Kafka的基本使用","date":"2023-09-15T03:19:21.000Z","path":"article/20230915.html","text":"Kafka 的基本使用消息引擎系统介绍 Apache Kafka是一款开源的消息引擎系统。根据维基百科的定义,消息引擎系统是一组规范。企业利用这组规范在不同系统之间传递语义准确的消息,实现松耦合的异步式数据传递。通俗来讲,就是系统A发送消息到消息引擎系统,系统B从消息引擎系统中读取A发送的消息。 消息引擎系统要设定具体的传输协议,常见的有2种:点对点模型;发布订阅模型。Kafka同事支持这两种。 主要作用是削峰填谷,避免下游系统因突发流量而崩溃。 术语简介 消息:Record。Kafka 是消息引擎嘛,这里的消息就是指 Kafka 处理的主要对象。 主题:Topic。主题是承载消息的逻辑容器,在实际使用中多用来区分具体的业务。 分区:Partition。一个有序不变的消息序列。每个主题下可以有多个分区。 消息位移:Offset。表示分区中每条消息的位置信息,是一个单调递增且不变的值。 副本:Replica。Kafka 中同一条消息能够被拷贝到多个地方以提供数据冗余,这些地方就是所谓的副本。副本还分为领导者副本和追随者副本,各自有不同的角色划分。副本是在分区层级下的,即每个分区可配置多个副本实现高可用。 生产者:Producer。向主题发布新消息的应用程序。 消费者:Consumer。从主题订阅新消息的应用程序。 消费者位移:Consumer Offset。表征消费者消费进度,每个消费者都有自己的消费者位移。 消费者组:Consumer Group。多个消费者实例共同组成的一个组,同时消费多个分区以实现高吞吐。 重平衡:Rebalance。消费者组内某个消费者实例挂掉后,其他消费者实例自动重新分配订阅主题分区的过程。Rebalance 是 Kafka 消费者端实现高可用的重要手段。","tags":[{"name":"Kafka","slug":"Kafka","permalink":"https://alexhuihui.github.io/tags/Kafka/"}]},{"title":"【DNS】DNS的架构","date":"2023-07-06T01:51:22.000Z","path":"article/20230706.html","text":"","tags":[{"name":"DNS","slug":"DNS","permalink":"https://alexhuihui.github.io/tags/DNS/"},{"name":"系统设计","slug":"系统设计","permalink":"https://alexhuihui.github.io/tags/%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1/"}]},{"title":"【并发】Java和Go的并发编程对比","date":"2023-06-16T06:10:03.000Z","path":"article/20230616.html","text":"Go使用的是CSP并发模型,而JAVA使用的是基于传统的内存访问控制的并发模型。他们有以下区别。我们通过示例代码来比对一下Go和 Java的并发编程 “Communicating Sequential Processes” (CSP) 是一种并发计算的数学模型,最早由计算机科学家Tony Hoare于1978年提出。CSP 的主要目标是描述并发系统中进程之间的交互和通信方式,而不涉及共享内存的概念。与传统的内存访问控制有很大的区别,下面是一些主要的区别: 通信方式: CSP: 使用进程之间的明确通信来实现协同工作。进程通过发送和接收消息进行通信,但它们并不共享内存空间。 传统内存访问控制: 多个进程可能在同一块内存中进行读写操作,通过共享内存来实现通信。这可能导致诸如竞态条件和死锁等问题。 并发模型: CSP: 采用事件驱动的方式,进程之间通过消息传递进行通信,以实现并发。并发在这里是通过协作和通信而非共享状态来实现的。 传统内存访问控制: 并发通常是通过多个进程或线程共享同一块内存来实现的。这可能引入一系列并发控制问题,如锁和同步。 数据共享: CSP: 鼓励避免共享数据,而是通过消息传递来传递必要的信息。这样设计有助于减少竞态条件和提高系统的可靠性。 传统内存访问控制: 通常涉及多个进程或线程共享相同的内存区域,需要使用锁或其他同步机制来确保数据一致性。 同步和互斥: CSP: 使用通信机制来进行同步,进程之间通过消息传递协调各自的动作。 传统内存访问控制: 常常需要使用锁或信号量等机制来进行同步和互斥,以防止多个进程同时访问共享的内存区域。 总的来说,CSP 提供了一种不同于传统共享内存的并发模型,强调通过明确的通信来实现进程之间的协同工作,以减少并发问题的出现。这种方式更容易推理和调试,并且有助于构建可靠的并发系统。 Go123456789101112131415161718192021222324252627282930313233343536373839404142package mainimport ( "fmt" "sync")func main() { type Button struct { Clicked *sync.Cond } button := Button{Clicked: sync.NewCond(&sync.Mutex{})} subscribe := func(c *sync.Cond, fn func()) { var goroutineRunning sync.WaitGroup goroutineRunning.Add(1) go func() { defer c.L.Unlock() goroutineRunning.Done() c.L.Lock() c.Wait() fn() }() goroutineRunning.Wait() } var clickRegistered sync.WaitGroup clickRegistered.Add(3) subscribe(button.Clicked, func() { fmt.Println("Maximizing window.") clickRegistered.Done() }) subscribe(button.Clicked, func() { fmt.Println("Displaying annoying dialog box!") clickRegistered.Done() }) subscribe(button.Clicked, func() { fmt.Println("Mouse clicked.") clickRegistered.Done() }) button.Clicked.Broadcast() clickRegistered.Wait()} 这段代码演示了使用条件变量(sync.Cond)实现订阅和发布模式的示例。 首先,代码定义了一个名为Button的结构体,其中包含一个指向条件变量的指针Clicked。通过sync.NewCond函数,我们创建了一个与互斥锁关联的条件变量。 接下来,代码定义了一个subscribe函数,用于订阅事件。该函数接收一个条件变量c和一个回调函数fn作为参数。在函数内部,它创建了一个sync.WaitGroup类型的变量goroutineRunning,用于等待goroutine的启动。然后,它启动一个新的goroutine,在其中等待条件变量的信号。一旦接收到信号,它会执行回调函数fn。在等待和执行过程中,它使用互斥锁来保护共享资源。最后,通过goroutineRunning.Wait()确保goroutine已经启动。这样做是为了避免出现竞争条件,确保回调函数在订阅完成之后才会执行。 在main函数中,我们创建了一个Button实例button,并调用subscribe函数三次,每次传递不同的回调函数。这些回调函数分别打印不同的消息。 随后,我们创建了一个sync.WaitGroup类型的变量clickRegistered,用于等待所有订阅的回调函数执行完成。通过clickRegistered.Add(3)将等待计数设置为3,因为我们有三个订阅的回调函数。 最后,我们调用button.Clicked.Broadcast()发送广播信号,通知所有订阅者事件已发生。这将触发所有等待中的subscribe函数中的条件变量的信号,并执行对应的回调函数。 通过使用条件变量和互斥锁,代码实现了一个简单的订阅和发布模式。当事件发生时,订阅者收到信号并执行相应的回调函数。这种模式可以在并发环境中实现解耦和事件驱动的编程 Java1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;public class Main { public static void main(String[] args) { Button button = new Button(); Thread thread1 = new Thread(() -> { button.subscribe(() -> { System.out.println("Maximizing window."); }); }); Thread thread2 = new Thread(() -> { button.subscribe(() -> { System.out.println("Displaying annoying dialog box!"); }); }); Thread thread3 = new Thread(() -> { button.subscribe(() -> { System.out.println("Mouse clicked."); }); }); thread1.start(); thread2.start(); thread3.start(); // 模拟按钮点击事件 button.click(); try { thread1.join(); thread2.join(); thread3.join(); } catch (InterruptedException e) { e.printStackTrace(); } }}class Button { private Lock lock; private Condition condition; public Button() { lock = new ReentrantLock(); condition = lock.newCondition(); } public void click() { lock.lock(); try { condition.signalAll(); } finally { lock.unlock(); } } public void subscribe(Runnable callback) { Thread thread = new Thread(() -> { lock.lock(); try { condition.await(); callback.run(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } }); thread.start(); }} 在上述代码中,使用了Condition、Lock和ReentrantLock来实现线程间的同步和通信。 Lock接口是Java提供的用于多线程同步的机制之一。ReentrantLock是Lock接口的一个具体实现类,它提供了独占锁的功能。在示例代码中,我们创建了一个ReentrantLock实例,用于保护共享资源的访问。 Condition接口是与锁关联的条件,可以用于实现线程间的等待和通知机制。在示例代码中,我们使用lock.newCondition()创建了一个Condition实例,用于实现订阅者线程的等待和主线程的通知。 lock.lock()和lock.unlock()用于获取和释放锁。通过使用lock.lock()获取锁,可以确保只有一个线程可以执行被保护的代码块,其他线程将被阻塞。一旦线程完成了对共享资源的操作,使用lock.unlock()释放锁,以便其他线程可以获取锁并执行。 condition.await()用于使当前线程进入等待状态,直到其他线程通过调用condition.signal()或condition.signalAll()发出信号。在示例代码中,订阅者线程在收到信号前会调用condition.await()进入等待状态。 condition.signalAll()用于唤醒所有等待在该条件上的线程。在示例代码中,主线程调用button.click()后会调用condition.signalAll(),以通知所有等待的订阅者线程。 通过使用Condition、Lock和ReentrantLock,我们可以实现更精细的线程同步和通信。它们提供了更灵活的控制机制,使得线程之间的交互更加可控和高效。 可以使用Object类中的wait()和notifyAll()方法来实现线程间的等待和通知机制,用于替代Condition接口和Lock机制。 在使用wait()和notifyAll()时,需要注意以下几点: wait()方法用于使当前线程进入等待状态,直到其他线程调用相同对象的notify()或notifyAll()方法。在等待期间,当前线程会释放对象的锁。 notifyAll()方法用于唤醒所有等待在相同对象上的线程。它会通知所有等待的线程继续执行,但只有在获取到对象的锁之后才能真正执行。 在使用wait()和notifyAll()时,必须在同步代码块或同步方法中调用,以确保对对象的锁的正确使用。 通常,你需要使用一个共享的对象作为通信的锁,类似于示例代码中的button对象。 下面是修改后的示例代码,使用wait()和notifyAll()实现线程间的等待和通知: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556class Button { private final Object lock = new Object(); public void click() { synchronized (lock) { lock.notifyAll(); } } public void subscribe(Runnable task) { synchronized (lock) { try { lock.wait(); task.run(); } catch (InterruptedException e) { e.printStackTrace(); } } }}public class Main { public static void main(String[] args) { Button button = new Button(); Thread subscriber1 = new Thread(() -> { button.subscribe(() -> { System.out.println("Maximizing window."); }); }); Thread subscriber2 = new Thread(() -> { button.subscribe(() -> { System.out.println("Displaying annoying dialog box!"); }); }); Thread subscriber3 = new Thread(() -> { button.subscribe(() -> { System.out.println("Mouse clicked."); }); }); subscriber1.start(); subscriber2.start(); subscriber3.start(); // 主线程等待订阅者线程完成 try { subscriber1.join(); subscriber2.join(); subscriber3.join(); } catch (InterruptedException e) { e.printStackTrace(); } button.click(); }} 在这个示例中,Button类使用了一个共享的锁对象lock。在订阅者线程中,调用lock.wait()使线程进入等待状态,在主线程中调用lock.notifyAll()唤醒所有等待的线程。主线程使用join()方法等待订阅者线程完成后再调用button.click()。 使用wait()和notifyAll()可以实现基本的线程间等待和通知机制,但相比Condition和Lock,它们的使用更加基础和低级,需要手动处理锁的获取和释放,并且可能存在更多的风险,如死锁和竞态条件。因此,在实际开发中,建议使用Condition和Lock机制,因为它们提供了更灵活、更可靠的线程同步和通信方式。相比于使用wait()和notifyAll(),Condition和Lock具有以下优势: 精确的通知机制:Condition接口提供了更细粒度的通知机制,可以选择性地通知等待线程。你可以创建多个Condition实例来控制不同的等待条件,并使用signal()或signalAll()方法通知特定的等待线程。 更灵活的锁控制:Lock接口提供了更灵活的锁控制机制。它支持可重入锁(ReentrantLock)和读写锁(ReentrantReadWriteLock),以及各种锁的高级功能,如公平性、超时等待和中断响应。 更安全的并发控制:Condition和Lock提供了更安全的并发控制机制,避免了可能导致死锁、竞态条件和线程饥饿等问题。它们通过显示地获取和释放锁来确保线程的正确同步和协调。 可扩展性和性能优化:Condition和Lock机制提供了更高级的线程同步功能,可以更好地满足复杂的并发需求。它们支持更多的高级操作,如条件等待、多个等待队列、可中断的等待等,并提供了更好的性能优化选项。 综上所述,尽管可以使用wait()和notifyAll()来实现简单的线程同步和通信,但在更复杂的并发场景下,使用Condition和Lock会更加可靠和灵活。它们提供了更多的功能和性能优化选项,可以更好地管理线程的状态、等待和唤醒,确保线程间的正确同步和协作。","tags":[{"name":"并发","slug":"并发","permalink":"https://alexhuihui.github.io/tags/%E5%B9%B6%E5%8F%91/"}]},{"title":"【重构】改善既有代码设计的阅读笔记","date":"2023-06-13T02:55:06.000Z","path":"article/20230613.html","text":"所谓重构(refactoring)是这样一个过程:在不改变代码外在行为的前提下,对代码做出修改,以改进程序的内部结构。重构是一种经千锤百炼形成的有条不紊的程序整理方法,可以最大限度地减少整理过程中引入错误的几率。这本书告诉你如何以一种可控制且高效率的方式进行重构,如何有条不紊地改进程序结构,而且不会引入错误。 重构第一个案例如果你发现自己需要为程序添加一个特性,而代码结构使你无法很方便地达成目的,那就先重构那个程序,使特性的添加比较容易进行,然后再添加特性。 第一步构建完整的测试用例,保证重构之后的代码行为表现与之前无异。 Extract Method 修改变量命名 Move Method Replace Temp with Query Replace Conditional with Polymorphism 用多态取代条件表达式 Self Encapsulate Field 自封装字段(字段访问保护) Replace Type Code with State/Strategy 用于改善基于类型码(Type Code)的条件逻辑,并通过引入状态模式(State Pattern)或策略模式(Strategy Pattern)来实现更清晰和可扩展的代码结构 重构原则什么是重构对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本 两顶帽子,当你使用重构技术进行开发时,把时间分配到两种不同的行为:添加新功能以及重构。添加新功能时,你不应该修改既有代码,只管添加新功能。 重构时你就不应该添加新功能而是只管改变程序结构。两顶帽子要随时切换,应该清除意识到你戴的是什么帽子。 为何重构重构改进软件设计 重构使软件更容易理解 何时重构事不过三,三则重构 代码的坏味道Duplicated Code Long Method(过长函数) Large Class(过大的类) Long Parameter List(过长参数列) Divergent Change(发散式变化) Shotgun Surgery(霰弹式修改) Feature Envy(依恋情结) 重构列表","tags":[{"name":"重构","slug":"重构","permalink":"https://alexhuihui.github.io/tags/%E9%87%8D%E6%9E%84/"}]},{"title":"【Mybatis】Mybatis源码解析—基础功能包","date":"2023-06-08T05:35:48.000Z","path":"article/20230608.html","text":"阅读mybatis源码可以先从外围的基础功能包开始,剥洋葱一样一层一层深入 type 包归类总结 类型处理器 类型注册器 注解类 设计模式 模板模式 模板中定义了大体的处理框架,留下一些细节供使用者来完善。在设计模式中,使用一个抽象类定义一整套的操作流程,而抽象类的子类则完成每个操作步骤的实现。 类型处理器用于ORM框架中处理Java类型和数据库类型,使用模板方式定义了一个BaseTypeHandler,getResult方法完成了异常处理等统一的工作,而与具体类型的操作则通过抽象方法由具体的子类类型处理器实现。 类型注册表光有类型处理器还不够,还需要快速查找数据类型对应的类型处理器 根据一个Java类型和JDBC类型就能确定一个类型处理器 1234567891011121314151617181920// TypeHandlerRegistry.classprivate <T> TypeHandler<T> getTypeHandler(Type type, JdbcType jdbcType) { if (ParamMap.class.equals(type)) { return null; } Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = getJdbcHandlerMap(type); TypeHandler<?> handler = null; if (jdbcHandlerMap != null) { handler = jdbcHandlerMap.get(jdbcType); if (handler == null) { handler = jdbcHandlerMap.get(null); } if (handler == null) { // #591 handler = pickSoleHandler(jdbcHandlerMap); } } // type drives generics here return (TypeHandler<T>) handler; } io 包io包即输入/输出包,负责完成 MyBatis中与输入/输出相关的操作 设计模式单例模式单例模式(Singleton Pattern)是一种非常简单的设计模式。使 用了单例模式的类提供一个方法得到该类的对象,并且总保证这个对 象是唯一的。 123456789public class Singleton { private static final Singleton INSTANCE = new Singleton(); private Singleton() {}; public static Singleton getInstance() { return INSTANCE; }} 代理模式代理模式(Proxy Pattern)是指建立某一个对象的代理对象,并且由代理对象控制对原对象的引用。 logging 包logging包负责完成 MyBatis操作中的日志记录工作。 设计模式适配器模式适配器模式(Adapter Pattern)是一种结构型模式,基于该模式 设计的类能够在两个或者多个不兼容的类之间起到沟通桥梁的作用。 转换插头就是一个适配器的典型例子。不同的转换插头能够适配 不同国家的插座标准,从而使得一个电器能在各个国家使用。","tags":[{"name":"Mybatis","slug":"Mybatis","permalink":"https://alexhuihui.github.io/tags/Mybatis/"}]},{"title":"【Mybatis】Mybatis运行流程","date":"2023-06-07T01:59:55.000Z","path":"article/20230607.html","text":"今天让我们来探寻一下Mybatis的运行流程,我们将它的运行流程分为2个阶段。 第一阶段:MyBatis初始化阶段。该阶段用来完成 MyBatis运行环境的准备工作,只在 MyBatis启动时运行一次。 第二阶段:数据读写阶段。该阶段由数据读写操作触发,将根据要求完成具体的增、删、改、查等数据库操作。 初始化阶段 根据配置文件的位置,获取它的输入流 InputStream 12345678910111213141516171819InputStream getResourceAsStream(String resource, ClassLoader[] classLoader) { for (ClassLoader cl : classLoader) { if (null != cl) { // try to find the resource as passed InputStream returnValue = cl.getResourceAsStream(resource); // now, some class loaders want this leading "/", so we'll add it and try again if we didn't find the resource if (null == returnValue) { returnValue = cl.getResourceAsStream("/" + resource); } if (null != returnValue) { return returnValue; } } } return null; } 从配置文件的根节点开始,逐层解析配置文件,也包括相关的映射文件。解析过程中不断将解析结果放入 Configuration对象。 以配置好的 Configuration对象为参数,获取一个 SqlSessionFactory对象。 123456789101112131415161718public SqlSessionFactory build(Reader reader, String environment, Properties properties) { try { XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties); return build(parser.parse()); } catch (Exception e) { throw ExceptionFactory.wrapException("Error building SqlSession.", e); } finally { ErrorContext.instance().reset(); try { if (reader != null) { reader.close(); } } catch (IOException e) { // Intentionally ignore. Prefer previous error. } } } 重要代码注释: > 1. 生成了一个 XMLConfigBuilder 对象,并调用了其 parse 方法,得到一个Configuration对象(因为 parse方法的输出结果为 Configuration对象)。 > 2. 调用了 SqlSessionFactoryBuilder 自身的 build方法, 传入参数为上一步得到的Configuration对象。 数据读写阶段追踪进行一次数据库的读或写操作时,MyBatis内部都要经过哪些步骤 获得SqlSession通过初始化阶段获取的SqlSessionFactory生成数据库操作中所需要的SqlSession 1SqlSession sqlSession = sqlSessionFactory.openSession(); 映射接口文件与映射文件的绑定映射接口文件是指存有Java接口的文件,而映射文件是指存有sql操作的xml文件 在进行数据查询之前,先通过SqlSession的getMapper()获取映射接口的实现, 该操作通过 Configuration类的 getMapper方法转接,最终进入 MapperRegistry类中的getMapper方法。MapperRegistry类中的 getMapper() 1234567891011121314151617181920212223// DefaultSqlSession.class@Overridepublic <T> T getMapper(Class<T> type) { return configuration.getMapper(type, this);}// Configuration.classpublic <T> T getMapper(Class<T> type, SqlSession sqlSession) { return mapperRegistry.getMapper(type, sqlSession);}// MapperRegistry.classpublic <T> T getMapper(Class<T> type, SqlSession sqlSession) { final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type); if (mapperProxyFactory == null) { throw new BindingException("Type " + type + " is not known to the MapperRegistry."); } try { return mapperProxyFactory.newInstance(sqlSession); } catch (Exception e) { throw new BindingException("Error getting mapper instance. Cause: " + e, e); } } 映射接口的代理session.getMapper() 得到的是一个mapperProxyFactory.newInstance(sqlSession)返回的对象,这个对象是一个基于反射的动态代理对象 123456789@SuppressWarnings("unchecked")protected T newInstance(MapperProxy<T> mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);}public T newInstance(SqlSession sqlSession) { final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy);} 最终数据查询会进入MapperProxy的invoke(),这是因为被代理对象的方法会被代理对象的invoke()拦截 1234567891011@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } return cachedInvoker(method).invoke(proxy, method, args, sqlSession); } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); }} 然后会触发MapperMethod对象的execute() 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748public Object execute(SqlSession sqlSession, Object[] args) { Object result; switch (command.getType()) { case INSERT: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.insert(command.getName(), param)); break; } case UPDATE: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.update(command.getName(), param)); break; } case DELETE: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.delete(command.getName(), param)); break; } case SELECT: if (method.returnsVoid() && method.hasResultHandler()) { executeWithResultHandler(sqlSession, args); result = null; } else if (method.returnsMany()) { result = executeForMany(sqlSession, args); } else if (method.returnsMap()) { result = executeForMap(sqlSession, args); } else if (method.returnsCursor()) { result = executeForCursor(sqlSession, args); } else { Object param = method.convertArgsToSqlCommandParam(args); result = sqlSession.selectOne(command.getName(), param); if (method.returnsOptional() && (result == null || !method.getReturnType().equals(result.getClass()))) { result = Optional.ofNullable(result); } } break; case FLUSH: result = sqlSession.flushStatements(); break; default: throw new BindingException("Unknown execution method for: " + command.getName()); } if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) { throw new BindingException("Mapper method '" + command.getName() + "' attempted to return null from a method with a primitive return type (" + method.getReturnType() + ")."); } return result; } MyBatis根据不同数据库操作类型调用了不同的处理方法 executeForMany 方法中,MyBatis 开始通过 SqlSession 对象的selectList方法开展后续的查询工作。 追踪到这里,MyBatis 已经完成了为映射接口注入实现的过程。 于是,对映射接口中抽象方法的调用转变为了数据查询操作。 SQL语句的查找每个 MappedStatement 对象对应了我们设置的一个数据库操作节 点,它主要定义了数据库操作语句、输入/输出参数等信息。configuration.getMappedStatement(statement)语句将要执行的MappedStatement对象从 Configuration`对象存储的映射文件信息中找了出来。 1234567891011private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) { try { MappedStatement ms = configuration.getMappedStatement(statement); dirty |= ms.isDirtySelect(); return executor.query(ms, wrapCollection(parameter), rowBounds, handler); } catch (Exception e) { throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } } 查询结果缓存query方法是一个 Executor接口中的抽象方法,实际执行的是 CachingExecutor类中的方法 1234567// CachingExecutor.classpublic <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameterObject); CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql); return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); } BoundSql是经过层层转化后去除掉 if、where等标签的 SQL语 句,而 CacheKey是为该次查询操作计算出来的缓存键 数据库查询12345678910111213141516171819// CachingExecutor.classpublic <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { Cache cache = ms.getCache(); if (cache != null) { flushCacheIfRequired(ms); if (ms.isUseCache() && resultHandler == null) { ensureNoOutParams(ms, boundSql); @SuppressWarnings("unchecked") List<E> list = (List<E>) tcm.getObject(cache, key); if (list == null) { list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); tcm.putObject(cache, key, list); // issue #578 and #116 } return list; } } return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); } MyBatis查看当前的查询操作是否命中缓存。如果 是,则从缓存中获取数据结果;否则,便通过 delegate调用 query方 法。 delegate调用的 query方法实际上是BaseExecutor类中的 query方法 1234567891011121314151617181920212223242526272829303132333435// BaseExecutor.classpublic <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId()); if (closed) { throw new ExecutorException("Executor was closed."); } if (queryStack == 0 && ms.isFlushCacheRequired()) { clearLocalCache(); } List<E> list; try { queryStack++; list = resultHandler == null ? (List<E>) localCache.getObject(key) : null; if (list != null) { handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); } else { list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); } } finally { queryStack--; } if (queryStack == 0) { for (DeferredLoad deferredLoad : deferredLoads) { deferredLoad.load(); } // issue #601 deferredLoads.clear(); if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { // issue #482 clearLocalCache(); } } return list; } MyBatis开始调用数据库展开查询操 作 12345678910111213141516// BaseExecutor.classprivate <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { List<E> list; localCache.putObject(key, EXECUTION_PLACEHOLDER); try { list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql); } finally { localCache.removeObject(key); } localCache.putObject(key, list); if (ms.getStatementType() == StatementType.CALLABLE) { localOutputParameterCache.putObject(key, parameter); } return list; } MyBatis先在缓存中放置一个占位符,然 后调用 doQuery方法实际执行查询操作。最后,又把缓存中的占位符 替换成真正的查询结果 1234567891011121314// SimpleExecutor.classpublic <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { Statement stmt = null; try { Configuration configuration = ms.getConfiguration(); StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); stmt = prepareStatement(handler, ms.getStatementLog()); return handler.query(stmt, resultHandler); } finally { closeStatement(stmt); } } 上述方法生成了Statement对象stmt。Statement类并不是MyBatis 中的类,而是java.sql包中的类。Statement类能够执行静态 SQL语句 并返回结果。 程序还通过 Configuration的 newStatementHandler方法获得了 一个 StatementHandler对象 handler,然后将查询操作交给 StatementHandler对象进行。StatementHandler是一个语句处理器类,其中封 装了很多语句操作方法 123456// PreparedStatementHandler.classpublic <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException { PreparedStatement ps = (PreparedStatement) statement; ps.execute(); return resultSetHandler.handleResultSets(ps); } 这里 ps.execute()真正执行了 SQL 语句,然后把执行结果交 给 ResultHandler 对象处理。而PreparedStatement类并不是MyBatis 中的类,因而ps.execute()的执行不再由MyBatis负责,而是由 com.mysql.cj.jdbc包中的类负责,这里不再继续追踪。 总结: 在进行数据库查询前,先查询缓存;如果确实需要查询数据 库,则数据库查询之后的结果也放入缓存中。 · SQL 语句的执行经过了层层转化,依次经过了 MappedStatement 对象、Statement对象和 PreparedStatement对象,最后才得以执 行。 · 最终数据库查询得到的结果交给 ResultHandler对象处理。 处理结果集12345678910111213141516171819202122232425262728293031323334353637// DefaultResultSetHandler.classpublic List<Object> handleResultSets(Statement stmt) throws SQLException { ErrorContext.instance().activity("handling results").object(mappedStatement.getId()); final List<Object> multipleResults = new ArrayList<>(); int resultSetCount = 0; ResultSetWrapper rsw = getFirstResultSet(stmt); List<ResultMap> resultMaps = mappedStatement.getResultMaps(); int resultMapCount = resultMaps.size(); validateResultMapsCount(rsw, resultMapCount); while (rsw != null && resultMapCount > resultSetCount) { ResultMap resultMap = resultMaps.get(resultSetCount); handleResultSet(rsw, resultMap, multipleResults, null); rsw = getNextResultSet(stmt); cleanUpAfterHandlingResultSet(); resultSetCount++; } String[] resultSets = mappedStatement.getResultSets(); if (resultSets != null) { while (rsw != null && resultSetCount < resultSets.length) { ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]); if (parentMapping != null) { String nestedResultMapId = parentMapping.getNestedResultMapId(); ResultMap resultMap = configuration.getResultMap(nestedResultMapId); handleResultSet(rsw, resultMap, null, parentMapping); } rsw = getNextResultSet(stmt); cleanUpAfterHandlingResultSet(); resultSetCount++; } } return collapseSingleResultList(multipleResults); }","tags":[{"name":"Mybatis","slug":"Mybatis","permalink":"https://alexhuihui.github.io/tags/Mybatis/"}]},{"title":"【事务处理】本地事务","date":"2023-05-17T02:36:48.000Z","path":"article/20230517.html","text":"大家都知道数据库事务中有着AICD的说法,实际上C(一致性)是目的,而对应的A(原子性)、I(隔离性)、D(持久性)是手段,AID -> C。 概念介绍原子性原子性保证了事务的多个操作要么都生效要么都不生效,不会存在中间状态 隔离性在不同的业务处理过程中,互不影响各自读写数据 持久性所有提交成功的数据修改都应该被持久化保存 实现原子性和持久性","tags":[{"name":"事务","slug":"事务","permalink":"https://alexhuihui.github.io/tags/%E4%BA%8B%E5%8A%A1/"}]},{"title":"【项目】分诊知识库系统设计","date":"2023-05-12T07:54:56.000Z","path":"article/20230512.html","text":"分诊知识库的系统设计 设计方案: 数据库设计针对疾病、药品、急救和异常分诊案例,分别设计对应的数据表以及关联的数据表。 Elasticsearch索引设计在Elasticsearch中,为疾病、药品和急救建立一个统一的索引,方便进行搜索。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177{ "index_patterns": [ "local-knowledge-*", "dev-knowledge-*", "sit-knowledge-*", "uat-knowledge-*", "prod-knowledge-*" ], "settings": { "number_of_shards": 1, "number_of_replicas": 1, "analysis": { "filter": { "len": { "type": "length", "min": 2 } }, "analyzer": { "custom_ik_max": { "tokenizer": "ik_max_word", "filter": [ "len" ] } } } }, "mappings": { "properties": { "name": { "type": "text", "analyzer": "custom_ik_max", "search_analyzer": "ik_smart" }, "commonName": { "type": "text", "analyzer": "custom_ik_max", "search_analyzer": "ik_smart" }, "infoType": { "type": "integer" }, "introduction": { "type": "text", "analyzer": "custom_ik_max", "search_analyzer": "ik_smart" }, "typeId": { "type": "long" }, "typeName": { "type": "keyword" }, "relatedDepartments": { "type": "nested", "properties": { "departmentId": { "type": "long" }, "departmentName": { "type": "keyword" } } }, "relatedPositions": { "type": "nested", "properties": { "positionId": { "type": "long" }, "positionName": { "type": "keyword" } } }, "fdaId": { "type": "long" }, "fdaName": { "type": "keyword" }, "morbidityId": { "type": "long" }, "morbidityName": { "type": "keyword" }, "symptomDescription": { "type": "text", "analyzer": "custom_ik_max", "search_analyzer": "ik_smart" }, "checkDescription": { "type": "text", "analyzer": "custom_ik_max", "search_analyzer": "ik_smart" }, "treatment": { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_smart" }, "prevention": { "type": "text", "analyzer": "custom_ik_max", "search_analyzer": "ik_smart" }, "concurrent": { "type": "text", "analyzer": "custom_ik_max", "search_analyzer": "ik_smart" }, "diagnosis": { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_smart" }, "adverseReaction": { "type": "text", "analyzer": "custom_ik_max", "search_analyzer": "ik_smart" }, "component": { "type": "text", "analyzer": "custom_ik_max", "search_analyzer": "ik_smart" }, "contraindication": { "type": "text", "analyzer": "custom_ik_max", "search_analyzer": "ik_smart" }, "dosage": { "type": "text", "analyzer": "custom_ik_max", "search_analyzer": "ik_smart" }, "indication": { "type": "text", "analyzer": "custom_ik_max", "search_analyzer": "ik_smart" }, "content": { "type": "text", "analyzer": "custom_ik_max", "search_analyzer": "ik_smart" }, "note": { "type": "text", "analyzer": "custom_ik_max", "search_analyzer": "ik_smart" }, "crtId": { "type": "long" }, "crtName": { "type": "text" }, "crtTime": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis" }, "updId": { "type": "long" }, "updName": { "type": "text" }, "updTime": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis" } } }} 应用程序设计为疾病、药品、急救和异常分诊案例提供基础数据管理能力,API如下 疾病模块API接口 API名称 请求方式 接口路径 请求参数 返回结果 备注 获取疾病列表 GET /diseases - 疾病列表 分页获取所有疾病信息 获取疾病详情 GET /diseases/{id} id (疾病ID) 疾病详情 根据疾病ID获取疾病详细信息 创建疾病 POST /diseases 疾病信息 创建成功的疾病 创建新的疾病 更新疾病 PUT /diseases/{id} id (疾病ID) 更新成功的疾病 根据疾病ID更新疾病信息 删除疾病 DELETE /diseases/{id} id (疾病ID) 删除成功的疾病 根据疾病ID删除疾病 药品模块API接口 API名称 请求方式 接口路径 请求参数 返回结果 备注 获取药品列表 GET /drugs - 药品列表 分页获取所有药品信息 获取药品详情 GET /drugs/{id} id (药品ID) 药品详情 根据药品ID获取药品详细信息 创建药品 POST /drugs 药品信息 创建成功的药品 创建新的药品 更新药品 PUT /drugs/{id} id (药品ID) 更新成功的药品 根据药品ID更新药品信息 删除药品 DELETE /drugs/{id} id (药品ID) 删除成功的药品 根据药品ID删除药品 急救模块API接口 API名称 请求方式 接口路径 请求参数 返回结果 备注 获取急救信息列表 GET /emergencies - 急救信息列表 分页获取所有急救信息 获取急救信息详情 GET /emergencies/{id} id (急救信息ID) 急救信息详情 根据急救信息ID获取急救信息详细信息 创建急救信息 POST /emergencies 急救信息 创建成功的急救信息 创建新的急救信息 更新急救信息 PUT /emergencies/{id} id (急救信息ID) 更新成功的急救信息 根据急救信息ID更新急救信息 删除急救信息 DELETE /emergencies/{id} id (急救信息ID) 删除成功的急救信息 根据急救信息ID删除急救信息 异常分诊案例模块API接口 API名称 请求方式 接口路径 请求参数 返回结果 备注 获取异常分诊案例列表 GET /case - 异常分诊案例列表 分页获取所有异常分诊案例信息 获取异常分诊案例详情 GET /case/{id} id (案例ID) 异常分诊案例详情 根据案例ID获取异常分诊案例详细信息 创建异常分诊案例 POST /case 异常分诊案例信息 创建成功的异常分诊案例 创建新的异常分诊案例 更新异常分诊案例 PUT /case/{id} id (案例ID) 更新成功的异常分诊案例 根据案例ID更新异常分诊案例信息 删除异常分诊案例 DELETE /case/{id} id (案例ID) 删除成功的异常分诊案例 根据案例ID删除异常分诊案例 获取案例关联疾病列表 GET /case/{id}/diseases id (案例ID) 关联疾病列表 根据案例ID获取关联的疾病列表 获取案例关联科室列表 GET /case/{id}/departments id (案例ID) 关联科室列表 安全性设计针对敏感数据,例如药品名称、疾病名称和病人隐私信息等,需要实现访问控制和安全保护措施。基于角色和权限的访问控制,以及对数据的加密和解密保护。","tags":[{"name":"design","slug":"design","permalink":"https://alexhuihui.github.io/tags/design/"}]},{"title":"【LLM】LLM入门","date":"2023-04-14T06:58:04.000Z","path":"article/20230414.html","text":"LLM介绍语言模型在最近几年内迅速提高,大型语言模型(LLMs)如GPT-3和GPT-4成为中心。这些模型因其能够以惊人的技巧执行各种任务而变得流行。此外,随着这些模型的参数数量(数十亿!)增加,它们不可预测地获得了新的能力。 在本文中,我们将探讨LLMs、它们可以执行的任务、它们的缺点以及各种提示工程策略。 什么是LLMs?LLMs是神经网络,在大量文本数据上进行了训练。训练过程使模型学习文本中的模式,包括语法、句法和词汇关联。这些学习到的模式被用于生成类似人类文字,使其非常适合自然语言处理(NLP)任务。 哪些LLM可用?有几个LLM可用,其中GPT-4最受欢迎。其他模型包括 LLaMA、PaLM、BERT 和 T5 等等。每个模型都有其优点和缺点,并且一些是开放性使用而其他则只能通过API使用。 LLM 的局限性尽管表现出色, 但 LLMS 也存在一些限制. 其中一个重要缺陷是无法超越提示中提供的信息进行推理。此外,LLMs 可以根据它们所训练的数据生成有偏见的文本。控制 LLMs 的输出也很具挑战性,因此需要使用提示工程策略来实现所需的输出。。 你能执行哪些任务?我们可以通过为每个案例格式化提示来指导LLMs执行特定的任务。以下是这些任务和提示模板以及每个示例的列表。 文本摘要LLMs可以生成长文本的摘要,使内容更易于理解和消化。我们可以使用以下提示模板请求对一段文字进行摘要: 12<Full text>Summarize the text above: / Explain the text above in <N> sentences: 例如,我们可以要求按照以下提示对这段文本进行总结: 123In a mechanical watch the watch's gears are turned by a spiral spring called a mainspring. In a manual watch energy is stored in the mainspring by turning a knob, the crown on the side of the watch. Then the energy from the mainspring powers the watch movement until it runs down, requiring the spring to be wound again.A self-winding watch movement has a mechanism which winds the mainspring using the natural motions of the wearer's body. The watch contains an oscillating weight that turns on a pivot. The normal movements of the watch in the user's pocket (for a pocketwatch) or on the user's arm (for a wristwatch) cause the rotor to pivot on its staff, which is attached to a ratcheted winding mechanism. The motion of the watch is thereby translated into circular motion of the weight which, through a series of reverser and reducing gears, eventually winds the mainspring. There are many different designs for modern self-winding mechanisms. Some designs allow winding of the watch to take place while the weight swings in only one direction while other, more advanced, mechanisms have two ratchets and wind the mainspring during both clockwise and anti-clockwise weight motions.Explain the text above in 2 sentences: 这将产生以下输出: 1A mechanical watch uses a mainspring to power the movement of the watch's gears. In a manual watch, the mainspring is wound by turning a knob, while a self-winding watch uses the natural motion of the wearer's body to wind the mainspring through an oscillating weight and ratcheted winding mechanism. 信息提取LLMs可以从大量文本中识别和提取特定信息。这在新闻和商业情报等领域非常有用。以下模板指导模型提取信息: 12<Full text>Mention X in the text above: / List X in the text above: 例如,我们可以要求从以下文本中提取地点名称: 12The earliest credible evidence for a successful design is the watch made by the Swiss watchmaker Abraham-Louis Perrelet, who lived in Le Locle. In late 1776 or early 1777, he invented a self-winding mechanism for pocket watches using an oscillating weight inside the watch that moved up and down. The Geneva Society of Arts, reporting on this watch in 1777, stated that 15 minutes walking was necessary to fully wind the watch.List the names of places in the text above: 这将产生以下输出: 1Le Locle, Geneva. 问答LLMs还可以根据提示提供的信息回答问题。以下模板可用于指导LLM回答问题: 模板 1234基于下面的上下文回答问题。<其他说明>。如果不确定,请回复“不确定”。上下文:<Context>Q:<Question>A: 例如,使用以下提示: 提示 1234基于下面的上下文回答问题。用一句话回答。如果不确定,请回复“不确定”。上下文:自动表也称为自动绕线表或简单地称为自动表,是一种机械表,在佩戴者的自然运动中提供能量来绕制主发条,使手工绕线变得不必要。它与手动表有所区别,因为手动表必须定期手摇其主发条。Q:自动腕表需要手动绕吗?A: ChatGPT 回应道: 输出 1否,如果足够佩戴,则无需手动缠绕自动腕表,因为佩戴者的自然运动会提供能量来维持其正常运转。 文本分类LLMs可以将文本分类为预定义的类别,例如情感分析或垃圾邮件检测。您可以按以下方式格式化文本分类指令: 模板 123将文本分类为<选择列表>:文本:<完整文本>分类: 例如,我们可以尝试对一段文字进行情感分析,判断其情绪是积极的、中性的还是消极的: 提示 123将该段文字归类为积极、中性或消极:文本:那个投篮选择太糟糕了。分类: 输出 1消极 对话LLM可以生成类似人类的对话,使它们在聊天机器人和客户服务应用程序中非常有用。我们可以指示LLM按照特定方式跟随对话行动。 模板 12345以下是与<Actor>的对话。 <Actor>应该回答<Further instructions>。个人:<Conversation><Actor>:<Response>个人:<Conversation><Actor>: LLM将填写我们指示其类型演员的响应,并且我们可以通过再次将整个会话及新响应传递给它来继续会话。例如: 提示 123以下是与律师的对话。 律师应简单明了地回答。个人:您好,我想知道如何争议停车罚单律师: 输出 1当然,我能够提供帮助。第一步是检查罚单是否存在任何错误或失误。如果发现任何问题,则可以使用这些作为争议罚款的理由。如果没有错误,则仍然可以通过提供证据支持您的案件(例如照片或证言)来争议罚款。您还可以请求听证会以亲自陈述您的案件。 提示 12345以下是与律师的对话。 律师应简单明了地回答。个人:您好,我想知道如何争议停车罚单律师:当然,我能够提供帮助。第一步是检查罚单是否存在任何错误或失误。如果发现任何问题,则可以使用这些作为争议罚款的理由。如果没有错误,则仍然可以通过提供证据支持您的案件(例如照片或证言)来争议罚款。您还可以请求听证会以亲自陈述您的案件。个人:我如何请求听证会?律师: 输出 1要请求听证会,您需要按照票据背面或网站上的说明进行操作 代码生成LLMs可以生成代码,使开发人员更容易创建复杂的软件程序。我们可以编写一个代码注释来描述我们希望代码执行的操作,LLM将能够尽力完成它。我们希望代码执行的操作越复杂,我们在提示注释中需要提供的信息就越具体和详细。 模板可能因所需生成的编程语言而异,但我建议大致遵循以下模板: 模板 12345678<您选择的语言的注释符号(/*或'''或#)>方法:<方法名称>语言:<编程语言>说明:<方法必须执行什么操作>参数:<方法将接收哪些参数>上下文:<实现该方法所需进一步信息(URL、数据库等)>输出:<该方法输出内容><您选择的语言的注释符号(/*或'''或#)> 例如,如果我们想要获取分页API,则可以编写以下评论: 提示 1234567/* Method: fetchFirst5Pages * Language: Javascript * Description: Fetch the first 5 pages from this paginated API: http://example.com/pages/?n=x * Context: This API accepts the following URL parameters: * n: the number of the page to fetch * Output: the content of the first 5 pages */ 输出 1234567891011121314151617181920212223这是JavaScript中fetchFirst5Pages函数实现示例,假设使用了fetch函数:async function fetchFirst5Pages() { const baseUrl = 'http://example.com/pages/?n='; const pages = []; for (let i = 1; i <= 5; i++) { const url = baseUrl + i; const response = await fetch(url); if (!response.ok) { throw new Error(`Error fetching page ${i}`); } const pageData = await response.json(); pages.push(pageData); } return pages;}该函数使用循环通过将页码附加到基本URL来获取API的前5个页面。它检查服务器的响应是否正常,如果不正常则抛出错误。然后,它从响应中解析JSON数据并将其添加到页面数组中。最后,它返回页面数组。 推理法学硕士可能会在一些基本任务上遇到困难,比如算术问题。然而,如果我们要求LLM对其回答进行推理,我们可以帮助它得出正确的答案。这是我推荐的模板: 模板 12问: <问题>答: 让我们逐步解决这个问题,确保我们有正确的答案。首先,<说明>。 在这个例子中,如果没有推理就提出一个简单的算术问题,则无法成功: 提示 123回答以下问题。 如果不确定答案,请回复“不确定”。问:以下列表中质数之和是3的倍数:[2, 4, 6, 7, 9, 14]A: [是或否] 输出 不是。 列表[2、4、6、7、9、14]中质数之和不是3的倍数。 因此,答案为“否”。 但是,如果指示LLM对其回答进行推理,则会得到非常不同的结果: 提示 12问:以下列表中质数之和是否为3的倍数? [2、4、6、7、9、14]A: 让我们逐步解决这个问题以确保我们有正确的答案。 首先找到列表中所有质数并将它们加起来。 然后检查所得数字是否可以被3整除。 输出 12345678910111213好吧,请让我们逐步解决这个问题。列表中的质数是2和7。 所以我们可以将它们加在一起:2 + 7 = 9结果为9。 现在我们需要检查它是否是3的倍数。 我们可以通过检查9除以3的余数是否等于0来做到这一点:9%3 = 0由于余数为0,因此我们知道9确实是3的倍数。因此,答案是“是”,列表[2、4、6、7、9、14]中质数之和是3的倍数。 策略虽然这些模板可以帮助您挤出LLMs的潜力以满足您的需求,但有时它们仍然可能在某些复杂任务上遇到困难。已经开发了一些技术来帮助LLMs得出令人满意的答案。到目前为止,我们只使用了零-shot提示,现在我们将对其进行定义。但是,我们可以使用更复杂的技术。 零-shot提示在本文中的所有示例中,我们都使用了零-shot提示。零-shot提示意味着向LLM传递一个提示,其中只包含我们想要它回答的问题,并没有任何其他情况下应该如何响应的示例。例如,在早期示例中: Prompt 123将文本分类为积极、中性或消极:Text: That shot selection was awfulClassification: Output 1Negative 这可能不足以让模型准确地回答我们想要它回答的问题。让我们看看还有哪些替代方案。 少量样本提示与零-shot提示相反,在少量样本提示中,在提问之前给LLM提供一些如何响应的示例。例如,假设我们想像先前示例那样对文本进行情感分类。但是每次都希望以非常特定格式获得答案。我们可以事先给它一些示例,如下所示: Prompt 123456789将文本分类为积极、中性或消极:Text: Today the weather is fantasticClassification: PosText: The furniture is small.Classification: NeuText: I don't like your attitudeClassification: NegText: That shot selection was awfulClassification: Output 1Neg 思维链提示对于更复杂的任务,我们可以将少量示例提示与使模型推理其答案相结合。 我们可以使用之前的相同示例来查看推理和少量提示如何影响结果: 12345678Q: The sum of prime numbers in the following list are a multiple of 3: [2, 4, 6, 7, 9, 14]A: The prime numbers in the list are 2 and 7. Their sum is 9. 9 % 3 = 0. The answer is yes.Q: The sum of prime numbers in the following list are a multiple of 3: [1, 2, 4, 7, 8, 10]A: The prime numbers in the list are 1, 2 and 7. Their sum is 10. 10 % 3 = 1. The answer is no.Q: The sum of prime numbers in the following list are a multiple of 3: [1, 2, 3, 4]A: The prime numbers in the list are 1, 2 and 3. Their sum is 6. 6 % 3 = 0. The answer is yes.Q: The sum of prime numbers in the following list are a multiple of 3: [5, 6, 7, 8]A: 1The prime numbers in the list are 5 and 7. Their sum is 12. 12 % 3 = 0. The answer is yes. 最后的注释LLM已经彻底改变了自然语言处理领域,但为了最大化其潜力,提示工程至关重要。通过理解LLM可以执行的任务、它们的缺点以及各种提示工程策略,开发人员可以利用LLM的能力来创建创新和有效的解决方案。未来可能会开发更多的策略和技术,因此请继续关注这个领域的进展,以进一步最大化LLM潜力。此外,随着LLMs参数数量不断增加到数十亿级别,我们现在甚至无法想象更多任务将有可能被实现。想到使用这些新工具所能实现什么样子以及它们将为我们服务于哪些用例是非常令人兴奋的事情。","tags":[{"name":"LLM","slug":"LLM","permalink":"https://alexhuihui.github.io/tags/LLM/"},{"name":"AI","slug":"AI","permalink":"https://alexhuihui.github.io/tags/AI/"}]},{"title":"【项目】升级现有的授权系统","date":"2023-04-11T03:09:38.000Z","path":"article/20230411.html","text":"现有的授权系统已无法满足对业务的支持,所以有必要针对授权系统进行一次升级。在升级之前先详细拆解一下之前的授权系统。 旧的授权系统详情认证系统负责颁发令牌,系统一共拥有3种类型的用户,分别是普通用户、系统用户和微服务。其中普通用户和系统用户都是共用的同一种类型的token,叫做UserToken,微服务使用的是ClientToken。这两种不同类型的token在传输过程中使用不同的请求头进行存储,当微服务之间互相调用的时候,会有一个feign拦截器负责把当前线程中的用户token和本身的微服务token,存入到对应的请求头中。发送mq时,也是采用了类似的机制进行传递token。尤其要注意的是在只有微服务token时并且CheckUserToken注解不为空时,拦截器会把微服务的信息转换成用户信息存储到安全上下文中,但不会设置用户token。 流程图 旧系统的痛点 无法做到针对每个接口进行访问控制 没有token续期功能,用户体验不好 没有能力提供给外部系统与我们进行对接 新的授权系统对每个接口进行访问控制授权过程在实际的授权过程中,可以在请求到达接口之前,使用拦截器来进行身份验证和授权处理。在拦截器中,首先从请求头中获取Token,如果Token不存在,则返回“未授权”的错误信息。 如果Token存在,则使用JWT对Token进行解密和验证,如果解密和验证成功,则从Token中获取用户信息和访问权限信息,并将它们存储在ThreadLocal中,以便在整个请求处理过程中可以方便地访问它们。 最后,在请求处理完成后,清除ThreadLocal中存储的信息,以避免对下一个请求产生干扰。 流程图 Token续期Token刷新机制JWT Token自动刷新机制需要在前端和后端同时实现。 在每次请求的返回中都检查 token 是否快过期,如果临近过期就刷新 token,并且在 response 的 header 中返回给前端。前端可以通过读取响应 header 中的 token 来更新本地的 token。 具体实现可以在后端的拦截器中添加一个逻辑,在每次请求返回时检查 token 是否快过期,如果是,则进行 token 的刷新。同时,将新的 token 放入 response 的 header 中返回给前端。 123456789101112131415161718192021// 伪代码public class TokenRefreshInterceptor implements HandlerInterceptor { private TokenProvider tokenProvider; public TokenRefreshInterceptor(TokenProvider tokenProvider) { this.tokenProvider = tokenProvider; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 检查 token 是否需要刷新 String token = tokenProvider.resolveToken(request); if (token != null && tokenProvider.isTokenNeedRefresh(token)) { // 刷新 token String newToken = tokenProvider.refreshToken(token); // 将新的 token 放入响应 header 中 response.setHeader("Authorization", "Bearer " + newToken); } }} 流程图","tags":[{"name":"security","slug":"security","permalink":"https://alexhuihui.github.io/tags/security/"},{"name":"Authorization","slug":"Authorization","permalink":"https://alexhuihui.github.io/tags/Authorization/"}]},{"title":"【Redis】数据结构和对象","date":"2023-03-29T00:42:28.000Z","path":"article/20230329.html","text":"开篇本文介绍redis中的数据结构和对象 简单动态字符串Redis没有使用C语言传统的字符串表示,而是自己构建了一种名为简单动态字符串的抽象类型,用在可以被修改的字符串值,比如包含字符串值的键值对。 SDS的定义每个 sds.h/sdshdr结构表示一个 SDS值: 12345678struct sdshdr { // 记录已使用字节的数量 int len; // 记录buf数组中未使用字节的数量 int free; // 字节数组,用于保存字符串 chat buf[];} SDS 与 C 字符串的区别根据传统,C语言使用长度为N+1的字符数组来表示长度为N的字符串,并且字符数组的最后一个元素总是空字符·\\0’。C语言使用的这种简单的字符串表示方式,并不能满足Rdis对字符串在安全性、效率以及功能方面的要求。 常数复杂度获取字符串长度 杜绝缓冲区溢出 减少修改字符串时带来的内存重分配次数 二进制安全 兼容部分C字符串函数 链表链表提供了高效的节点重排能力,以及顺序性的节点访问方式,并且可以通过增删节点来灵活地调整链表的长度。作为一种常用数据结构,链表内置在很多高级的编程语言里面,因为Rdis使用的C语言并没有内置这种数据结构,所以Redis构建了自己的链表实现。 使用场景: 列表键 发布订阅 慢查询 监视器 服务端保存各个客户端的状态信息 使用链表来构建客户端输出缓冲区 链表和链表节点的实现每个链表节点使用一个 adlist.h/listNode结构来表示 12345678typedef struct listNode { // 前置节点 struct listNode *prev; // 后置节点 struct listNode *next; // 节点的值 void *value;} 虽然仅仅使用多个listNode结构就可以组成链表,但使用adlist.h/list来持有链表的话,操作起来会更方便: 12345678910111213141516171819typedef struct list { // 表头节点 listNode *head; // 表尾结点 listNode *tail; // 链表所包含的节点数量 unsigned long len; // 节点值复制函数 void *(*dup) (void *ptr); // 节点值释放函数 void *(*free) (void *ptr); // 节点值对比函数 int (*match) (void *ptr, void *key);} Redis的链表实现的特性可以总结如下: 双端:链表节点带有prev和next指针,获取某个节点的前置节点和后置节点的复杂度都是O(1) 无环:表头节点的prev指针和表尾节点的next指针都指向NULL,对链表的访问以NULL为终点。 带表头指针和表尾指针:通过list结构的head指针和tail指针,程序获取链表的表头节点和表尾节点的复杂度为O(1)。 带链表长度计数器:程序使用1ist结构的1en属性来对1ist持有的链表节点进行计数,程序获取链表中节点数量的复杂度为O(1)。 多态:链表节点使用void*指针来保存节点值,并且可以通过ist结构的dup、free、match三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。 字典字典,又称为符号表(symbol table)关联数组(associative array)或映射(map),是一种用于保存键值对(key-value pair)的抽象数据结构。","tags":[{"name":"Redis","slug":"Redis","permalink":"https://alexhuihui.github.io/tags/Redis/"},{"name":"数据结构","slug":"数据结构","permalink":"https://alexhuihui.github.io/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"}]},{"title":"【Go语言学习】06-协程、延迟函数调用、以及恐慌和恢复","date":"2023-03-06T06:33:03.000Z","path":"article/20230306.html","text":"协程协程(coroutine)是一种轻量级线程,可以在一个线程中实现多个协程并发执行,而不是像传统的线程一样需要多个线程并发执行。在 Go 语言中,协程是通过关键字 go 来启动的。 例如,以下代码启动了一个协程,执行一个函数 myFunc: 12go myFunc() 在上面的代码中,myFunc 函数会在一个新的协程中执行,而主线程会继续执行下一条语句,不会阻塞等待 myFunc 的执行完成。 延迟函数调用延迟函数调用(defer)是一种语言特性,它可以在函数执行结束后再执行一个函数。在 Go 语言中,延迟函数调用是通过关键字 defer 来实现的。 例如,以下代码中的函数 myFunc 中有一个延迟函数调用 defer cleanup(): 12345func myFunc() { defer cleanup() // do some work} 在上面的代码中,当函数 myFunc 执行完毕后,会自动调用函数 cleanup。这种特性通常用于资源的清理工作,例如关闭文件、释放内存等。 恐慌和恢复恐慌(panic)和恢复(recover)是 Go 语言中处理异常的机制。当程序出现严重错误时,例如空指针引用、数组越界等,Go 会自动抛出一个恐慌(panic),如果没有被处理,程序会中止执行。 恢复(recover)是一种处理恐慌的机制。在一个 defer 函数中使用 recover 函数可以捕获到恐慌,并进行处理,防止程序中止执行。 例如,以下代码中的函数 myFunc 中有一个恐慌的处理: 123456789func myFunc() { defer func() { if r := recover(); r != nil { // handle the panic } }() // do some work} 在上面的代码中,当函数 myFunc 出现恐慌时,会执行 defer 中的函数,并且可以通过 recover 函数捕获到恐慌,并进行处理。 举例说明: 12345678910111213func main() { go func() { defer func() { if r := recover(); r != nil { fmt.Println("Recovered in f", r) } }() fmt.Println("start") panic("something wrong") }() time.Sleep(time.Second)} 上面的代码中,我们启动了一个协程,在其中执行了一个函数,当函数执行到 panic("something wrong") 时,会触发恐慌,但是由于我们在 defer 中使用了 recover 函数捕获了恐慌,因此程序并不会中止执行。最终,程序输出了以下内容: 123startRecovered in f something wrong","tags":[{"name":"Go","slug":"Go","permalink":"https://alexhuihui.github.io/tags/Go/"}]},{"title":"【Go语言学习】05-表达式、语句和简单语句","date":"2023-03-06T05:34:02.000Z","path":"article/20230306.html","text":"在编程语言中,表达式(Expression)、语句(Statement)和简单语句(Simple Statement)是三个不同的概念。 表达式(Expression)是计算出一个值的代码片段,它可以包含变量、常量、运算符、函数调用等。表达式是程序中的基本构建块之一,可以用于赋值、比较、条件分支等场景。 举例来说,在 Go 中,以下是一些表达式的例子: 12345678910111213141516171819// 字面量表达式10"Hello World"true// 变量表达式ab + 5c > 0// 函数调用表达式math.Sqrt(x)fmt.Println("Hello")// 运算符表达式a + bc * d - 1!isDone 语句(Statement)是执行一些操作的代码片段,它可以是赋值语句、控制流语句、函数调用语句等。语句可以改变程序状态或执行一些计算,它们通常由一个或多个表达式组成。 举例来说,在 Go 中,以下是一些语句的例子: 123456789101112131415161718192021// 赋值语句a = 10b = 5 + ac, d = d, c // 交换变量值// 控制流语句if x > 0 { fmt.Println("Positive")} else if x < 0 { fmt.Println("Negative")} else { fmt.Println("Zero")}for i := 0; i < 10; i++ { fmt.Println(i)}// 函数调用语句fmt.Println("Hello World") 简单语句(Simple Statement)是一种特殊的语句,它只包含一个单独的表达式。简单语句通常出现在循环体中或者控制流语句中的条件部分。 举例来说,在 Go 中,以下是一些简单语句的例子: 12345678// 赋值语句是一种简单语句a = 10// 函数调用语句是一种简单语句fmt.Println("Hello World")// 空语句是一种特殊的简单语句,它不做任何操作 总之,表达式是计算值的代码片段,语句是执行操作的代码片段,而简单语句是一种特殊的语句,只包含一个表达式。在编写程序时,理解它们之间的差异非常重要,以便正确地组织代码。","tags":[{"name":"Go","slug":"Go","permalink":"https://alexhuihui.github.io/tags/Go/"}]},{"title":"【Go语言学习】04-结构体","date":"2023-03-04T03:04:16.000Z","path":"article/20230304.html","text":"编程实际上是对真实世界的映射 在go中可以使用以下方式定义类型 1type T S 每个类型都有其底层类型 底层类型不同的两个类型无法做到显示转换 如何定义一个结构体类型","tags":[{"name":"Go","slug":"Go","permalink":"https://alexhuihui.github.io/tags/Go/"}]},{"title":"【Go语言学习】03-复合数据类型","date":"2023-03-04T02:45:11.000Z","path":"article/20230304.html","text":"本文介绍go的另一个复合类型 map 什么是map类型map 是 Go 语言提供的一种抽象数据类型,它表示一组无序的键值对。在后面的讲解中,我们会直接使用 key 和 value 分别代表 map 的键和值。而且,map 集合中每个 key 都是唯一的: 和切片类似,作为复合类型的 map,它在 Go 中的类型表示也是由 key 类型与 value 类型组成的,就像下面代码: 1map[key_type]value_type key 与 value 的类型可以相同,也可以不同: 12map[string]string // key与value元素的类型相同map[int]string // key与value元素的类型不同 map 类型对 value 的类型没有限制,但是对 key 的类型却有严格要求,因为 map 类型要保证 key 的唯一性。Go 语言中要求,key 的类型必须支持“==”和“!=”两种比较操作符。但是,在 Go 语言中,函数类型、map 类型自身,以及切片只支持与 nil 的比较,而不支持同类型两个变量的比较。如果像下面代码这样,进行这些类型的比较,Go 编译器将会报错: 12345678910s1 := make([]int, 1)s2 := make([]int, 2)f1 := func() {}f2 := func() {}m1 := make(map[int]string)m2 := make(map[int]string)println(s1 == s2) // 错误:invalid operation: s1 == s2 (slice can only be compared to nil)println(f1 == f2) // 错误:invalid operation: f1 == f2 (func can only be compared to nil)println(m1 == m2) // 错误:invalid operation: m1 == m2 (map can only be compared to nil) 所以函数类型、map 类型自身,以及切片类型是不能作为 map 的 key 类型的。 map 变量的声明和初始化方法一:使用复合字面值初始化 map 类型变量。 1234567type Position struct { x float64 y float64}m := map[Position]string{ {29.935523, 52.568915}: "school", {25.352594, 113.304361}: "shopping-mall", {73.224455, 111.804306}: "hospital",} 方法二:使用 make 为 map 类型变量进行显式初始化。 12m1 := make(map[int]string) // 未指定初始容量m2 := make(map[int]string, 8) // 指定初始容量为8 map的基本操作插入 查找: 12m := make(map[string]int)v := m["key1"] 当我们尝试去获取一个键对应的值的时候,如果这个键在 map 中并不存在,我们也会得到一个值,这个值是 value 元素类型的零值。所以go采用 comma ok的方式判断key对应的value值存在与否 1234567m := make(map[string]int)v, ok := m["key1"]if !ok { // "key1"不在map中}// "key1"在map中,v将被赋予"key1"键对应的value 删除: 只能使用delete函数,即使key值不存在,函数执行也不会失败 12345678m := map[string]int { "key1" : 1, "key2" : 2,}fmt.Println(m) // map[key1:1 key2:2]delete(m, "key2") // 删除"key2"fmt.Println(m) // map[key1:1] 遍历: 遍历 map 的键值对只有一种方法,那就是像对待切片那样通过 for range 语句对 map 数据进行遍历 1234567891011121314151617package main import "fmt"func main() { m := map[int]int{ 1: 11, 2: 12, 3: 13, } fmt.Printf("{ ") for k, v := range m { fmt.Printf("[%d, %d] ", k, v) } fmt.Printf("}\\n")} map的底层实现待补充","tags":[{"name":"Go","slug":"Go","permalink":"https://alexhuihui.github.io/tags/Go/"}]},{"title":"【ElasticSearch】Elasticsearch简介及其发展历史","date":"2023-02-28T11:53:28.000Z","path":"article/20230228.html","text":"Elasticsearch 是一种开源的搜索引擎,它采用 Lucene 库作为底层搜索引擎,并且通过 RESTful API 提供分布式搜索和分析功能。 Elasticsearch 能够快速地处理大量的结构化和非结构化数据,并提供了全文搜索、聚合分析、地理空间搜索等多种功能。 Elasticsearch 的发展历史可以追溯到 2010 年,当时 Shay Banon(现在是 Elasticsearch 公司的 CEO)开始开发一种名为 Compass 的搜索引擎。随着时间的推移,Compass 逐渐发展成为 Elasticsearch。Elasticsearch 在开源社区得到了广泛的关注和使用,并且逐渐成为一种非常流行的搜索引擎。 Elasticsearch 的一个典型应用场景是日志分析。例如,当一个公司的网络设备、服务器和应用程序发生故障时,日志数据通常会被记录下来。使用 Elasticsearch 可以帮助工程师快速地查找故障原因,例如确定哪些设备、服务器或应用程序出现了问题。Elasticsearch 还可以与 Kibana 等工具集成,使用户可以可视化地分析数据。 以下是 Elasticsearch 的一些主要特点: 分布式: Elasticsearch 是一种分布式搜索引擎,可以跨多台服务器分布式地存储和处理数据。这使得 Elasticsearch 具有高可用性、容错性和可扩展性。 实时搜索和分析: Elasticsearch 可以实时地搜索和分析数据,使用户可以及时地获得最新的结果。 全文搜索: Elasticsearch 可以对全文进行搜索,支持多种语言和复杂查询。 聚合分析: Elasticsearch 可以对数据进行聚合分析,例如计算平均值、最大值、最小值等等。 地理空间搜索: Elasticsearch 支持地理空间搜索,使用户可以在地图上搜索位置相关的信息。 总之,Elasticsearch 是一种功能强大、可靠的搜索引擎,它在搜索、分析和可视化数据方面都有广泛的应用。","tags":[{"name":"ElasticSearch","slug":"ElasticSearch","permalink":"https://alexhuihui.github.io/tags/ElasticSearch/"},{"name":"linux","slug":"linux","permalink":"https://alexhuihui.github.io/tags/linux/"}]},{"title":"【Java】JVM学习","date":"2023-02-24T01:11:44.000Z","path":"article/20230224.html","text":"JVM 的垃圾回收,其实就是收拾那些不再使用的 Java 对象,把他们曾经占用的内存重新释放出来。所以我们要搞清楚: 对象是如何创建的?对象是如何被访问的?到底哪些对象是废弃的?于是我们就需要搞清楚对象的生和死。 这些废弃了的对象到底放在哪?于是就需要了解JVM 的内存结构:方法区、堆、程序计数器、虚拟机栈和本地方法栈。 这些废弃了的对象会不会造成内存泄露(OOM,OutOfMemoryError)?于是我们就需要了解每个分区的 OOM。 这些废弃了对象什么时候被回收?于是我们就需要了解垃圾回收算法,比如说清除算法、复制算法、标记整理算法和分代收集算法。 知道了一个对象在内存中的生和死,我们还需要知道类是如何在内存中变成对象的?对象的方法是如何执行的? 于是我们开始学习 Java 虚拟机的执行过程,学习字节码文件( .class 文件),学习类的加载机制,学习虚拟机栈的栈帧结构,学习方法的调用过程,学习字节码指令等等。 为了监控虚拟机和故障排查,我们需要学习常用的 JDK 命令行工具,掌握必要的线上问题排查方法;此外,还需要了解 JIT (Just In Time)并不是简单的将热点代码编译成机器码就收工的,它还会对代码的执行进行优化(方法内联和逃逸分析)。 JVM 相关的知识已经成为面试必考的科目了,但老实讲,JVM 相关的知识还真的不太好用在项目中,或者说不太好在项目中体现出来。 那这里给大家推荐一个实战项目,基于 Spring Boot 的在线 Java IDE,可以远程执行 Java 代码并将程序的运行结果反馈出来。涉及了 Java 类文件的结构、Java 类加载器和 Java 类的热替换等 JVM 相关的技术。 https://github.com/TangBean/OnlineExecutor 听我这么一说,是不是一下子就清晰多了!","tags":[{"name":"Java","slug":"Java","permalink":"https://alexhuihui.github.io/tags/Java/"},{"name":"JVM","slug":"JVM","permalink":"https://alexhuihui.github.io/tags/JVM/"}]},{"title":"【ElasticSearch】docker安装es8和kibana8","date":"2022-12-10T02:30:41.000Z","path":"article/20221210.html","text":"本文介绍如何通过Docker安装elasticsearch 和 kibana 下载镜像123docker pull docker.elastic.co/elasticsearch/elasticsearch:8.5.2docker pull kibana:8.5.2 创建网络1docker network create elastic 启动es12345678910docker run -it \\--name elasticsearch \\--net elastic \\--restart=always \\-p 9200:9200 \\-p 9300:9300 \\-e "discovery.type=single-node" \\-e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \\docker.elastic.co/elasticsearch/elasticsearch:8.5.2 容器启动之后会打印密码和token,等会用于登录kibana 启动kibana123456docker run \\--name kibana \\--net elastic \\-p 5601:5601 \\-d \\kibana:8.5.2 访问等kibana容器启动成功之后,点击进入日志中输出的地址,然后按照提示依次输入token和密码就可以访问啦。 效果图:","tags":[{"name":"ElasticSearch","slug":"ElasticSearch","permalink":"https://alexhuihui.github.io/tags/ElasticSearch/"},{"name":"linux","slug":"linux","permalink":"https://alexhuihui.github.io/tags/linux/"},{"name":"docker","slug":"docker","permalink":"https://alexhuihui.github.io/tags/docker/"}]},{"title":"【系统设计】产品管理系统设计","date":"2022-12-02T02:26:02.000Z","path":"article/20221202.html","text":"","tags":[{"name":"系统设计","slug":"系统设计","permalink":"https://alexhuihui.github.io/tags/%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1/"},{"name":"mysql","slug":"mysql","permalink":"https://alexhuihui.github.io/tags/mysql/"}]},{"title":"【Kafka】Linux安装Kafka以及可视化界面","date":"2022-11-29T01:09:22.000Z","path":"article/20221129.html","text":"Kafka是当下非常流行的消息中间件,据官网透露,已有成千上万的公司在使用它。本文介绍如何在Linux上安装kafka以及Yahoo开源的一款可视化管理页面。 Kafka简介Kafka是由LinkedIn公司开发的一款开源分布式消息流平台,由Scala和Java编写。主要作用是为处理实时数据提供一个统一、高吞吐、低延迟的平台,其本质是基于发布订阅模式的消息引擎系统。 Kafka具有以下特性: 高吞吐、低延迟:Kafka收发消息非常快,使用集群处理消息延迟可低至2ms。 高扩展性:Kafka可以弹性地扩展和收缩,可以扩展到上千个broker,数十万个partition,每天处理数万亿条消息。 永久存储:Kafka可以将数据安全地存储在分布式的,持久的,容错的群集中。 高可用性:Kafka在可用区上可以有效地扩展群集,某个节点宕机,集群照样能够正常工作。 Kafka安装 首先我们需要下载Kafka的安装包,下载地址 下载完成后将Kafka解压到指定目录 123cd /opt/moduletar -zxvf kafka_2.13-3.3.1.tgzcd kafka_2.13-3.3.1 Kafka可以使用ZooKeeper或KRaft启动,使用任意一个即可,这里使用ZooKeeper。 启动Zookeeper服务,服务将运行在2181端口 12# 后台运行服务,并把日志输出到当前文件夹下的zookeeper-out.file文件中nohup bin/zookeeper-server-start.sh config/zookeeper.properties > zookeeper-out.file 2>&1 & 由于目前Kafka是部署在Linux服务器上的,外网如果想要访问,需要修改Kafka的配置文件config/server.properties,修改下Kafka的监听地址,否则会无法连接; 1234567# The address the socket server listens on. If not configured, the host name will be equal to the value of# java.net.InetAddress.getCanonicalHostName(), with PLAINTEXT listener name, and port 9092.# FORMAT:# listeners = listener_name://host_name:port# EXAMPLE:# listeners = PLAINTEXT://your.host.name:9092listeners=PLAINTEXT://:9092 最后启动Kafka服务,服务将运行在9092端口。 12# 后台运行服务,并把日志输出到当前文件夹下的kafka-out.file文件中nohup bin/kafka-server-start.sh config/server.properties > kafka-out.file 2>&1 & Kafka命令行操作 接下来我们使用命令行来操作下Kafka,熟悉下Kafka的使用。 首先创建一个叫consoleTopic的Topic; 1bin/kafka-topics.sh --create --topic consoleTopic --bootstrap-server 10.0.20.3:9092 接下来查看Topic; 1bin/kafka-topics.sh --describe --topic consoleTopic --bootstrap-server 10.0.20.3:9092 会显示如下Topic信息; 向Topic中发送消息: 1bin/kafka-console-producer.sh --topic consoleTopic --bootstrap-server 10.0.20.3:9092 直接在命令行中输入信息即可发送; 重新打开一个窗口,通过如下命令可以从Topic中获取消息: 1bin/kafka-console-consumer.sh --topic consoleTopic --from-beginning --bootstrap-server 10.0.20.3:9092 Kafka可视化 下载cmak的安装包,下载地址 必须通过jdk11来启动,参考文档 修改conf/application.conf 1234kafka-manager.zkhosts="127.0.0.1:2181"kafka-manager.zkhosts=${?ZK_HOSTS}cmak.zkhosts="127.0.0.1:2181"cmak.zkhosts=${?ZK_HOSTS} 启动 1nohup bin/cmak -java-home /opt/soft/jdk-11.0.17 > cmak-out.file 2>&1 & 配置nginx通过域名访问 123456789101112131415161718192021222324upstream kafka{ server localhost:9000; }server{ listen 80; server_name kafka.alexmmd.top; access_log /var/log/nginx/kafka-access.log main; if ( $host ~* "\\d+\\.\\d+\\.\\d+\\.\\d+" ) { return 400; } location / { proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://kafka; } error_page 500 502 503 504 /50x.html; location = /50x.html{ root html; }} 参考文档kafka的listeners的配置 kafka的安装","tags":[{"name":"Kafka","slug":"Kafka","permalink":"https://alexhuihui.github.io/tags/Kafka/"},{"name":"cmak","slug":"cmak","permalink":"https://alexhuihui.github.io/tags/cmak/"}]},{"title":"【Java】使用切面打印控制层出参入参","date":"2022-11-04T03:00:59.000Z","path":"article/20221104.html","text":"打印出参入参 1# 切面","tags":[{"name":"Java","slug":"Java","permalink":"https://alexhuihui.github.io/tags/Java/"},{"name":"Aop","slug":"Aop","permalink":"https://alexhuihui.github.io/tags/Aop/"}]},{"title":"【GitHub】使用Maven发布Java包","date":"2022-11-03T09:08:03.000Z","path":"article/20221103.html","text":"本文介绍如何通过GitHub的actions在代码推送到仓库时,将Java包发布到maven仓库的过程。 修改pom文件首先你要在项目的pom文件中添加要发布的仓库地址,我们准备发到github package上 12345678<!-- 发布maven私服 --> <distributionManagement> <repository> <id>github</id> <name>GitHub OWNER Apache Maven Packages</name> <url>https://maven.pkg.github.com/Alexhuihui/alex-common</url> </repository> </distributionManagement> 在项目根目录下新建.github/workflows/maven-publish.yml1234567891011121314151617181920212223# This workflow will build a package using Maven and then publish it to GitHub packages when a release is created# For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#apache-maven-with-a-settings-pathname: Maven Packageon: [push]jobs: publish: runs-on: ubuntu-latest permissions: contents: read packages: write steps: - uses: actions/checkout@v2 - uses: actions/setup-java@v2 with: java-version: '8' distribution: 'temurin' - name: Publish package run: mvn --batch-mode deploy env: GITHUB_TOKEN: ${{ secrets.REPOSITORY_TOKEN }} 申请自己的accessToken进入自己github,然后在设置下面的开发者设置中申请一个新的personal access token 配置加密机密进入自己的仓库,在设置下面的机密中的工作流中新建一个机密。 参考发布包到 Maven 中心仓库和 GitHub Packages","tags":[{"name":"actions","slug":"actions","permalink":"https://alexhuihui.github.io/tags/actions/"},{"name":"maven","slug":"maven","permalink":"https://alexhuihui.github.io/tags/maven/"},{"name":"Java","slug":"Java","permalink":"https://alexhuihui.github.io/tags/Java/"}]},{"title":"【项目】使用python做数据迁移","date":"2022-10-28T02:57:08.000Z","path":"article/20221028.html","text":"工作中经常会遇到重构老项目,所以需要迁移之前的数据库到新的数据库中,为此我使用python来做数据迁移脚本。 初始版本第一个版本的数据迁移脚本,主要是使用单条sql的插入,效率偏低。 先获取原始数据读取到内存中 1234567def get_original(): connect = Connect.get_original_connect() select_sql = '''select * from table''' cursor = connect.cursor() cursor.execute(select_sql) # 获取所有记录列表 return cursor.fetchall() 再在main函数中处理原始数据,调用插入函数,插入到新数据库中 123456789def main(): original_list = get_original() a = 0 for original in original_list: print(original) insert_into_target(original) print('插入数据成功' + str(original[0])) a = a + 1 print(a) 插入数据 1234567891011def insert_into_usr(mbr): connect = Connect.get_user_connect() cursor = connect.cursor() id = mbr[0] name = mbr[1] value_lo = ((id, name)) insert_sql = '''insert into usr_user(id, name) values (%s, %s)''' cursor.execute(insert_sql, value_lo) connect.commit() cursor.close() 第一个版本的脚本易于理解,代码简单,但是由于是单条插入,速度很慢。所以接下来我们改进一下脚本。 改进版改进思路就是由单条插入改成批量插入executemany 第一步先获取原始数据读取到内存中 1234567def get_original(): connect = Connect.get_original_connect() select_sql = '''select * from table''' cursor = connect.cursor() cursor.execute(select_sql) # 获取所有记录列表 return cursor.fetchall() 定义全局数组变量,然后在main函数中调用插入函数拼接好数据往这个全局变量中插入,然后到一定次数后调用数据库批量插入数据 123456789101112131415161718192021222324252627282930313233343536target_data = []def main(): original_list = get_original() a = 0 for i in range(len(original_list)): original = original_list[i] insert_into_target(original) if i == (len(original_list) - 1): do_insert_target() a = a + 1 if i != 0 and i % 1000 == 0: do_insert_target() a = a + 1 print(a) def do_insert_target(): connect = Connect.get_user_connect() cursor = connect.cursor() insert_sql = '''insert into target(id, name) values (%s, %s)''' cursor.executemany(insert_sql, target_data) connect.commit() cursor.close() print('插入数据成功' + str(len(target_data))) user_data.clear()def insert_into_target(mbr): id = mbr[0] name = mbr[1] value_lo = (id, name) target_data.append(value_lo) 总结遇到数据量很大的情况,使用批量插入能极大程度提高数据迁移的速度,但要做好数据防丢失的措施。","tags":[{"name":"python","slug":"python","permalink":"https://alexhuihui.github.io/tags/python/"}]},{"title":"【Kubernetes】使用minikube","date":"2022-05-07T14:05:44.000Z","path":"article/20220507.html","text":"Kubernetes使用minikube安装官网地址 在linux上安装 using binary download 12curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64sudo install minikube-linux-amd64 /usr/local/bin/minikube Start a cluster using the docker driver 1minikube start --driver=docker 启动集群不能用root启动,新增一个test用户加入到sudo组里,再把它加入到docker的用户组中 12su testminikube start 与集群交互123minikube kubectl -- get po -Aalias kubectl="minikube kubectl --"minikube dashboard 部署应用12kubectl create deployment hello-minikube --image=k8s.gcr.io/echoserver:1.4kubectl expose deployment hello-minikube --type=NodePort --port=8080 简述NodePort前言最近在学习Kubernetes的过程中,由于都是在K8s集群内部进行Docker通信的,就遇到了如何暴露服务给外界访问的问题,生产环境比较好的方案就是借助云服务商使用LoadBalancer的方式,但由于是测试环境就使用了比较简单的NodePort来暴露服务,在实践过程中,也加深了对K8s概念的理解。 Service把一组Pods抽象为网络服务,通过K8s你不需要通过修改程序的服务发现机制来管理通信。K8s给每个Pods独立的IP,以及给一组Pods一个DNS并通过负载均衡的方式进行访问。 动机K8s的Pods是有生命周期的,通常是可以被创建和销毁的,然后销毁之后就不会再启动了。如果采用的是Deployment,则可以动态的创建和销毁Pod。 每个Pod都有自己的IP,但是在Deployment中创建的Pod,先创建的可能会和后面创建Pod的IP地址不同。 那么就会出现一个问题,如果一组Pod为服务调用方,一组Pod为服务提供方,服务调用者怎么找到服务提供者的地址? Service 资源Kubernetes的Service定义了一种抽象:逻辑上的一组Pod,一种可以访问它们的方式。这一组Pod能通过Service被访问到,通过是通过Selector来实现的。 举个例子,如果后台有三个节点提供图片访问服务,调用者可以通过Service进行访问,它不需要知道具体访问的是哪一个节点,具体的策略由Service来配置,并实现负载均衡。某种意义上也是服务发现和解耦。 nodePort外部流量访问K8s的一种方式,即nodeIP:nodePort,是提供给外部流量访问K8s集群资源的一种方式。 例如需要暴露服务的端口给外界访问的话可以通过命令: 1kubectl expose deployment nginx --type=NodePort 可以随机暴露出一个端口外部访问的端口(默认值:30000-32767)出来。由于暴露的端口往往都比较大,这时候可以采用nginx反向代理的方式,为外界提供访问服务(HTTP:80,HTTPS:443)。 除了使用命令之外也可以使用yaml配置文件的方式进行服务的配置,如下所示: 12345678910111213apiVersion: v1kind: Servicemetadata: name: webapp-servicespec: type: NodePort selector: app: webapp ports: - protocol: TCP port: 3000 targetPort: 3000 nodePort: 30100 然后如果是一些内部的服务,比如数据库服务,或者除了网关以外的微服务,这些服务是不需要外部访问的。因此没有必要设置nodePort属性。 portK8s集群内部服务访问service的入口。是service暴露在Cluster上的端口,ClusterIP:Port。 targetPort容器的端口,也是最终底层的服务所提供的端口,所以说targetPod也就是Pod的端口。从port或者是nodePort进入的流量,经过路由转发之后,最终都会都通过targetPort进入到Pod中。 总结总体来说,除了targetPort是容器本身的端口之外,port和nodePod都是Service的端口。不同的是port是暴露给K8s访问的,nodePort是暴露给外部访问的。 将 minikube 的服务暴露到宿主机外minikube 是一款基于 Kubernetes 的定位于快速验证功能的小型容器编排环境。 由于它的定位特性,我们在使用中会发现 minikube 虚拟出了一个 IP 作为自身的节点 IP,该 IP 和宿主机不同。对于 NodePort 类型的 Service 也没有办法通过 127.0.0.1 访问。 Host 内访问可以通过 <minikube-ip>:<service-port> 来访问 service Host 外访问通过 minikube 可以很方便地在本机访问,同时避免了对宿主机端口的占用。但是也带来了另一个问题:无法直接通过访问宿主机的端口来访问 services 进行调试。 比如我的实验机是一台云服务虚拟机,我在自己的电脑上可以访问这台虚拟机,但是不能访问虚拟机上 minikube 暴露的服务。 虽然没有办法让 minikube 直接通过宿主机端口对外暴露 services,但是如果我们把问题换个角度思考,就很容易找到解决办法:如何将一个 service(不特定类型)暴露到本机。 这时候最简单的办法就是通过 kubectl port-forward 转发端口。 我有一台web服务,service监听3000端口(port),我希望暴露在宿主机上的8888端口,就可以使用如下命令: 1kubectl port-forward --address 0.0.0.0 service/webapp-service 8888:3000 kubectl操作命令伸缩pod 1kubectl scale deployment my-nginx --replicas=0 清除 Deployment、ReplicaSet 和 Pod 12kubectl delete services frontend backendkubectl delete deployment frontend backend","tags":[{"name":"Kubernetes","slug":"Kubernetes","permalink":"https://alexhuihui.github.io/tags/Kubernetes/"},{"name":"minikube","slug":"minikube","permalink":"https://alexhuihui.github.io/tags/minikube/"}]},{"title":"【spring-authorization-server】基于spring-authorization-server实现Oauth2授权流程","date":"2022-04-10T09:31:06.000Z","path":"article/20220410.html","text":"","tags":[{"name":"oauth2","slug":"oauth2","permalink":"https://alexhuihui.github.io/tags/oauth2/"},{"name":"spring-authorization-server","slug":"spring-authorization-server","permalink":"https://alexhuihui.github.io/tags/spring-authorization-server/"}]},{"title":"【Kubernetes】kubernetes-tutorial","date":"2022-04-05T09:34:23.000Z","path":"article/20220405.html","text":"Kubernetes介绍它是一款由Google开发并开源的容器编排工具,帮助你在不同的部署环境中管理容器化应用。","tags":[{"name":"Kubernetes","slug":"Kubernetes","permalink":"https://alexhuihui.github.io/tags/Kubernetes/"}]},{"title":"【Docker】docker tutorial","date":"2022-04-04T03:53:36.000Z","path":"article/20220404.html","text":"docker 是什么 Docker is a tool for running applications in an isolated Environment Similar to VM App run in same environment Just works Standard for software deployment containers容器是一个应用层的抽象,可以打包代码和依赖。多个容器可以运行在同一台机器并共享一个OS内核,每个容器都是运行在单独的进程中的。 VMVM是一个物理硬件的一个抽象,把一台服务变成了多个服务。每个VM都是对OS的完整复制,占用空间更大,启动更慢。 pulling nginx image123456# 拉取镜像docker pull nginx# 展示所有镜像docekr images# 运行容器docker run --name website -d -p 3000:80 -p 8080:80 nginx:latest format docker ps12345docker ps --format="ID\\t{{.ID}}\\nNAME\\t{{.Names}}\\nIMAGE\\t{{.Image}}\\nPORTS\\t{{.Ports}}\\nCOMMAND\\t{{.Command}}\\nCREATED\\t{{.CreatedAt}}\\nSTATUS\\t{{.Status}}\\n"export Format="ID\\t{{.ID}}\\nNAME\\t{{.Names}}\\nIMAGE\\t{{.Image}}\\nPORTS\\t{{.Ports}}\\nCOMMAND\\t{{.Command}}\\nCREATED\\t{{.CreatedAt}}\\nSTATUS\\t{{.Status}}\\n"docker ps --format=$Format Volumes允许宿主机和容器、容器和容器之间相互共享数据包括文件或者文件夹。 1docker run --name website -v $(pwd):/usr/share/nginx/html:ro -d -p 8080:80 nginx:latest 容器之间共享数据 1docker run --name website_copy --volumes-from website -d -p 8081:80 nginx DockerfileBuild own image docker官网文档 在项目根目录创建 Dockerfile 文件 12FROM nginx:latestADD . /usr/share/nginx/html 12docker build --tag website:latest .docker run --name website -d -p 8080:80 website:latest 构建 user-service-api 12345FROM node:latestWORKDIR /appADD . .RUN npm installCMD node index.js 编写 .dockerignore 123node_modulesDockerfile.git 使用缓存,只改变源码的情况下不用重新下载依赖 123456FROM node:latestWORKDIR /appADD package*.json .RUN npm installADD . .CMD node index.js Alpine选择镜像tag里包含alpine去构建,会大大的减少镜像体积 Tags and VersionUsage: docker tag SOURCE_IMAGE[:TAG] TARGET_IMAGE[:TAG] Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE Docker Registries注册 hub.docker.com 账号,创建仓库,把本地的镜像通过tag命令指定成远程仓库的名称,然后推送上去。 Docker Inspect123docker inspect name/iddocker logs name/iddocker exec -it name/id /bash","tags":[{"name":"Docker","slug":"Docker","permalink":"https://alexhuihui.github.io/tags/Docker/"}]},{"title":"【项目】使用python生成anki文件并上传到iCloud中","date":"2022-03-11T08:45:55.000Z","path":"article/20220311.html","text":"这周开始看 java-design-partterns的时候经常在例子中看到很多不认识的单词,就想着把每次遇到的不认识的单词记录下来发送到手机里,可以随时复习。经过调研,发现python都有现成的库可以使用,使用genanki生成anki文件,再使用pyicloud登录自己的icloud账号,上传文件到icloud drive中,最后你就能通过anki 备忘录之类的软件从icloud drive中导入你的文件了。 使用pyicloud上传文件从配置文件读取你的apple账号密码,在初始化的方法中登录。对外暴露一个上传文件的接口,默认使用anki文件夹存储。 12345678910111213141516from pyicloud import PyiCloudServiceclass Cloud(object): def __init__(self): username = '' password = '' for line in open('account.txt', 'r', encoding='utf-8'): lines = line.split(' ') username = lines[0] password = lines[1] self.api = PyiCloudService(username, password) def upload_file(self, file_path): with open(file_path, 'rb') as file_in: self.api.drive['anki'].upload(file_in) 使用genanki生成anki文件比较简单,先定义模板model,然后再把单词的中英文填入模板生成一个个的note,最后再使用deck写入文件(也可以写入数据库)。 效果图上传的效果 导入之后的效果 源码地址https://github.com/Alexhuihui/anki-icloud.git","tags":[{"name":"genanki","slug":"genanki","permalink":"https://alexhuihui.github.io/tags/genanki/"},{"name":"anki","slug":"anki","permalink":"https://alexhuihui.github.io/tags/anki/"},{"name":"pyicloud","slug":"pyicloud","permalink":"https://alexhuihui.github.io/tags/pyicloud/"}]},{"title":"【Go语言学习】02-数据类型","date":"2022-03-02T03:10:37.000Z","path":"article/20220302.html","text":"数组Go 的数组类型包含两个重要属性:元素的类型和数组长度(元素的个数)。这两个属性也直接构成了 Go 语言中数组类型变量的声明: 123var arr [N]Tvar arr = [6]int{1, 2, 3, 4, 5, 6} 数组类型变量是一个整体,这就意味着一个数组变量表示的是整个数组。这点与 C 语言完全不同,在 C 语言中,数组变量可视为指向数组第一个元素的指针。这样一来,无论是参与迭代,还是作为实际参数传给一个函数 / 方法,Go 传递数组的方式都是纯粹的值拷贝,这会带来较大的内存拷贝开销。 切片切片的定义比数组少了一个长度的声明 1var nums = []int{1, 2, 3, 4, 5, 6} Go 值传递的机制让数组在各个函数间传递起来比较“笨重”,开销较大,且开销随数组长度的增加而增加。为了解决这个问题,Go 引入了切片这一不定长同构数据类型。 slice可以看做数组的文件描述符。它由三部分组成:指向底层数组的指针、当前大小和总容量。它还可以进行动态扩容,但是要小心它的底层数组在扩容的时候是复制到新的数组中的。 切片的内部结构 123456type slice struct { array unsafe.Pointer len int cap int} array: 是指向底层数组的指针; len: 是切片的长度,即切片中当前元素的个数; cap: 是底层数组的长度,也是切片的最大容量,cap 值永远大于等于 len 值。 创建切片的方法: 方法一:通过 make 函数来创建切片,并指定底层数组的长度。我们直接看下面这行代码: 1sl := make([]byte, 6, 10) // 其中10为cap值,即底层数组长度,6为切片的初始长度 方法二:采用 array[low : high : max]语法基于一个已存在的数组创建切片。这种方式被称为数组的切片化,比如下面代码: 12arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}sl := arr[3:7:9]","tags":[{"name":"Go","slug":"Go","permalink":"https://alexhuihui.github.io/tags/Go/"}]},{"title":"【数据结构】线性表","date":"2022-02-28T02:35:13.000Z","path":"article/20220228.html","text":"线性表就是数据排成像一条线一样的结构。每个线性表上的数据最多只有前和后两个方向 除了数组,链表、队列、栈等也是线性表结构。 数组拥有连续的内存空间和相同类型的数据,能够随机访问,寻址公式: 1a[i]_address = base_address + i * data_type_size 插入和删除低效 回文字符串判断 1234567891011121314151617181920212223242526272829303132public class PalindromeChecker { public static void main(String[] args) { String str = "level"; if (isPalindrome(str)) { System.out.println(str + " is a palindrome."); } else { System.out.println(str + " is not a palindrome."); } } // 判断字符串是否是回文字符串的方法 public static boolean isPalindrome(String str) { // 移除字符串中的非字母和数字的字符 String cleanStr = str.replaceAll("[^a-zA-Z0-9]", "").toLowerCase(); // 检查是否为回文 int left = 0; int right = cleanStr.length() - 1; while (left < right) { if (cleanStr.charAt(left) != cleanStr.charAt(right)) { return false; } left++; right--; } return true; }} 链表链表有单链表、循环链表和双向链表,插入和删除比较快 LRU缓存淘汰算法 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182import java.util.HashMap;import java.util.Map;class LRUCache<K, V> { private final int capacity; private final Map<K, Node<K, V>> cache; private final Node<K, V> head; public LRUCache(int capacity) { this.capacity = capacity; this.cache = new HashMap<>(); this.head = new Node<>(null, null); } public V get(K key) { if (cache.containsKey(key)) { Node<K, V> node = cache.get(key); moveToHead(node); return node.value; } return null; } public void put(K key, V value) { if (cache.containsKey(key)) { Node<K, V> node = cache.get(key); node.value = value; moveToHead(node); } else { if (cache.size() >= capacity) { removeTail(); } Node<K, V> newNode = new Node<>(key, value); cache.put(key, newNode); addToHead(newNode); } } private void moveToHead(Node<K, V> node) { removeNode(node); addToHead(node); } private void removeTail() { Node<K, V> prev = head; while (prev.next.next != null) { prev = prev.next; } Node<K, V> tail = prev.next; cache.remove(tail.key); prev.next = null; } private void removeNode(Node<K, V> node) { Node<K, V> prev = head; while (prev.next != null && prev.next != node) { prev = prev.next; } if (prev.next != null) { prev.next = node.next; } } private void addToHead(Node<K, V> node) { node.next = head.next; head.next = node; } private static class Node<K, V> { K key; V value; Node<K, V> next; Node(K key, V value) { this.key = key; this.value = value; this.next = null; } }}","tags":[{"name":"数组","slug":"数组","permalink":"https://alexhuihui.github.io/tags/%E6%95%B0%E7%BB%84/"},{"name":"链表","slug":"链表","permalink":"https://alexhuihui.github.io/tags/%E9%93%BE%E8%A1%A8/"},{"name":"栈","slug":"栈","permalink":"https://alexhuihui.github.io/tags/%E6%A0%88/"},{"name":"队列","slug":"队列","permalink":"https://alexhuihui.github.io/tags/%E9%98%9F%E5%88%97/"}]},{"title":"【ZooKeeper】The ZooKeeper Data Model","date":"2022-02-25T08:54:06.000Z","path":"article/20220225.html","text":"zookeeper拥有多层的命名空间,就像一个分布式文件系统。唯一的不同就是它的每个节点都可以包含数据,就像文件系统中允许一个文件可以作为目录。它没有相对路径。 ZNodeszookeeper中每个节点都都被叫做znode。它包含了数据本身、数据变更的版本和访问控制(acl)的版本 Watches客户端可以在znodes上设置监控,当znode出发了监控,zookeeper就会向客户端发送一个通知,然后清除监控。 Data Access数据在每个节点中的读写操作都是原子的,每个节点都有一个 Access Control List (ACL)来决定谁可以干什么。 Ephemeral Nodeszookeeper可以创建临时性的节点,这些节点只有在session创建和结束期间才是活跃的,可以通过getEphemerals() 获取当前session创建的临时节点。 Sequence Nodes有序的节点,拥有唯一的名字。 Container Nodes容器节点是专门用来作为leader或者lock。当容器的最后一个节点被删除,容器就会被服务端在未来的某个时间删除。 TTL Nodes当创建永久或永久有序的节点,你可以选择设置过期时间。 Time in ZooKeeper","tags":[{"name":"ZooKeeper","slug":"ZooKeeper","permalink":"https://alexhuihui.github.io/tags/ZooKeeper/"}]},{"title":"【spring-authorization-server】样例说明","date":"2022-02-23T07:21:05.000Z","path":"article/20220223.html","text":"介绍前段时间spring-security团队推出了新的基于Oauth2.0的授权服务器实现,叫做spring-authorization-server,让我们来探究一下里面example。 client : 客户端首先向授权服务器的授权节点(Authorization endpoint)发起授权请求,通过@RegisteredOAuth2AuthorizedClient(“messaging-client-authorization-code”) 1OAuth2AuthorizedClientArgumentResolver 获取注解里面定义的 clientRegistrationId = messaging-client-authorization-code 1DefaultOAuth2AuthorizedClientManager 在authorize()中去寻找OAuth2AuthorizedClient,发现没有之后再去寻找ClientRegistration,在InMemoryClientRegistrationRepository中可以找到clientRegistrationId对应的ClientRegistration。 然后用这个clientRegistration构建OAuth2AuthorizationContext,再使用DelegatingOAuth2AuthorizedClientProvider去调用authorizedClientProviders中的authorize() 1AuthorizationCodeOAuth2AuthorizedClientProvider 抛出ClientAuthorizationRequiredException被OAuth2AuthorizationRequestRedirectFilter捕获 然后返回一个重定向地址:http://auth-server:9000/oauth2/authorize?response_type=code&client_id=messaging-client&scope=message.read%20message.write&state=NVdF9Kzi80STPhHHHs9BTv9oAJlODlzpFEFVtl_TI6g%3D&redirect_uri=http://127.0.0.1:8080/authorized auth-server: 1OAuth2AuthorizationEndpointFilter 处理授权请求,发现没有认证,然后就会经过DelegatingAuthenticationEntryPoint选择对应的LoginUrlAuthenticationEntryPoint返回401状态码","tags":[{"name":"oauth2","slug":"oauth2","permalink":"https://alexhuihui.github.io/tags/oauth2/"},{"name":"spring-authorization-server","slug":"spring-authorization-server","permalink":"https://alexhuihui.github.io/tags/spring-authorization-server/"}]},{"title":"【gRPC】Java和Python使用gRPC","date":"2022-02-10T10:35:10.000Z","path":"article/20220210.html","text":"使用Java搭建gRPC服务端1、如何配置Maven方式一: io.grpc grpc-all 1.26.0 方式二: 12345678910111213141516171819202122<dependency> <groupId>io.grpc</groupId> <artifactId>grpc-netty-shaded</artifactId> <version>1.44.0</version> <scope>runtime</scope></dependency><dependency> <groupId>io.grpc</groupId> <artifactId>grpc-protobuf</artifactId> <version>1.44.0</version></dependency><dependency> <groupId>io.grpc</groupId> <artifactId>grpc-stub</artifactId> <version>1.44.0</version></dependency><dependency> <!-- necessary for Java 9+ --> <groupId>org.apache.tomcat</groupId> <artifactId>annotations-api</artifactId> <version>6.0.53</version> <scope>provided</scope></dependency> 方式三: io.grpc grpc-netty 1.26.0 io.grpc grpc-protobuf 1.26.0 io.grpc grpc-stub 1.26.0 三种方式的区别:a.方式一会把与gRPC相关的所有jar都引入,不管实际项目中是否会用到。引入相关jar较多,但其配置简单。方式三只会引入与gRPC和netty相关的jar,如果要使用OkHttp,则要再引入。配置相对较多,但引入jar相对较少。方式二引入的jar的数量介于方式一与方式三之间。 b.方式一中netty是以原始jar引用的。这样方便在使用的过程中查看netty源代码,方便开发。方式二中netty被放入grpc-netty-shaded-1.26.0.jar的io.grpc.netty.shaded包中。这样的好处是当项目中有多个模块使用了不同版本的netty时,gRPC功能不会受到影响。但坏处就是不方便查看netty源代码了。方式三与方式一相同,但不会引入像OkHttp这样与netty与gRPC不相关的jar。 总之,方式一和方式三都适合用在只需要使用单一netty版本的环境,方式二适合用在多个netty版本共存的环境 。 2、编写protobuf文件在idea的插件市场下载安装protobuf的插件 创建src/main/proto/rpc_date.proto 12345678910111213141516171819202122232425syntax = "proto3"; // 协议版本// 选项配置option java_package = "top.alexmmd.grpc.api";option java_outer_classname = "RPCDateServiceApi";option java_multiple_files = true;// 定义包名package top.alexmmd.grpc.api;// 服务接口.定义请求参数和相应结果service RPCDateService { rpc getDate (RPCDateRequest) returns (RPCDateResponse) { }}// 定义请求体message RPCDateRequest { string userName = 1;}// 定义相应内容message RPCDateResponse { string serverDate = 1;} 使用maven插件进行编译 把target目录中生成的文件复制到java目录中 3、编写接口实现类12345678910111213141516171819202122232425// RPCDateServiceGrpc.RPCDateServiceImplBase 这个就是接口.// RPCDateServiceImpl 我们需要继承他的,实现方法回调public class RPCDateServiceImpl extends RPCDateServiceGrpc.RPCDateServiceImplBase { @Override public void getDate(RPCDateRequest request, StreamObserver<RPCDateResponse> responseObserver) { System.out.println("request = " + request); // 请求结果,我们定义的 RPCDateResponse rpcDateResponse = null; String userName = request.getUserName(); String response = String.format("你好: %s, 今天是%s.", userName, LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))); try { // 定义响应,是一个builder构造器. rpcDateResponse = RPCDateResponse .newBuilder() .setServerDate(response) .build(); } catch (Exception e) { responseObserver.onError(e); } finally { // 这种写法是observer,异步写法,老外喜欢用这个框架. responseObserver.onNext(rpcDateResponse); } responseObserver.onCompleted(); }} 4、定义服务端12345678910111213public class GRPCServer { private static final int port = 9999; public static void main(String[] args) throws Exception { // 设置service接口. Server server = ServerBuilder. forPort(port) .addService(new RPCDateServiceImpl()) .build().start(); System.out.println(String.format("GRpc服务端启动成功, 端口号: %d.", port)); server.awaitTermination(); }} 5、定义客户端123456789101112131415161718192021222324public class GRPCClient { private static final String host = "localhost"; private static final int serverPort = 9999; public static void main(String[] args) throws Exception { // 1. 拿到一个通信的channel ManagedChannel managedChannel = ManagedChannelBuilder.forAddress(host, serverPort).usePlaintext().build(); try { // 2.拿到道理对象 RPCDateServiceGrpc.RPCDateServiceBlockingStub rpcDateService = RPCDateServiceGrpc.newBlockingStub(managedChannel); RPCDateRequest rpcDateRequest = RPCDateRequest .newBuilder() .setUserName("anthony") .build(); // 3. 请求 RPCDateResponse rpcDateResponse = rpcDateService.getDate(rpcDateRequest); // 4. 输出结果 System.out.println(rpcDateResponse.getServerDate()); } finally { // 5.关闭channel, 释放资源. managedChannel.shutdown(); } }} 使用Python搭建gRPC客户端1、环境配置 protobuf 运行时(runtime) 1pip install grpcio 安装 python 下的 protoc 编译器 1pip install grpcio-tools 2、编译proto文件1python -m grpc_tools.protoc --python_out=. --grpc_python_out=. -I. rpc_date.proto 3、编写客户端12345678910111213141516import grpcimport rpc_date_pb2import rpc_date_pb2_grpcdef run(): # 连接 rpc 服务器 channel = grpc.insecure_channel('127.0.0.1:9999') # 调用 rpc 服务 stub = rpc_date_pb2_grpc.RPCDateServiceStub(channel) response = stub.getDate(rpc_date_pb2.RPCDateRequest(userName='czl')) print(response)if __name__ == '__main__': run() 最后proto文件中的包名也要保持一致,最好不要改动","tags":[{"name":"gRPC","slug":"gRPC","permalink":"https://alexhuihui.github.io/tags/gRPC/"}]},{"title":"【Go语言学习】01 | 初窥门径","date":"2022-01-25T07:25:18.000Z","path":"article/20220125.html","text":"创建hello, world程序创建hello, world文件夹12345$mkdir ~/goprojects // 创建一个可以集合所有专栏项目的根文件夹$cd ~/goprojects$mkdir helloworld // 创建存储helloworld示例的文件夹$cd helloworld 编写和运行第一个Go程序首先,我们需要创建一个名为 main.go 的源文件。 Go 源文件总是用全小写字母形式的短小单词命名,并且以.go 扩展名结尾。 如果要在源文件的名字中使用多个单词,我们通常直接是将多个单词连接起来作为源文件名,而不是使用其他分隔符,比如下划线。也就是说,我们通常使用 helloworld.go 作为文件名而不是 hello_world.go。 12345678package mainimport "fmt"func main() { fmt.Println("hello, world")} 保存文件,编译和运行 1234$go build main.go$./mainhello, world hello, world程序的结构上述代码可以分成三部分剖析: package main 这一行代码定义了 Go 中的一个包 package。包是 Go 语言的基本组成单元,通常使用单个的小写单词命名,一个 Go 程序本质上就是一组包的集合。所有 Go 代码都有自己隶属的包,在这里我们的“hello,world”示例的所有代码都在一个名为 main 的包中。main 包在 Go 中是一个特殊的包,整个 Go 程序中仅允许存在一个名为 main 的包 main 函数 1234func main() { fmt.Println("hello, world")} 这里的 main 函数会比较特殊:当你运行一个可执行的 Go 程序的时候,所有的代码都会从这个入口函数开始运行。 import “fmt” import “fmt” 一行中“fmt”代表的是包的导入路径(Import),它表示的是标准库下的 fmt 目录,整个 import 声明语句的含义是导入标准库 fmt 目录下的包; fmt.Println 函数调用一行中的“fmt”代表的则是包名。 导入多个包可以使用圆括号 1234import ( "fmt" "strings") Go语言中程序是怎么编译的Go 是一种编译型语言,这意味着只有你编译完 Go 程序之后,才可以将生成的可执行文件交付于其他人,并运行在没有安装 Go 的环境中。 1go build main.go 当然,Go 也借鉴了动态语言的一些对开发者体验较好的特性,比如基于源码文件的直接执行,Go 提供了 run 命令可以直接运行 Go 源码文件,比如我们也可以使用下面命令直接基于 main.go 运行: 1go run main.go 复杂项目下Go程序的编译我们新建一个项目: 1234$cd ~/goprojects$mkdir hellomodule$cd hellomodule 在hellomodule文件夹中创建并编辑main.go文件 123456789101112131415161718192021package mainimport ( "github.com/valyala/fasthttp" "go.uber.org/zap")var logger *zap.Loggerfunc init() { logger, _ = zap.NewProduction()}func fastHTTPHandler(ctx *fasthttp.RequestCtx) { logger.Info("hello, go module", zap.ByteString("uri", ctx.RequestURI()))}func main() { fasthttp.ListenAndServe(":9090", fastHTTPHandler)} 对于复杂项目的构建,我们采用 Go module Go module 构建模式是在 Go 1.11 版本正式引入的,为的是彻底解决 Go 项目复杂版本依赖的问题,在 Go 1.16 版本中,Go module 已经成为了 Go 默认的包依赖管理机制和 Go 源码构建机制。 Go Module 的核心是一个名为 go.mod 的文件,在这个文件中存储了这个 module 对第三方依赖的全部信息。 通过 init来创建go.mod文件: 12345$go mod init github.com/alexhuihui/hellomodulego: creating new go.mod: module github.com/bigwhite/hellomodulego: to add module requirements and sums: go mod tidy 让我们看看生成的go.mod文件中有什么: 12345$cat go.modmodule github.com/alexhuihui/hellomodulego 1.17 其实,一个 module 就是一个包的集合,这些包和 module 一起打版本、发布和分发。go.mod 所在的目录被我们称为它声明的 module 的根目录。不过呢,这个时候的 go.mod 文件内容还比较简单,第一行内容是用于声明 module 路径(module path)的。而且,module 隐含了一个命名空间的概念,module 下每个包的导入路径都是由 module path 和包所在子目录的名字结合在一起构成。比如,如果 hellomodule 下有子目录 pkg/pkg1,那么 pkg1 下面的包的导入路径就是由 module path(github.com/alexhuihui/hellomodule)和包所在子目录的名字(pkg/pkg1)结合而成,也就是 github.com/alexhuihui/hellomodule/pkg/pkg1。另外,go.mod 的最后一行是一个 Go 版本指示符,用于表示这个 module 是在某个特定的 Go 版本的 module 语义的基础上编写的。 通过 go mod tidy,让 Go工具自动添加依赖 1234567891011121314151617181920$ go mod tidygo: finding module for package go.uber.org/zapgo: finding module for package github.com/valyala/fasthttpgo: downloading go.uber.org/zap v1.20.0go: downloading github.com/valyala/fasthttp v1.32.0go: found github.com/valyala/fasthttp in github.com/valyala/fasthttp v1.32.0go: found go.uber.org/zap in go.uber.org/zap v1.20.0go: downloading go.uber.org/atomic v1.7.0go: downloading go.uber.org/multierr v1.6.0go: downloading github.com/pkg/errors v0.8.1go: downloading github.com/stretchr/testify v1.7.0go: downloading go.uber.org/goleak v1.1.11go: downloading github.com/andybalholm/brotli v1.0.2go: downloading github.com/klauspost/compress v1.13.4go: downloading github.com/valyala/bytebufferpool v1.0.0go: downloading gopkg.in/yaml.v2 v2.2.8go: downloading github.com/davecgh/go-spew v1.1.1go: downloading github.com/pmezard/go-difflib v1.0.0go: downloading gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307bgo: downloading github.com/benbjohnson/clock v1.1.0 下载依赖的时候你可能会碰上超时的问题,执行以下命令即可: 1$ go env -w GOPROXY=https://goproxy.cn,direct go mod tidy执行之后,go.mod就变成了这个样子: 12345678910111213141516module github.com/bigwhite/hellomodulego 1.17require ( github.com/valyala/fasthttp v1.32.0 go.uber.org/zap v1.20.0)require ( github.com/andybalholm/brotli v1.0.2 // indirect github.com/klauspost/compress v1.13.4 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.6.0 // indirect) 这个时候,go.mod 已经记录了 hellomodule 直接依赖的包的信息。不仅如此,hellomodule 目录下还多了一个名为 go.sum 的文件,这个文件记录了 hellomodule 的直接依赖和间接依赖包的相关版本的 hash 值,用来校验本地包的真实性。在构建的时候,如果本地依赖包的 hash 值与 go.sum 文件中记录的不一致,就会被拒绝构建。 有了 go.mod 以及 hellomodule 依赖的包版本信息后,我们再来执行构建: 1234$go build main.go$lsgo.mod go.sum main main.go 成功构建后,执行main命令,然后新开一个窗口执行curl localhost:9090/foo/bar 123$ ./main {"level":"info","ts":1643097830.6936307,"caller":"hellomodule/main.go:16","msg":"hello, go module","uri":"/foo/bar"} 总结 Go 包是 Go 语言的基本组成单元。一个 Go 程序就是一组包的集合,所有 Go 代码都位于包中; Go 源码可以导入其他 Go 包,并使用其中的导出语法元素,包括类型、变量、函数、方法等,而且,main 函数是整个 Go 应用的入口函数; Go 源码需要先编译,再分发和运行。如果是单 Go 源文件的情况,我们可以直接使用 go build 命令 +Go 源文件名的方式编译。不过,对于复杂的 Go 项目,我们需要在 Go Module 的帮助下完成项目的构建。","tags":[{"name":"Go","slug":"Go","permalink":"https://alexhuihui.github.io/tags/Go/"}]},{"title":"【微信机器人】(一)CentOS7安装wechaty运行环境","date":"2022-01-24T11:08:45.000Z","path":"article/20220124.html","text":"安装Node14.x 运行Node.js安装程序脚本 123$ sudo yum -y install curl$ curl -sL https://rpm.nodesource.com/setup_14.x | sudo bash - 在CentOS7上安装Node.js 14版本 1$ sudo yum install -y nodejs 查看版本 12$ node -v$ npm -v 创建和初始化你的机器人项目(npm install)因为 PadLocal 使用的某些依赖(特别是 better-sqlite3)是 node native module。所以 npm install 会通过 gyp 编译项目,这个过程中不同平台上可能出现错误。 centos安装 Build Tools 1$ sudo yum groupinstall "Development Tools" centos升级gcc版本 安装依赖 1234mkdir node-wechat && cd node-wechatnpm init -ynpm install ts-node typescript -g --registry=https://r.npm.taobao.orgtsc --init --target ES6 编写demo 1234567891011121314151617181920212223242526272829303132333435363738// bot.tsimport {PuppetPadlocal} from "wechaty-puppet-padlocal";import {Contact, Message, ScanStatus, Wechaty} from "wechaty";const token: string = "" // padlocal tokenconst puppet = new PuppetPadlocal({ token })const bot = new Wechaty({ name: "TestBot", puppet,})bot.on("scan", (qrcode: string, status: ScanStatus) => { if (status === ScanStatus.Waiting && qrcode) { const qrcodeImageUrl = ["https://api.qrserver.com/v1/create-qr-code/?data=", encodeURIComponent(qrcode)].join(""); console.log(`onScan: ${ScanStatus[status]}(${status}) - ${qrcodeImageUrl}`); } else { console.log(`onScan: ${ScanStatus[status]}(${status})`); }}).on("login", (user: Contact) => { console.log(`${user} login`);}).on("logout", (user: Contact) => { console.log(`${user} logout`);}).on("message", async (message: Message) => { console.log(`on message: ${message.toString()}`);}).start()console.log("TestBot", "started"); 运行demo 1ts-node bot.ts","tags":[{"name":"node","slug":"node","permalink":"https://alexhuihui.github.io/tags/node/"},{"name":"微信机器人","slug":"微信机器人","permalink":"https://alexhuihui.github.io/tags/%E5%BE%AE%E4%BF%A1%E6%9C%BA%E5%99%A8%E4%BA%BA/"},{"name":"gcc","slug":"gcc","permalink":"https://alexhuihui.github.io/tags/gcc/"}]},{"title":"【Java】深入解析ThreadLocal","date":"2022-01-20T06:21:42.000Z","path":"article/20220120.html","text":"无论是轻量级的Atomic、volatile,还是synchronized,其实都是采用同步的方式解决了线程安全问题。还有另外一种解决线程安全问题的思路,线程封闭。 如果你有一个全局共享的变量,那么多线程并发的时候,对这个共享变量的访问是不安全的。方法内的局部变量是线程安全的,由于每个线程都会有自己的副本。也就是说局部变量被封闭在线程内部,其它线程无法访问(引用型有所区别)。那么有没有作用域介于两者之间,既能保证线程安全,又不至于只局限于方法内部的方式呢? 答案是肯定的,我们使用ThreadLocal就可以做到这一点。ThreadLocal变量的作用域是为线程,也就是说线程内跨方法共享。例如某个对象的方法A对threadLocal变量赋值,在同一个线程中的另外一个对象的方法B能够读取到该值。因为作用域为同一个线程,那么自然就是线程安全的。但是需要注意的是,如果threadLocal存储的是共享变量的引用,那么同样会有线程安全问题。","tags":[{"name":"多线程","slug":"多线程","permalink":"https://alexhuihui.github.io/tags/%E5%A4%9A%E7%BA%BF%E7%A8%8B/"}]},{"title":"【MySQL】MySQL boolean类型","date":"2022-01-17T11:12:49.000Z","path":"article/20220117.html","text":"MySQL BOOLEAN数据类型MySQL没有内置的布尔类型。 但是它使用TINYINT(1)。 为了更方便,MySQL提供BOOLEAN或BOOL作为TINYINT(1)的同义词。在MySQL中,0被认为是false,非零值被认为是true。 MySQL BOOLEAN示例MySQL将布尔值作为整数存储在表中,我们建立一张tasks表来作为示例展示。 12345CREATE TABLE tasks ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(255) NOT NULL, completed BOOLEAN); 我们在建表语句中将 completed设为boolean类型,但是展示表结构的时候它会变为 tinyint(1)。 123456CREATE TABLE `tasks` ( `id` int(11) NOT NULL AUTO_INCREMENT, `title` varchar(255) NOT NULL, `completed` tinyint(1) DEFAULT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4; 我们向表中插入两条数据 12INSERT INTO `Test`.`tasks` (`id`, `title`, `completed`) VALUES (2, '测试', true);INSERT INTO `Test`.`tasks` (`id`, `title`, `completed`) VALUES (3, '测试2', false); MySQL会在存储数据之前,把true和false转换成1和2再保存,下图是查询结果 因为Boolean类型是tinyint(1)的同义词,所以可以在布尔列中插入1和0以外的值。如下示例: 12INSERT INTO `Test`.`tasks` (`id`, `title`, `completed`) VALUES (1, '打算', NULL);INSERT INTO `Test`.`tasks` (`id`, `title`, `completed`) VALUES (4, 'MySQL', 2); 现在表中的数据如下: 如果要将结果输出为true和false,可以使用IF函数,如下所示: 123456SELECT id, title, IF(completed, 'true', 'false') completedFROM tasks; 执行以上语句,结果如下 MySQL BOOLEAN运算符如果你想查询表中所有 completed为true的数据,那么就应该使用IS运算符而不是= 123456SELECT id, title, completedFROM tasksWHERE completed = TRUE 执行以上语句,结果如下: 它只返回completed列值为1的数据,要想返回所有为true的则必须使用 IS运算符 123456SELECT id, title, completedFROM tasksWHERE completed IS TRUE; 执行结果: 当然你还可以使用 IS FALSE 或者IS NOT TRUE,来查询所有待处理的数据。必须注意的是,前者只能查询出completed等于0的数据,后者还可以查询出completed等于null的数据。","tags":[{"name":"MySQL","slug":"MySQL","permalink":"https://alexhuihui.github.io/tags/MySQL/"}]},{"title":"【RocketMq】rocketmq概念和特性","date":"2022-01-07T03:53:49.000Z","path":"article/20220107.html","text":"基本概念 1 消息模型(Message Model)RocketMQ主要由 Producer、Broker、Consumer 三部分组成,其中Producer 负责生产消息,Consumer 负责消费消息,Broker 负责存储消息。Broker 在实际部署过程中对应一台服务器,每个 Broker 可以存储多个Topic的消息,每个Topic的消息也可以分片存储于不同的 Broker。Message Queue 用于存储消息的物理地址,每个Topic中的消息地址存储于多个 Message Queue 中。ConsumerGroup 由多个Consumer 实例构成。 2 消息生产者(Producer)负责生产消息,一般由业务系统负责生产消息。一个消息生产者会把业务应用系统里产生的消息发送到broker服务器。RocketMQ提供多种发送方式,同步发送、异步发送、顺序发送、单向发送。同步和异步方式均需要Broker返回确认信息,单向发送不需要。 3 消息消费者(Consumer)负责消费消息,一般是后台系统负责异步消费。一个消息消费者会从Broker服务器拉取消息、并将其提供给应用程序。从用户应用的角度而言提供了两种消费形式:拉取式消费、推动式消费。 4 主题(Topic)表示一类消息的集合,每个主题包含若干条消息,每条消息只能属于一个主题,是RocketMQ进行消息订阅的基本单位。 5 代理服务器(Broker Server)消息中转角色,负责存储消息、转发消息。代理服务器在RocketMQ系统中负责接收从生产者发送来的消息并存储、同时为消费者的拉取请求作准备。代理服务器也存储消息相关的元数据,包括消费者组、消费进度偏移和主题和队列消息等。 6 名字服务(Name Server)名称服务充当路由消息的提供者。生产者或消费者能够通过名字服务查找各主题相应的Broker IP列表。多个Namesrv实例组成集群,但相互独立,没有信息交换。 7 拉取式消费(Pull Consumer)Consumer消费的一种类型,应用通常主动调用Consumer的拉消息方法从Broker服务器拉消息、主动权由应用控制。一旦获取了批量消息,应用就会启动消费过程。 8 推动式消费(Push Consumer)Consumer消费的一种类型,该模式下Broker收到数据后会主动推送给消费端,该消费模式一般实时性较高。 9 生产者组(Producer Group)同一类Producer的集合,这类Producer发送同一类消息且发送逻辑一致。如果发送的是事务消息且原始生产者在发送之后崩溃,则Broker服务器会联系同一生产者组的其他生产者实例以提交或回溯消费。 10 消费者组(Consumer Group)同一类Consumer的集合,这类Consumer通常消费同一类消息且消费逻辑一致。消费者组使得在消息消费方面,实现负载均衡和容错的目标变得非常容易。要注意的是,消费者组的消费者实例必须订阅完全相同的Topic。RocketMQ 支持两种消息模式:集群消费(Clustering)和广播消费(Broadcasting)。 11 集群消费(Clustering)集群消费模式下,相同Consumer Group的每个Consumer实例平均分摊消息。 12 广播消费(Broadcasting)广播消费模式下,相同Consumer Group的每个Consumer实例都接收全量的消息。 13 普通顺序消息(Normal Ordered Message)普通顺序消费模式下,消费者通过同一个消息队列( Topic 分区,称作 Message Queue) 收到的消息是有顺序的,不同消息队列收到的消息则可能是无顺序的。 14 严格顺序消息(Strictly Ordered Message)严格顺序消息模式下,消费者收到的所有消息均是有顺序的。 15 消息(Message)消息系统所传输信息的物理载体,生产和消费数据的最小单位,每条消息必须属于一个主题。RocketMQ中每个消息拥有唯一的Message ID,且可以携带具有业务标识的Key。系统提供了通过Message ID和Key查询消息的功能。 16 标签(Tag)为消息设置的标志,用于同一主题下区分不同类型的消息。来自同一业务单元的消息,可以根据不同业务目的在同一主题下设置不同标签。标签能够有效地保持代码的清晰度和连贯性,并优化RocketMQ提供的查询系统。消费者可以根据Tag实现对不同子主题的不同消费逻辑,实现更好的扩展性。 特性(features) 1 订阅与发布消息的发布是指某个生产者向某个topic发送消息;消息的订阅是指某个消费者关注了某个topic中带有某些tag的消息,进而从该topic消费数据。 2 消息顺序消息有序指的是一类消息消费时,能按照发送的顺序来消费。例如:一个订单产生了三条消息分别是订单创建、订单付款、订单完成。消费时要按照这个顺序消费才能有意义,但是同时订单之间是可以并行消费的。RocketMQ可以严格的保证消息有序。 顺序消息分为全局顺序消息与分区顺序消息,全局顺序是指某个Topic下的所有消息都要保证顺序;部分顺序消息只要保证每一组消息被顺序消费即可。 全局顺序 对于指定的一个 Topic,所有消息按照严格的先入先出(FIFO)的顺序进行发布和消费。 适用场景:性能要求不高,所有的消息严格按照 FIFO 原则进行消息发布和消费的场景 分区顺序 对于指定的一个 Topic,所有消息根据 sharding key 进行区块分区。 同一个分区内的消息按照严格的 FIFO 顺序进行发布和消费。 Sharding key 是顺序消息中用来区分不同分区的关键字段,和普通消息的 Key 是完全不同的概念。 适用场景:性能要求高,以 sharding key 作为分区字段,在同一个区块中严格的按照 FIFO 原则进行消息发布和消费的场景。 3 消息过滤 RocketMQ的消费者可以根据Tag进行消息过滤,也支持自定义属性过滤。消息过滤目前是在Broker端实现的,优点是减少了对于Consumer无用消息的网络传输,缺点是增加了Broker的负担、而且实现相对复杂。 4 消息可靠性 RocketMQ支持消息的高可靠,影响消息可靠性的几种情况: 1) Broker非正常关闭 2) Broker异常Crash 3) OS Crash 4) 机器掉电,但是能立即恢复供电情况 5) 机器无法开机(可能是cpu、主板、内存等关键设备损坏) 6) 磁盘设备损坏 1)、2)、3)、4) 四种情况都属于硬件资源可立即恢复情况,RocketMQ在这四种情况下能保证消息不丢,或者丢失少量数据(依赖刷盘方式是同步还是异步)。 5)、6)属于单点故障,且无法恢复,一旦发生,在此单点上的消息全部丢失。RocketMQ在这两种情况下,通过异步复制,可保证99%的消息不丢,但是仍然会有极少量的消息可能丢失。通过同步双写技术可以完全避免单点,同步双写势必会影响性能,适合对消息可靠性要求极高的场合,例如与Money相关的应用。注:RocketMQ从3.0版本开始支持同步双写。 5 至少一次至少一次(At least Once)指每个消息必须投递一次。Consumer先Pull消息到本地,消费完成后,才向服务器返回ack,如果没有消费一定不会ack消息,所以RocketMQ可以很好的支持此特性。 6 回溯消费回溯消费是指Consumer已经消费成功的消息,由于业务上需求需要重新消费,要支持此功能,Broker在向Consumer投递成功消息后,消息仍然需要保留。并且重新消费一般是按照时间维度,例如由于Consumer系统故障,恢复后需要重新消费1小时前的数据,那么Broker要提供一种机制,可以按照时间维度来回退消费进度。RocketMQ支持按照时间回溯消费,时间维度精确到毫秒。 7 事务消息RocketMQ事务消息(Transactional Message)是指应用本地事务和发送消息操作可以被定义到全局事务中,要么同时成功,要么同时失败。RocketMQ的事务消息提供类似 X/Open XA 的分布事务功能,通过事务消息能达到分布式事务的最终一致。 8 定时消息定时消息(延迟队列)是指消息发送到broker后,不会立即被消费,等待特定时间投递给真正的topic。 broker有配置项messageDelayLevel,默认值为“1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h”,18个level。可以配置自定义messageDelayLevel。注意,messageDelayLevel是broker的属性,不属于某个topic。发消息时,设置delayLevel等级即可:msg.setDelayLevel(level)。level有以下三种情况: level == 0,消息为非延迟消息 1<=level<=maxLevel,消息延迟特定时间,例如level==1,延迟1s level > maxLevel,则level== maxLevel,例如level==20,延迟2h 定时消息会暂存在名为SCHEDULE_TOPIC_XXXX的topic中,并根据delayTimeLevel存入特定的queue,queueId = delayTimeLevel – 1,即一个queue只存相同延迟的消息,保证具有相同发送延迟的消息能够顺序消费。broker会调度地消费SCHEDULE_TOPIC_XXXX,将消息写入真实的topic。 需要注意的是,定时消息会在第一次写入和调度写入真实topic时都会计数,因此发送数量、tps都会变高。 9 消息重试Consumer消费消息失败后,要提供一种重试机制,令消息再消费一次。Consumer消费消息失败通常可以认为有以下几种情况: 由于消息本身的原因,例如反序列化失败,消息数据本身无法处理(例如话费充值,当前消息的手机号被注销,无法充值)等。这种错误通常需要跳过这条消息,再消费其它消息,而这条失败的消息即使立刻重试消费,99%也不成功,所以最好提供一种定时重试机制,即过10秒后再重试。 由于依赖的下游应用服务不可用,例如db连接不可用,外系统网络不可达等。遇到这种错误,即使跳过当前失败的消息,消费其他消息同样也会报错。这种情况建议应用sleep 30s,再消费下一条消息,这样可以减轻Broker重试消息的压力。 RocketMQ会为每个消费组都设置一个Topic名称为“%RETRY%+consumerGroup”的重试队列(这里需要注意的是,这个Topic的重试队列是针对消费组,而不是针对每个Topic设置的),用于暂时保存因为各种异常而导致Consumer端无法消费的消息。考虑到异常恢复起来需要一些时间,会为重试队列设置多个重试级别,每个重试级别都有与之对应的重新投递延时,重试次数越多投递延时就越大。RocketMQ对于重试消息的处理是先保存至Topic名称为“SCHEDULE_TOPIC_XXXX”的延迟队列中,后台定时任务按照对应的时间进行Delay后重新保存至“%RETRY%+consumerGroup”的重试队列中。 10 消息重投生产者在发送消息时,同步消息失败会重投,异步消息有重试,oneway没有任何保证。消息重投保证消息尽可能发送成功、不丢失,但可能会造成消息重复,消息重复在RocketMQ中是无法避免的问题。消息重复在一般情况下不会发生,当出现消息量大、网络抖动,消息重复就会是大概率事件。另外,生产者主动重发、consumer负载变化也会导致重复消息。如下方法可以设置消息重试策略: retryTimesWhenSendFailed:同步发送失败重投次数,默认为2,因此生产者会最多尝试发送retryTimesWhenSendFailed + 1次。不会选择上次失败的broker,尝试向其他broker发送,最大程度保证消息不丢。超过重投次数,抛出异常,由客户端保证消息不丢。当出现RemotingException、MQClientException和部分MQBrokerException时会重投。 retryTimesWhenSendAsyncFailed:异步发送失败重试次数,异步重试不会选择其他broker,仅在同一个broker上做重试,不保证消息不丢。 retryAnotherBrokerWhenNotStoreOK:消息刷盘(主或备)超时或slave不可用(返回状态非SEND_OK),是否尝试发送到其他broker,默认false。十分重要消息可以开启。 11 流量控制生产者流控,因为broker处理能力达到瓶颈;消费者流控,因为消费能力达到瓶颈。 生产者流控: commitLog文件被锁时间超过osPageCacheBusyTimeOutMills时,参数默认为1000ms,返回流控。 如果开启transientStorePoolEnable == true,且broker为异步刷盘的主机,且transientStorePool中资源不足,拒绝当前send请求,返回流控。 broker每隔10ms检查send请求队列头部请求的等待时间,如果超过waitTimeMillsInSendQueue,默认200ms,拒绝当前send请求,返回流控。 broker通过拒绝send 请求方式实现流量控制。 注意,生产者流控,不会尝试消息重投。 消费者流控: 消费者本地缓存消息数超过pullThresholdForQueue时,默认1000。 消费者本地缓存消息大小超过pullThresholdSizeForQueue时,默认100MB。 消费者本地缓存消息跨度超过consumeConcurrentlyMaxSpan时,默认2000。 消费者流控的结果是降低拉取频率。 12 死信队列死信队列用于处理无法被正常消费的消息。当一条消息初次消费失败,消息队列会自动进行消息重试;达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息,此时,消息队列 不会立刻将消息丢弃,而是将其发送到该消费者对应的特殊队列中。 RocketMQ将这种正常情况下无法被消费的消息称为死信消息(Dead-Letter Message),将存储死信消息的特殊队列称为死信队列(Dead-Letter Queue)。在RocketMQ中,可以通过使用console控制台对死信队列中的消息进行重发来使得消费者实例再次进行消费。","tags":[{"name":"RocketMq","slug":"RocketMq","permalink":"https://alexhuihui.github.io/tags/RocketMq/"}]},{"title":"【Redis】基于redisson搭建延迟队列","date":"2022-01-06T08:18:56.000Z","path":"article/20220106.html","text":"什么是延时队列? 它首先具有队列的特性(先进先出),同时还能指定队列中的消息在未来某个时刻被消费。 应用:抢单成功后,订单1分钟内未支付,自动关闭。 实现延时队列很早之前我有使用过redis的zset数据结构来实现延时队列(自动关闭考试),但是这种方法需要开启定时任务去轮询delayqueue,实时性不好,且无法支持应用的水平扩展。后来有接触到redisson——一个Java实现的redis客户端,发现用它来实现延时队列可以做的更好。 1、定义接口12public interface Queue {} 2、实现抽象类首先要实现一个redisson的客户端 1234567891011@Beanpublic RedissonClient getRedissonClient() { Config config = new Config(); SingleServerConfig server = config.useSingleServer(); String address = StrUtil.format("redis://{}:{}", host, port); server.setAddress(address); if (StrUtil.isNotEmpty(password)) { server.setPassword(password); } return Redisson.create(config);} 抽象类AbstractQueue 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374public abstract class AbstractQueue implements Queue { protected String name; protected static Map<String, IQueueListener> listenerMap; protected static RedissonClient redissonClient = SpringUtil.getBean(RedissonClient.class); protected static String LOCK_PREFIX = "alexmmd:queue:listeners"; static { // put topic and corresponding queueListener into the listenerMap listenerMap = new HashMap<>(); QueueConsumer annotation; Map<String, IQueueListener> beansOfType = SpringUtil.getBeansOfType(IQueueListener.class); if (MapUtil.isNotEmpty(beansOfType)) { for (IQueueListener listener : beansOfType.values()) { // bind topic and corresponding listener annotation = listener.getClass().getAnnotation(QueueConsumer.class); if (ObjectUtil.isNull(annotation)) { continue; } String topic = annotation.topic(); if (StrUtil.isNotEmpty(topic)) { listenerMap.put(topic, listener); } String[] topics = annotation.topics(); if (ArrayUtil.isEmpty(topics)) { continue; } for (String s : topics) { listenerMap.put(s, listener); } } } } public AbstractQueue(String name) { this.name = name; new Thread(() -> { log.info("开启一个线程 {} 监听队列 : {}", Thread.currentThread().getName(), name); while (true) { try { Pair<String, String> take = this.take(); String topic = take.getKey(); String body = take.getValue(); new Thread(consumer(name, topic, body)).start(); } catch (Exception e) { log.error("监听队列线程错误,{}", e.getMessage()); } } }).start(); } private Runnable consumer(String name, String topic, String body) { return () -> { log.info("线程: {} 监听队列:{}, topic: {}, body: {}, 开始处理", Thread.currentThread().getName() , name, topic, body); RLock lock = redissonClient.getLock(LOCK_PREFIX + topic); try { lock.lock(); IQueueListener queueListener = listenerMap.get(topic); if (ObjectUtil.isNull(queueListener)) { log.error("topic {} 没有找到对应的监听器", topic); return; } queueListener.consumer(body); } catch (Exception e) { log.error("消费失败"); } finally { lock.unlock(); } }; } protected abstract Pair<String, String> take() throws InterruptedException;} 在抽象类的静态代码块中,获取所有实现了IQueueListener接口的类。获取这些类中QueueConsumer注解里标明的topic,把topic和QueueListener绑定——存入lsitenerMap容器中。 在唯一的带参构造器中,先给当前的实现类的name属性赋值,然后再开启一个线程去监听延时队列,调用子类实现的take()获取队列中的值,如果取得到值就开启一个线程去调用consumer()消费。 在consumer()中,首先通过消息体Pair类的key值(topic)从listenerMap中获取对应的IQueueListener类,再由该类去处理消息体。 take()由具体的子类来实现 12345678910111213141516171819202122232425262728@Slf4jpublic class DelayQueue extends AbstractQueue { private RDelayedQueue<Pair<String, String>> delayedQueue; private RBlockingDeque<Pair<String, String>> blockingDeque; public DelayQueue(String name, RDelayedQueue<Pair<String, String>> delayedQueue, RBlockingDeque<Pair<String, String>> blockingDeque) { super(name); this.blockingDeque = blockingDeque; this.delayedQueue = delayedQueue; } public static DelayQueue create(String name) { RBlockingDeque<Pair<String, String>> blockingDeque = redissonClient.getBlockingDeque(name); RDelayedQueue<Pair<String, String>> delayedQueue = redissonClient.getDelayedQueue(blockingDeque); return new DelayQueue(name, delayedQueue, blockingDeque); } public <T> void offer(String topic, T body, long delay) { delayedQueue.offer(Pair.of(topic, JSONUtil.toJsonStr(body)), delay, TimeUnit.SECONDS); } @Override protected Pair<String, String> take() throws InterruptedException { return blockingDeque.take(); }} 子类中提供了一个create()的静态方法,暴露给外部来创建延时队列。其中通过redissonClient创建了RBlockingDeque和RDelayedQueue,然后再调用构造方法。在子类的构造方法中,调用抽象类的构造方法,然后把给RBlockingDeque和RDelayedQueue两个成员变量赋值。 offer()就是新增延时任务,往RDelayedQueue中添加值。 take()就是获取延时任务,从RBlockingDeque获取值,在父类中的构造函数中开启的监听线程中被调用。 以上就通过redisson实现了延时队列,下面是使用用例。 123456789101112public class DelayClient { private static DelayQueue delayQueue = DelayQueue.create("DELAY_QUEUE_01"); public static <T> void offer(String topic, T body, long delay) { delayQueue.offer(topic, body, delay); } public static <T> void offer(String topic, T body, Date date) { delayQueue.offer(topic, body, DateUtil.between(new Date(), date, DateUnit.SECOND)); }} tip: 项目源码","tags":[{"name":"Redis","slug":"Redis","permalink":"https://alexhuihui.github.io/tags/Redis/"},{"name":"延时队列","slug":"延时队列","permalink":"https://alexhuihui.github.io/tags/%E5%BB%B6%E6%97%B6%E9%98%9F%E5%88%97/"}]},{"title":"【安全】Java如何使用JWT","date":"2021-12-25T04:36:02.000Z","path":"article/20211225.html","text":"使用Java JWT引入maven依赖 12345<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.18.2</version></dependency> 生成密钥对RSA生成1024位私钥: 1$ openssl genrsa -out private.key 1024 RSA生成公钥: 1$ openssl rsa -in private.key -pubout -out pub.key 通过java使用私钥必须要先经过PCK8转换 1openssl pkcs8 -topk8 -nocrypt -in private.key -outform PEM -out java_private.key 从文件中读取公钥和私钥12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364@Slf4jpublic class PemUtil { private static byte[] parsePEMFile(File pemFile) throws IOException { if (!pemFile.isFile() || !pemFile.exists()) { throw new FileNotFoundException(String.format("The file '%s' doesn't exist.", pemFile.getAbsolutePath())); } PemReader reader = new PemReader(new FileReader(pemFile)); PemObject pemObject = reader.readPemObject(); byte[] content = pemObject.getContent(); reader.close(); return content; } private static PublicKey getPublicKey(byte[] keyBytes, String algorithm) { PublicKey publicKey = null; try { KeyFactory kf = KeyFactory.getInstance(algorithm); EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes); publicKey = kf.generatePublic(keySpec); } catch (NoSuchAlgorithmException e) { System.out.println("Could not reconstruct the public key, the given algorithm could not be found."); } catch (InvalidKeySpecException e) { System.out.println("Could not reconstruct the public key"); } return publicKey; } private static PrivateKey getPrivateKey(byte[] keyBytes, String algorithm) { PrivateKey privateKey = null; try { KeyFactory kf = KeyFactory.getInstance(algorithm); EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); privateKey = kf.generatePrivate(keySpec); } catch (NoSuchAlgorithmException e) { System.out.println("Could not reconstruct the private key, the given algorithm could not be found."); } catch (InvalidKeySpecException e) { System.out.println("Could not reconstruct the private key"); } return privateKey; } public static PublicKey readPublicKeyFromFile(String filepath, String algorithm) { byte[] bytes = new byte[0]; try { bytes = PemUtil.parsePEMFile(new File(filepath)); } catch (IOException exception) { log.error(exception.getMessage()); } return PemUtil.getPublicKey(bytes, algorithm); } public static PrivateKey readPrivateKeyFromFile(String filepath, String algorithm) { byte[] bytes = new byte[0]; try { bytes = PemUtil.parsePEMFile(new File(filepath)); } catch (IOException exception) { log.error(exception.getMessage()); } return PemUtil.getPrivateKey(bytes, algorithm); }} 使用私钥生成Token123456789101112131415161718192021222324public static String generateToken(JSONObject jsonObject) { try { //加密时,使用私钥生成RSA算法对象 Algorithm algorithm = Algorithm.RSA256(null, PRIVATE_KEY); DateTime date = DateUtil.date(); return JWT.create() //签发人 .withIssuer("auth-server") //接收者 .withAudience("client") //签发时间 .withIssuedAt(date) //过期时间 .withExpiresAt(DateUtil.offsetMinute(date, 5)) //相关信息 .withClaim("data", jsonObject.toString()) //签入 .sign(algorithm); } catch (JWTCreationException exception) { //Invalid Signing configuration / Couldn't convert Claims. log.error(exception.getMessage()); } return null; } 使用公钥校验Token123456789101112131415161718192021public static boolean verifierToken(String token) { // 根据密钥对生成RS256算法对象 Algorithm algorithm = Algorithm.RSA256(PUBLIC_KEY, null); JWTVerifier verifier = JWT.require(algorithm) .build(); try { // 验证Token,verifier自动验证 DecodedJWT jwt = verifier.verify(token); // 打印用户声明的信息 Claim data = jwt.getClaim("data"); JSONObject jsonObject = JSONUtil.parseObj(data.asString()); for (String key : jsonObject.keySet()) { log.info("key === {}, value === {}", key, jsonObject.get(key)); } return true; } catch (JWTVerificationException e) { log.error("Token无法通过验证! " + e.getMessage()); return false; } } Tip 私钥保存在服务器端,被Auth server 持有,公钥可以颁发给需要验证Token的服务 使用JSONObject对象存储用户声明的信息,并放入payload中 本示例源码","tags":[{"name":"JWT","slug":"JWT","permalink":"https://alexhuihui.github.io/tags/JWT/"},{"name":"签名","slug":"签名","permalink":"https://alexhuihui.github.io/tags/%E7%AD%BE%E5%90%8D/"}]},{"title":"【安全】JWT介绍和使用","date":"2021-12-24T03:11:49.000Z","path":"article/20211224.html","text":"Why use it? JWT可以存储用户基本信息,常用于微服务中对请求的鉴权。 JWT是无状态的,方便后端服务的水平扩展 一、JWT的构成一共三部分,第一部分我们称它为头部(header),第二部分我们称其为载荷(payload),第三部分是签名(signature). 1.headerjwt的头部承载两部分信息: 声明类型,这里是jwt 声明加密的算法 通常直接使用 HMAC SHA256 完整的头部就像下面这样的JSON: 1234{ 'typ': 'JWT', 'alg': 'HS256'} 然后将头部进行base64编码,构成了第一部分. 1eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 2.payload载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分 标准中注册的声明 公共的声明 私有的声明 标准中注册的声明 (建议但不强制使用) : iss: jwt签发者 sub: jwt所面向的用户 aud: 接收jwt的一方 exp: jwt的过期时间,这个过期时间必须要大于签发时间 nbf: 定义在什么时间之前,该jwt都是不可用的 iat: jwt的签发时间 jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击 公共的声明 : 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密。 私有的声明 : 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64只是进行编码,意味着该部分信息可以归类为明文信息。 定义一个payload: 12345{ "sub": "1234567890", "name": "John Doe", "admin": true} 然后将其进行base64加密,得到JWT的第二部分。 1eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9 3.signature签名可以校验payload里面的信息是否被篡改,它由编码过后的header和payload采用header里声明的算法使用secret key进行加密。 1signature = Crypto(secret, base64(header), base64(payload)) 下面是个签名示例 1jbcOUQ2bbiYlfVtprEkaT_S6Y6yQnBDOAKDHIHjvl7g 如果你感兴趣,可以拷贝下面的JWT去https://jwt.io进行解密. 1eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxZGZlZThkOC05OGE1LTQzMTQtYjRhZS1mYjU1YzRiMTg4NDUiLCJlbWFpbCI6ImFyaWVsQGNvZGluZ2x5LmlvIiwibmFtZSI6IkFyaWVsIFdlaW5iZXJnZXIiLCJyb2xlIjoiVVNFUiIsImlhdCI6MTU5ODYwODg0OCwiZXhwIjoxNTk4NjA5MTQ4fQ.oa3ziIZAoVFdn-97rweJAjjFn6a4ZSw7ogIHA74mGq0 既然JWT不能对信息加密,那它是如何防止信息被篡改的呢?比如我把角色改为Admin。服务端对JWT进行校验的时候会 对header和payload再次使用secret key加密,和传进来的JWT第三部分进行比较,如果不匹配则JWT不合法。 二、常见的误解、问题和技巧1.短暂的有效期和主动失效有效期较短的token是被高度推荐的,一些服务的token过期时间为5分钟在发布它们之后。 在token过期之后,auth server将会颁发一个包含最新的身份信息的access token,这个过程被叫做 token refresh。举个例子,如果用户角色从 Admin 变更为 User,拥有较短的生命周期的token可以保证用户的token包含最近的用户角色. 综上所述,较短的有效期的token有以下两个好处: 如果你的token被盗取,攻击者使用你的token的有效时间窗口将会很短 JWT是无状态的,服务端无法主动令其失效,因此,较短的生命周期可以让我们在用户权限和角色方面保证强大的一致性 2.Refresh Token接上文,Refresh Token在很多使用JWT的系统的是必须要用的。 它的使用很简单,在最初authentication success的时候,给用户颁发两个token Access Token : 每个请求必须携带的JSON Web Token ,包含用户声明的信息 Refresh Token : 这个特殊类型的token是被持久化保存在数据库中的,被Auth Server所持有。它经常不是JWT,而是一个独一无二的hash值。 我们已经知道,Access Token会携带在每个请求中并会在某个时间节点失效。然后前端将会发送一个Refresh请求并携带 Refresh Token。Auth Server将会生成一个新的Access Token返回给用户,用户使用这个token直到它过期,然后再刷新一遍,Over and over。 Refresh Token的有效期一般会长达数月,当它失效后,用户必须重新登录。 3.Secret VS Private-Public Key流行的 HS256 签名算法的缺点是在生成和验证的时候都需要访问密钥。对于单一程序而言,这不是一个问题,但如果你有一个由多个服务构建的分布式系统,彼此独立运行,你必须在两个糟糕的选择之间进行选择: 你可以选择使用专用服务进行 token 的生成和验证。从客户端接收 token 的任何服务,都需要调用身份验证服务来验证 token。对于繁忙的系统来说,这会在身份验证服务上产生性能问题。 你可以为所有需要从客户端接收 token 的服务配置密钥,以便它们可以验证 token,无需调用身份验证服务。但是,在多个地方使用密钥会增加其受到攻击的风险,并且一旦受到攻击,攻击者就可以生成有效 token 并模拟系统中的任何用户。 因此对于这些程序,最好将签名密钥安全存储在身份验证服务中,并且仅用于生成密钥,而其他所有服务无需访问密钥就可以验证这些 token。实际上,这可以通过公钥加密来实现。 公钥加密基于两个组件的加密密钥:一个公钥和一个私钥。在命名实现时,公钥组件可以自由共享。可以使用公钥加密来完成两个工作流程: 消息加密:如果你想要向某人发送加密消息,我可以使用他的公钥来加密。加密的消息只能用他的私钥来解密。 消息签名:如果我想要签署一条消息它来自我,我可以使用自己的私钥来生成签名。任何对验证消息感兴趣的人,都可以使用我的公钥来确认签名是否有效。 JWT 的签名算法实现了上面的第二种情况。使用服务器的私钥对 token 进行签名,然后任何使用服务器公钥的人都可以验证它们,任何想要拥有它的人都可以免费使用。举个例子来说下面,我使用 RS256 签名算法,它是 RSA-SHA256 的简写。 下一步是为程序生成公钥/私钥集 (通常称为 “秘钥对”)。生成 RSA 密钥有几种不同的方法,但我喜欢使用 openssh 中的 ssh-keygen 工具: 123456789101112131415161718192021$ ssh-keygen -t rsa -b 4096Generating public/private rsa key pair.Enter file in which to save the key (/home/miguel/.ssh/id_rsa): jwt-keyEnter passphrase (empty for no passphrase):Enter same passphrase again:Your identification has been saved in jwt-key.Your public key has been saved in jwt-key.pub.The key fingerprint is:SHA256:ZER3ddV4/smE0rnoNesS+IwCNSbwu5SThfiWWtLYRVM miguel@MS90J8G8WLThe key's randomart image is:+---[RSA 4096]----+| .+E. ....=|| . + . . ..o|| + o + . oo || . + O . + ..|| = @ S . o + o|| o # . o + o.|| * + = o o || . . . . = . || . o. |+----[SHA256]-----+ ssh-keygen 命令的 -t 选项定义了,我正在请求的秘钥对,-b 选项指定密钥大小为 4096 位,这被认为是一个非常安全的密钥长度。当你运行该命令时,系统提示你需要为秘钥对提供文件名,这里我在当前路径下使用 jwt-key 。然后,系统将提示你输入密码来保护密钥,密钥需要保留为空。 当运行完命令,你会在当前目录下获得两个文件,jwt-key 和 jwt-key.pub。前者是私钥,将用于生成 token 签名,因此你需要保存好它。特别是,你不应该将私钥提交到代码仓库,而应直接在服务器上安装 (如果你需要重建服务器,则应做好备份)。.pub 文件用于验证 token。由于此文件没有敏感信息,因此你可以在需要验证 token 的任何项目上随意添加该文件的副本。 使用此秘钥对生成 token 的过程与我之前的内容非常相似。我们先使用私钥创建一个新的 token: 1234import jwtprivate_key = open('jwt-key').read()token = jwt.encode({'user_id': 123}, private_key, algorithm='RS256').decode('utf-8')print(token) 然后再使用公钥来验证: 12345import jwtpublic_key = open('jwt-key.pub').read()token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1c2VyX2lkIjoxMjN9.HT1kBSdGFAznrhbs2hB6xjVDilMUmKA-_36n1pLLtFTKHoO1qmRkUcy9bJJwGuyfJ_dbzBMyBwpXMj-EXnKQQmKlXsiItxzLVIfC5qE97V6l6S0LzT9bzixvgolwi-qB9STp0bR_7suiXaON-EzBWFh0PzZi7l5Tg8iS_0_iSCQQlX5MSJW_-bHESTf3dfj5GGbsRBRsi1TRBzvxMUB6GhNsy6rdUhwoTkihk7pljISTYs6BtNoGRW9gVUzfA2es3zwBaynyyMeSocYet6WJri97p0eRnVGtHSWwAmnzZ-CX5-scO9uYmb1fT1EkhhjGhnMejee-kQkMktCTNlPsaUAJyayzdgEvQeo5M9ZrfjEnDjF7ntI03dck1t9Bgy-tV1LKH0FWNLq3dCJJrYdQx--A-I7zW1th0C4wNcDe_d_GaYopbtU-HPRG3Z1SPKFuX1m0uYhk9aySvkec66NBfvV2xEgo8lRZyNxntXkMdeJCEiLF1UhQvvSvmWaWC-0uRulYACn4H-tZiaK7zvpcPkrsfJ7iR_O1bxMPziKpsM4b7c7tmsEcOUZY-IHEI9ibd54_A1O72i08sCWKT5CXyG70MAPqyR0MFlcV7IuDtBW3LCqyvfsDVk4eIj8VcSU1OKQJ1Gl-CTOHEyN-ncV3NslVLaT9Q1C4E7uK2QpS8z0'payload = jwt.decode(token, public_key, algorithms=['RS256'])print(payload) 如果你感兴趣可以使用我的公钥来验证: 1public_key = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDKQ43JBiv6FwtMpq6hStz59tSUo8mEU2SwQSg6mGmm4RBZ7uudRSnSwcvGSv45J1rKx1GUSuGYs/To4Ehsney1utN3jUY0XPhSUgxgCIFz66R9eAu92pykWBcSF96mj753LsyBStqq1nCu1D0dEJOBQnz9J2Zzs0N71ZabEvb6hXE8e119j2LFEUQWfM+3+AzrgsVXsTFE8y3LKeWohzQRAY3cAsdPPKI2ayHtlW6DoQ9LLqnsj4YkCe0hDIP/unTyx5OijbD6r8AeAyY4EGIL3c3jYhla6pclSqyLyq7kI0lUAQq4IXS4DYdqNHX01fI57tZeim0+nM3/9H1vJC2EPCDZDRdC2reJ4go/AssyQGamlmlu21Xt3n7rthYWg1u6Y/wpabDBEQcoXJPrBwyuGK8VRQrUU16GMahSiTpHxwvKHBMVyghLPaC8SBf9/edJDxBIq9s2wpnNBdD8TohqAXQILnJt5QBbiG2L/3uMqrEg+udWURBOcVok9TnS6RTtvezM1dSlcYoNMhGYNLMKgBByMOM/bMqJg8udqWqPW2/i/eCNPTLRPqZR7Kdo/GGcMY9pSsDvbzM/IHdeV1JhTyRcSi0xJDCs2M3hmWLg1rJNf7wQZKISIXefIItrABRoF1MuKOsk2CgPnZJgjVhzjzSP7EmyzRAv0E8gCeCtDQ== root@VM-20-3-centos' 总结推荐大家采用 RS256 非对称加密算法来生成和验证JWT Token,token的有效期要短一点,并且要有Refresh Token机制。","tags":[{"name":"JWT","slug":"JWT","permalink":"https://alexhuihui.github.io/tags/JWT/"},{"name":"签名","slug":"签名","permalink":"https://alexhuihui.github.io/tags/%E7%AD%BE%E5%90%8D/"}]},{"title":"【安全】基本加密解密算法介绍","date":"2021-12-23T03:28:18.000Z","path":"article/20211223.html","text":"对称加密1.介绍 加密和解密都是使用相同的秘钥 优点是加密效率高,缺点是保密性低 2.常见的对称加密算法 DES算法(Data Encryption Standard) AES算法(Advanced Encryption Standard) 3.使用场景 微服务中用作MySQL,Redis等服务器地址、端口、账户名、密码等配置的加密 非对称加密1.介绍 有两把钥匙(密钥对),公钥和私钥,公钥的话给别人,私钥自己保存 特点是加密速度慢些,但是安全系数很高 加密和解密的规则是:公钥加密只能私钥解密,私钥加密只能公钥解密 2. 算法 RSA 3.使用场景 身份认证:一条加密信息若能用A 的公钥能解开,则该信息一定是用A 的私钥加密的,该能确定该用户是A 陌生人通信:A 和B 两个人互不认识,A把自己的公钥发给B,B也把自己的公钥发给A,则双方可以通过对方的公钥加密信息通信。C 虽然也能得到A、B 的公钥,但是他解不开密文。 支付宝支付加密 单向加密(MD5) 单向加密算法在加密过程中不需要使用密钥,输入明文后由算法直接加密成密文,密文无法解密,只有重新输入密文,并经过同样的加密算法处理,得到形同的密文并被系统重新识别后,才能真正的解密,这种算法非常复杂,通常只在数据量有限的情形下使用,如广泛应用在计算机系统中的口令加密等。 代码演示","tags":[{"name":"AES","slug":"AES","permalink":"https://alexhuihui.github.io/tags/AES/"},{"name":"RSA","slug":"RSA","permalink":"https://alexhuihui.github.io/tags/RSA/"},{"name":"MD5","slug":"MD5","permalink":"https://alexhuihui.github.io/tags/MD5/"}]},{"title":"【Hexo博客折腾】Hexo基于github搭建静态博客","date":"2021-12-22T08:56:24.000Z","path":"article/20211222.html","text":"开始之前在安装hexo之前,必须确认你已经安装了Node.js和Git。 1.创建GitHub仓库注册GitHub账号,创建一个以”用户名.github.io”命名的仓库,如我的用户名为alexhuihui,那我的仓库名为:alexhuihui.github.io,仓库默有master分支,用于托管生成的静态文件,再新建一个blog-files(名字自定)分支,用于托管后台文件,方便以后换电脑时后台文件不会丢失。 2.配置Git设置Git的用户名和邮件地址(邮箱就是你注册Github时候的邮箱),打开Git Bash,键入: 12$ git config --global user.name "username"$ git config --global user.email "email@example.com" 3.本地Git与GitHub建立联系这里介绍SSH的配置,先检查电脑是否已经有SSH 1$ ls -al ~/.ssh 如果不存在就没有关系,如果存在的话,直接删除.ssh文件夹里面所有文件。输入以下指令后,一路回车就好: 1$ ssh-keygen -t rsa -C "emailt@example.com" 然后键入以下指令: 12$ ssh-agent -s$ ssh-add ~/.ssh/id_rsa 如果出现这个错误:Could not open a connection to your authentication agent,则先执行如下命令即可: 1$ ssh-agent bash 再重新输入指令: 1$ ssh-add ~/.ssh/id_rsa 到了这一步,就可以添加SSH key到你的Github账户了。键入以下指令,拷贝Key(先拷贝了,等一下可以直接粘贴): 1$ clip < ~/.ssh/id_rsa.pub 在github上点击你的头像–>Your profile–>Edit profile–>SSH and GPG keys–>New SSH keyTitle自己随便取,然后这个Key就是刚刚拷贝的,你直接粘贴就好(也可以文本打开id_rsa.pub复制其内容),最后Add SSH key。最后还是测试一下吧,键入以下命令: 1$ ssh -T git@github.com 你可能会看到有警告,没事,输入“yes”就好。 4.初始化hexo文件夹到GitHub的alexhuihui.github.io仓库下,点击Clone or download,复制里面的HTTPS地址。在E盘或是你喜爱的文件夹下,右键Git Bash Here: 键入git clone -b develop <刚复制的地址> 12$ git clone -b blog-files https://github.com/alexhuihui/alexhuihui.github.io.git$ mkdir Hexo-admin Hexo安装配置1.Hexo初始化进入Hexo-admin文件夹 1$ cd Hexo-admin 接下来只需要使用 npm 即可完成 Hexo 的安装: 1$ npm install -g hexo-cli 安装 Hexo 完成后,请执行下列命令,Hexo 将会在指定文件夹中新建所需要的文件: 12$ hexo init$ npm install 接下来也可以本地预览博客,执行下列命令,然后到浏览器输入localhost:4000看看。 12$ hexo generate$ hexo server 输入Ctrl+C停止服务。 2.Hexo配置用编辑器打开 Hexo-admin/ 下的配置文件_config.yml找到: 12345# Deployment## Docs: https://hexo.io/docs/deployment.htmldeploy: type: repository: 到GitHub的alexhuihui.github.io仓库下,点击Clone or download,复制里面的HTTPS地址到repository:,添加branch: master。 123456# Deployment## Docs: https://hexo.io/docs/deployment.htmldeploy: type: git repository: https://github.com/alexhuihui/alexhuihui.github.io.git branch: master 3.完成部署最后一步,快要成功了,键入指令: 123$ npm install hexo-deployer-git --save$ hexo generate$ hexo deploy 输入弹出框的用户名与密码(首次使用git会弹出)。OK,我们的博客就已经完全搭建起来了,在浏览器输入(当然,是你的Repository名),例如我的:alexhuihui.github.io/每次修改本地文件后,需要键入hexo generate才能保存,再键入hexo deploy上传文件。成功之后命令行最后两句大概是这样: 123To https://github.com/alexhuihui/alexhuihui.github.io.git 7f3b50a..128a10d HEAD -> masterINFO Deploy done: git 当然,不要忘了回退到父文件夹提交网站相关的文件以备今后迁移,依次执行git add .、git commit -m “…”、git push origin develop。 日常操作1.写文章执行new命令,生成指定名称的文章至 Admin-blog\\source_posts\\文章标题.md 。 1$ hexo new [layout] "文章标题" #新建文章 然后用编辑器打开“文章标题.md”按照Markdown语法书写文章。 其中layout是可选参数,默认值为post。到 scaffolds 目录下查看现有的layout。当然你可以添加自己的layout, 同时你也可以编辑现有的layout,比如post的layout默认是 hexo\\scaffolds\\post.md 1234title: { { title } }date: { { date } }tags:--- 我想添加categories,以免每次手工输入,只需要修改这个文件添加一行,如下: 12345title: { { title } }date: { { date } }categories:tags:--- 文件标题也是md文件的名字,同时也出现在你文章的URL中,postName如果包含空格,必须用”将其包围。请注意,大括号与大括号之间我多加了个空格,否则会被转义,不能正常显示;所有文件":"后面都必须有个空格,不然会报错。 2.提交每次在本地对博客进行修改后,先执行下列命令提交网站相关的文件。 123$ git add .$ git commit -m "..."$ git push origin develop 然后才执行hexo generate -d发布网站到master分支上。 1$ hexo generate -d 3.本地仓库丢失当你想在其他电脑工作,或电脑重装系统后,安装Git与Node.js后,可以使用下列步骤: 3.1拷贝仓库1$ git clone -b develop https://github.com/alexhuihui/alexhuihui.github.io.git 3.2配置Hexo在本地新拷贝的alexhuihui.github.io文件夹下通过Git bash依次执行下列指令: 1234$ npm install -g hexo-cli$ npm install hexo$ npm install$ npm install hexo-deployer-git --save 小Tips:hexo 命令1234567891011121314hexo new "postName" #新建文章hexo new page "pageName" #新建页面hexo generate #生成静态页面至public目录hexo server #开启预览访问端口(默认端口4000,'ctrl + c'关闭server)hexo deploy #将.deploy目录部署到GitHubhexo help # 查看帮助hexo version #查看Hexo的版本hexo deploy -g #生成加部署hexo server -g #生成加预览#命令的简写hexo n == hexo newhexo g == hexo generatehexo s == hexo serverhexo d == hexo deploy","tags":[{"name":"hexo","slug":"hexo","permalink":"https://alexhuihui.github.io/tags/hexo/"},{"name":"blog","slug":"blog","permalink":"https://alexhuihui.github.io/tags/blog/"}]}]