浅入浅出 Spring Data Jpa(浅入浅出是成语吗)
mhr18 2024-10-09 12:23 30 浏览 0 评论
前言
目前在做的项目使用的是 Spring Data Jpa,以前都是使用 Mybatis ,前段时间研究了 JPA 的使用。
目前公司项目的架构许多的技术点是我没有实践过的,所以我这段时间在学习这些东西,从 2021-5-15 开始博客的更新尽量保证一周一篇。
下周更新 GraphQL 。
本文例子全部在 https://github.com/zhangpanqin/jpa-study ,数据库使用的是内存数据库H2。
JPA
认识JPA
JPA 是 Java Persistence API 的简称,定义了 Java 对象与数据库表的映射关系,以及定义运行时期怎么 CRUD 的接口规范。
Hibernate 提供了 JPA 的实现。除此之外还有别的实现,比如 Open Jpa 等等。
Spring Data 为数据访问提供了一个熟悉且一致的,基于Spring的编程模型,同时仍保留基础数据存储的特殊特征。
- Spring Data JPA 用于操作关系型数据库
- Spring Data MongoDB 用于操作 MongoDB
- Spring Data Elasticsearch 用于操作 Es
- Spring Data Redis 用于操作 Redis
Spring Data JPA 底层的 JPA 实现采用的是 Hibernate ,也可以说是封装了 Hibernate,提供了 Spring 统一的编程模型。
统一的编程模型是指:下面这段代码,可以操作 JPA,ES,Redis 等等,只是 Person 上的注解不一样。
又可以通过更换 CrudRepository 接口,提供更细粒度的不同数据库的数据控制。
public interface PersonRepository extends CrudRepository<Person, Long> {
List<Person> findByLastname(String lastname);
List<Person> findByFirstnameLike(String firstname);
}
JPA 常用注解介绍
使用 JPA 的时候不要使用数据库的外键,一是影响性能,二是不利于更换数据库。
不要使用 Hibernate 生成表结构,使用 flyway 组件,通过 SQL 来控制数据库表、索引,字段管理,flyway 灵活性更强
@Data
@Entity
@Table(name = "sys_user")
public class SysUserEntity extends BaseEntity {
private String nickname;
private Integer age;
/**
* name 定义的是关联表字段,referencedColumnName 是当前表中的主键字段
*/
@OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name = USER_ID,referencedColumnName = ID,foreignKey =@ForeignKey(NO_CONSTRAINT) )
private List<SysBlogEntity> sysBlogEntities;
}
@Entity
标记该类是一个 Entity ,被 JPA 管理
@Table
指定 Entity 与数据库中的那个表映射
@JoinColumn
指定了两个关联表之间使用哪两个字段关联
@Column
指定了Entity 字段与表的那个字段关联
@Id
指定主键字段
@GeneratedValue
指定主键的生成策略,下文详细介绍
@Transient
忽略字段与表字段的映射关系
@OneToMany
一对多关系指定,当前 Entity 与另一个 Entity 的映射关系。
/**
* name 定义的是关联表字段,referencedColumnName 是当前表中的主键字段
*/
@OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name = USER_ID,referencedColumnName = ID,foreignKey =@ForeignKey(NO_CONSTRAINT) )
private List<SysBlogEntity> sysBlogEntities;
@ManyToOne
参考 OneToMany
@ManyToMany
/**
* @JoinTable 指定中间表,及中间表中的字段映射
* @JoinColumn(name = ROLE_ID,referencedColumnName = ID) 指定了中间表的字段(name) 和另一个表那个字段关联(referencedColumnName)
*/
@ManyToMany
@JoinTable(name = SYS_USER_ROLE, joinColumns = {@JoinColumn(name = USER_ID, referencedColumnName = ID)},
inverseJoinColumns = {@JoinColumn(name = ROLE_ID,referencedColumnName = ID)})
private List<SysRoleEntity> sysRoleEntities;
@OneToOne
@OneToOne(optional=false)
@JoinColumn(name="CUSTREC_ID", unique=true, nullable=false, updatable=false)
private CustomerRecord customerRecord;
@Query
可以写 SQL 操作数据库
public interface SysBlogRepository extends JpaRepository<SysBlogEntity,Long> {
@Query(nativeQuery = true,value = "select * from sys_blog where user_id = :userId")
List<SysBlogEntity> findByUserId(Long userId);
@Query(nativeQuery = true ,value = "select * from sys_blog where title = :#{#sysBlogDTO.title}")
List<SysBlogEntity> findByTitle(@Param("sysBlogDTO") SysBlogDTO sysBlogDTO);
}
主键生成策略
/**
* strategy 取值
*
* AUTO 由程序控制,默认策略。Oracle 默认是 SEQUENCE,Mysql默认是 IDENTITY
*
* IDENTITY: 主键自增长,需要在表中定义表字段自增长,Mysql ,PostgreSQL,SQL Server 可以采用)
*
* SEQUENCE:使用序列作为主键 ,Oracle、PostgreSQL、DB2 可以使用
* @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "emailSeq")
* @SequenceGenerator(initialValue = 1, name = "emailSeq", sequenceName = "EMAIL_SEQUENCE")
* private long id;
* 然后再数据库创建一个序列 create sequence EMAIL_SEQUENCE;
* 当不在 SequenceGenerator 指定 sequenceName ,默认使用 Hibernate 提供的序列名称为 hibernate_sequence
*
* TABLE 一般不适用这一个
*/
public @interface GeneratedValue {
GenerationType strategy() default AUTO;
String generator() default "";
}
主键生成策略例子
@Data
@Table
@Entity
public class KeyGeneratorEntity {
@Id
@GeneratedValue(generator = "system-uuid")
@GenericGenerator(name = "system-uuid",strategy = "uuid")
private String id;
private String username;
}
@SpringBootTest
class KeyGeneratorRepositoryTest {
@Autowired
private KeyGeneratorRepository keyGeneratorRepository;
@Test
public void run(){
final KeyGeneratorEntity keyGeneratorEntity = new KeyGeneratorEntity();
keyGeneratorEntity.setUsername(LocalDateTime.now().toString());
keyGeneratorRepository.save(keyGeneratorEntity);
final List<KeyGeneratorEntity> all = keyGeneratorRepository.findAll();
// [KeyGeneratorEntity(id=ff808081796f260c01796f2616aa0000, username=2021-05-15T16:30:37.722)]
System.out.println(all);
}
}
hibernate 提供了以下主键生成策略
当 @GeneratedValue(strategy = GenerationType.SEQUENCE) 使用的是 SequenceStyleGenerator.class 控制主键生成。
当 @GenericGenerator(name = "system-uuid",strategy = "uuid") ,使用的是 UUIDHexGenerator.class
public class DefaultIdentifierGeneratorFactory
implements MutableIdentifierGeneratorFactory, Serializable, ServiceRegistryAwareService {
private ConcurrentHashMap<String, Class> generatorStrategyToClassNameMap = new ConcurrentHashMap<String, Class>();
@SuppressWarnings("deprecation")
public DefaultIdentifierGeneratorFactory() {
register( "uuid2", UUIDGenerator.class );
register( "guid", GUIDGenerator.class ); // can be done with UUIDGenerator + strategy
register( "uuid", UUIDHexGenerator.class ); // "deprecated" for new use
register( "uuid.hex", UUIDHexGenerator.class ); // uuid.hex is deprecated
register( "assigned", Assigned.class );
register( "identity", IdentityGenerator.class );
register( "select", SelectGenerator.class );
register( "sequence", SequenceStyleGenerator.class );
register( "seqhilo", SequenceHiLoGenerator.class );
register( "increment", IncrementGenerator.class );
register( "foreign", ForeignGenerator.class );
register( "sequence-identity", SequenceIdentityGenerator.class );
register( "enhanced-sequence", SequenceStyleGenerator.class );
register( "enhanced-table", TableGenerator.class );
}
public void register(String strategy, Class generatorClass) {
LOG.debugf( "Registering IdentifierGenerator strategy [%s] -> [%s]", strategy, generatorClass.getName() );
final Class previous = generatorStrategyToClassNameMap.put( strategy, generatorClass );
if ( previous != null ) {
LOG.debugf( " - overriding [%s]", previous.getName() );
}
}
}
Lazy 需要在一个事务内执行
public class Order1 {
@Id
@GeneratedValue
private Long id;
private String description;
@OneToMany(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id",referencedColumnName = "id",foreignKey =@ForeignKey(NO_CONSTRAINT))
private List<OrderItem> orderItemList;
}
// @Transactional(readOnly = true)
public List<Order1> listOrder(){
System.out.println("---------------------开始查询---------------------");
final List<Order1> all = orderRepository.findAll();
System.out.println("---------------------开始懒加载---------------------");
System.out.println(JSON.toJSONString(all));
return all;
}
当数据需要懒加载的时候,JPA不会查询 Lazy 的数据,只有在使用的时候才会查询,但是使用的时候需要和原来的查询在同一个事务中,不然会抛出以下异常
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.mflyyou.jpa.n1.Order1.orderItemList, could not initialize proxy - no Session
由于没有开启事务,orderRepository.findAll() 执行之后这个查询事务就关闭了,所以获取 Order1.orderItemList 的时候报错。
当添加事务注解 @Transactional ,整个方法在一个事务内执行,就不会报错了。
N+1 问题
@Entity
@Table(name = "order1")
@Data
public class Order1 {
@Id
@GeneratedValue
private Long id;
private String description;
@OneToMany(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id",referencedColumnName = "id",foreignKey =@ForeignKey(NO_CONSTRAINT))
private List<OrderItem> orderItemList;
}
@Transactional(readOnly = true)
public Order1 findOne(Long id){
System.out.println("---------------------开始查询---------------------");
final Optional<Order1> byId = orderRepository.findById(id);
System.out.println("---------------------开始懒加载---------------------");
System.out.println(JSON.toJSONString(byId.get()));
return byId.get();
}
当查询 Order1 的时候,实际上不会查询 orderItemList ,当使用 orderItemList 的时候再查询一次。
当 Order1 有 N 个关联属性的时候,就会查询 N 次来获取对应的数据。
当数据都处于 FetchType.LAZY 获取数据,就会产生懒加载问题
---------------------开始查询---------------------
---------------------开始查询---------------------
Hibernate:
select
order1x0_.id as id1_2_0_,
order1x0_.description as descript2_2_0_
from
order1 order1x0_
where
order1x0_.id=?
---------------------开始懒加载---------------------
Hibernate:
select
orderiteml0_.order_id as order_id3_3_0_,
orderiteml0_.id as id1_3_0_,
orderiteml0_.id as id1_3_1_,
orderiteml0_.name as name2_3_1_,
orderiteml0_.order_id as order_id3_3_1_,
orderiteml0_.price as price4_3_1_
from
order_item orderiteml0_
where
{
"description": "测试2021-05-15T17:35:50.349",
"id": 1,
"orderItemList": [
{
"id": 2,
"name": "ceshi2021-05-15T17:35:50.423",
"orderId": 1,
"price": 10
},
{
"id": 3,
"name": "ceshi2021-05-15T17:35:50.423",
"orderId": 1,
"price": 10
},
{
"id": 4,
"name": "ceshi2021-05-15T17:35:50.423",
"orderId": 1,
"price": 10
},
{
"id": 5,
"name": "ceshi2021-05-15T17:35:50.423",
"orderId": 1,
"price": 10
},
{
"id": 6,
"name": "ceshi2021-05-15T17:35:50.423",
"orderId": 1,
"price": 10
}
]
}
解决办法呢,可以使用 @OneToMany(fetch = FetchType.EAGER) 。
查询一次获取了全部数据
Hibernate:
select
order1x0_.id as id1_2_0_,
order1x0_.description as descript2_2_0_,
orderiteml1_.order_id as order_id3_3_1_,
orderiteml1_.id as id1_3_1_,
orderiteml1_.id as id1_3_2_,
orderiteml1_.name as name2_3_2_,
orderiteml1_.order_id as order_id3_3_2_,
orderiteml1_.price as price4_3_2_
from
order1 order1x0_
left outer join
order_item orderiteml1_
on order1x0_.id=orderiteml1_.order_id
where
order1x0_.id=?
当出现 orderItemList 和 orderItemList2 的时候,@OneToMany(fetch = FetchType.EAGER) 会报错
public class Order1 {
@Id
@GeneratedValue
private Long id;
private String description;
@OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name = "order_id",referencedColumnName = "id",foreignKey =@ForeignKey(NO_CONSTRAINT))
private List<OrderItem> orderItemList;
@OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name = "order_id",referencedColumnName = "id",foreignKey =@ForeignKey(NO_CONSTRAINT))
private List<OrderItem2> orderItemList2;
}
@NamedEntityGraph 和 @EntityGraph 可以解决 N+1 问题。又可以解决级联查询的时候,查询哪些成员变量,不查询哪些成员变量。让我们可以根据业务有更高的自由度查询数据。
EntityGraph
@NamedEntityGraph 定义查询的时候查询哪些数据,@EntityGraph 用于标记 Repository 使用哪个 NamedEntityGraph 。
@Entity
@Table(name = "order1")
@Data
@NamedEntityGraph(name = "searchOrderGraphItem",
attributeNodes = {
@NamedAttributeNode(value = "orderGraphItemList", subgraph = "OrderGraphItem_productGraphs"),
},
subgraphs = {
@NamedSubgraph(name = "OrderGraphItem_productGraphs", attributeNodes = {
@NamedAttributeNode(value = "productGraphs")
})
}
)
public class OrderGraph1 {
@Id
@GeneratedValue
private Long id;
private String description;
@OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name = "order_id", referencedColumnName = "id", foreignKey = @ForeignKey(NO_CONSTRAINT))
private Set<OrderGraphItem> orderGraphItemList;
@OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name = "order_id", referencedColumnName = "id", foreignKey = @ForeignKey(NO_CONSTRAINT))
private Set<OrderGraphItem2> orderGraphItemList2;
}
public interface OrderGraphRepository extends JpaRepository<OrderGraph1, Long> {
@EntityGraph(value = "searchOrderGraphItem", type = EntityGraph.EntityGraphType.FETCH)
OrderGraph1 findByIdEquals(Long id);
}
@Test
public void findById() {
final OrderGraph1 orderGraph1s = orderGraphService.findById(1L);
assertThat(orderGraph1s, notNullValue());
}
@EntityGraph 中指定的的 type 可以取值 FETCH与 LOAD
- FETCH 对于 NamedEntityGraph 定义的 attributeNodes 使用eager,未声明的使用 lazy
@EntityGraph(value = "searchOrderGraphItem", type = EntityGraph.EntityGraphType.FETCH)
OrderGraph1 findByIdEquals(Long id);
只会查询出 OrderGraph1 对应表中的字段和 orderGraphItemList。orderGraphItemList2 当用的时候才会查询。
- LOAD 对于 NamedEntityGraph 定义的 attributeNodes 使用eager,未声明的属性使用属性配置的 FetchType
@EntityGraph(value = "searchOrderGraphItem", type = EntityGraph.EntityGraphType.LOAD)
OrderGraph1 findByIdEquals(Long id);
审计功能
一般表中都会有,主键 id ,创建时间 ,更新事件,谁创建,谁更新,再加上乐观锁。
实现 AuditorAware,填充用户 id。
@Data
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {
@Id
private Long id;
/**
* 创建时间
*/
@CreatedDate
@Column(name = "create_date", updatable = false)
private Instant createDate;
/**
* 修改时间
*/
@LastModifiedDate
@Column(name = "update_date")
private Instant updateDate;
/**
* 被谁创建
*/
@CreatedBy
@Column(name = "create_by", updatable = false)
private Integer createBy;
/**
* 被谁修改
*/
@LastModifiedBy
@Column(name = "update_by")
private Integer updateBy;
/**
* 乐观锁
*/
@Version
@Column(name = "version")
private Long version = 0L;
}
@Component
public class MyAuditorAware implements AuditorAware<Integer> {
/**
* 获取当前登录的 id
*/
@Override
public Optional<Integer> getCurrentAuditor() {
// 在请求头获取登录标识,再查询用户主键 id
return Optional.ofNullable(100);
}
}
乐观锁,更新的version 必须等于数据库中的版本,否则更新会抛出异常。也可以使用 Spring-retry 捕获 ObjectOptimisticLockingFailureException 重试更新。
@Data
@Entity
@Table(name = "sys_user")
public class SysUserEntity extends BaseEntity {
private String nickname;
private Integer age;
}
@SpringBootTest
class JpaStudyApplicationTests {
private static final Long USER_ID_EQUALS_1 = 1L;
private static final Long USER_ID_EQUALS_2 = 2L;
private static final Long USER_ID_EQUALS_3 = 3L;
@Resource
private SysUserRepository sysUserRepository;
private SysUserEntity saveSysUserEntity;
private SysUserEntity saveSysUserEntity2;
private SysUserEntity saveSysUserEntity3;
@BeforeEach
public void beforeEach() {
saveSysUserEntity = new SysUserEntity();
saveSysUserEntity.setAge(10);
saveSysUserEntity.setId(USER_ID_EQUALS_1);
saveSysUserEntity.setNickname("测试");
saveSysUserEntity.setVersion(10L);
saveSysUserEntity2 = new SysUserEntity();
saveSysUserEntity2.setAge(10);
saveSysUserEntity2.setId(USER_ID_EQUALS_2);
saveSysUserEntity2.setNickname("测试");
saveSysUserEntity2.setVersion(10L);
saveSysUserEntity3 = new SysUserEntity();
saveSysUserEntity3.setAge(10);
saveSysUserEntity3.setId(USER_ID_EQUALS_3);
saveSysUserEntity3.setNickname("测试");
saveSysUserEntity3.setVersion(10L);
}
@Test
public void should_update_error() {
sysUserRepository.save(saveSysUserEntity);
final Optional<SysUserEntity> byId = sysUserRepository.findById(USER_ID_EQUALS_1);
assertThat(byId.isPresent(), is(Boolean.TRUE));
final SysUserEntity sysUserEntity = byId.get();
// 设置版本 2 也报错
// sysUserEntity.setVersion(2L);
sysUserEntity.setVersion(12L);
sysUserEntity.setNickname("乐观锁更新" + LocalDateTime.now().toString());
final Executable executable = () -> sysUserRepository.save(sysUserEntity);
assertThrows(ObjectOptimisticLockingFailureException.class, executable);
}
@Test
public void should_update_success() {
sysUserRepository.save(saveSysUserEntity2);
Optional<SysUserEntity> byId = sysUserRepository.findById(USER_ID_EQUALS_2);
assertThat(byId.isPresent(), is(Boolean.TRUE));
SysUserEntity sysUserEntity = new SysUserEntity();
sysUserEntity.setId(USER_ID_EQUALS_2);
sysUserEntity.setVersion(10L);
sysUserEntity.setNickname("乐观锁更新" + LocalDateTime.now().toString());
SysUserEntity save = sysUserRepository.save(sysUserEntity);
assertThat(save.getVersion(), is(11L));
}
}
相关推荐
- Java培训机构,你选对了吗?(java培训机构官网)
-
如今IT行业发展迅速,不仅是大学生,甚至有些在职的员工都想学习java开发,需求量的扩大,薪资必定增长,这也是更多人选择java开发的主要原因。不过对于没有基础的学员来说,java技术不是一两天就能...
- 产品经理MacBook软件清单-20个实用软件
-
三年前开始使用MacBookPro,从此再也不想用Windows电脑了,作为生产工具,MacBook可以说是非常胜任。作为产品经理,值得拥有一台MacBook。MacBook是工作平台,要发挥更大作...
- RAD Studio(Delphi) 本月隆重推出新的版本12.3
-
#在头条记录我的2025#自2024年9月,推出Delphi12.2版本后,本月隆重推出新的版本12.3,RADStudio12.3,包含了Delphi12.3和C++builder12.3最...
- 图解Java垃圾回收机制,写得非常好
-
什么是自动垃圾回收?自动垃圾回收是一种在堆内存中找出哪些对象在被使用,还有哪些对象没被使用,并且将后者删掉的机制。所谓使用中的对象(已引用对象),指的是程序中有指针指向的对象;而未使用中的对象(未引用...
- Centos7 初始化硬盘分区、挂载(针对2T以上)添加磁盘到卷
-
1、通过命令fdisk-l查看硬盘信息:#fdisk-l,发现硬盘为/dev/sdb大小4T。2、如果此硬盘以前有过分区,则先对磁盘格式化。命令:mkfs.文件系统格式-f/dev/sdb...
- 半虚拟化如何提高服务器性能(虚拟化 半虚拟化)
-
半虚拟化是一种重新编译客户机操作系统(OS)将其安装在虚拟机(VM)上的一种虚拟化类型,并在主机操作系统(OS)运行的管理程序上运行。与传统的完全虚拟化相比,半虚拟化可以减少开销,并提高系统性能。虚...
- HashMap底层实现原理以及线程安全实现
-
HashMap底层实现原理数据结构:HashMap的底层实现原理主要依赖于数组+链表+红黑树的结构。1、数组:HashMap最底层是一个数组,称为table,它存放着键值对。2、链...
- long和double类型操作的非原子性探究
-
前言“深入java虚拟机”中提到,int等不大于32位的基本类型的操作都是原子操作,但是某些jvm对long和double类型的操作并不是原子操作,这样就会造成错误数据的出现。其实这里的某些jvm是指...
- 数据库DELETE 语句,还保存原有的磁盘空间
-
MySQL和Oracle的DELETE语句与数据存储MySQL的DELETE操作当你在MySQL中执行DELETE语句时:逻辑删除:数据从表中标记为删除,不再可见于查询结果物理...
- 线程池—ThreadPoolExecutor详解(线程池实战)
-
一、ThreadPoolExecutor简介在juc-executors框架概述的章节中,我们已经简要介绍过ThreadPoolExecutor了,通过Executors工厂,用户可以创建自己需要的执...
- navicat如何使用orcale(详细步骤)
-
前言:看过我昨天文章的同鞋都知道最近接手另一个国企项目,数据库用的是orcale。实话实说,也有快三年没用过orcale数据库了。这期间问题不断,因为orcale日渐消沉,网上资料也是真真假假,难辨虚...
- 你的程序是不是慢吞吞?GraalVM来帮你飞起来性能提升秘籍大公开
-
各位IT圈内外的朋友们,大家好!我是你们的老朋友,头条上的IT技术博主。不知道你们有没有这样的经历:打开一个软件,半天没反应;点开一个网站,图片刷不出来;或者玩个游戏,卡顿得想砸电脑?是不是特别上火?...
- 大数据正当时,理解这几个术语很重要
-
目前,大数据的流行程度远超于我们的想象,无论是在云计算、物联网还是在人工智能领域都离不开大数据的支撑。那么大数据领域里有哪些基本概念或技术术语呢?今天我们就来聊聊那些避不开的大数据技术术语,梳理并...
- 秒懂列式数据库和行式数据库(列式数据库的特点)
-
行式数据库(Row-Based)数据按行存储,常见的行式数据库有Mysql,DB2,Oracle,Sql-server等;列数据库(Column-Based)数据存储方式按列存储,常见的列数据库有Hb...
- AMD发布ROCm 6.4更新:带来了多项底层改进,但仍不支持RDNA 4
-
AMD宣布,对ROCm软件栈进行了更新,推出了新的迭代版本ROCm6.4。这一新版本里,AMD带来了多项底层改进,包括更新改进了ROCm的用户空间库和AMDKFD内核驱动程序之间的兼容性,使其更容易...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- oracle位图索引 (74)
- oracle批量插入数据 (65)
- oracle事务隔离级别 (59)
- oracle 空为0 (51)
- oracle主从同步 (56)
- oracle 乐观锁 (53)
- redis 命令 (78)
- php redis (88)
- redis 存储 (66)
- redis 锁 (69)
- 启动 redis (66)
- redis 时间 (56)
- redis 删除 (67)
- redis内存 (57)
- redis并发 (52)
- redis 主从 (69)
- redis 订阅 (51)
- redis 登录 (54)
- redis 面试 (58)
- 阿里 redis (59)
- redis 搭建 (53)
- redis的缓存 (55)
- lua redis (58)
- redis 连接池 (61)
- redis 限流 (51)