1 分布式锁介绍
1.1 什么是分布式
一个大型的系统往往被分为几个子系统来做,一个子系统可以部署在一台机器的多个 JVM(java虚拟机) 上,也可以部署在多台机器上。但是每一个系统不是独立的,不是完全独立的。需要相互通信,共同实现业务功能。
一句话来说:分布式就是通过计算机网络将后端工作分布到多台主机上,多个主机一起协同完成工作。
1.2 什么是锁--作用安全
现实生活中,当我们需要保护一样东西的时候,就会使用锁。例如门锁,车锁等等。很多时候可能许多人会共用这些资源,就会有很多个钥匙。但是有些时候我们希望使用的时候是独自不受打扰的,那么就会在使用的时候从里面反锁,等使用完了再从里面解锁。这样其他人就可以继续使用了。
JAVA程序中,当存在多个线程可以同时改变某个变量(可变共享变量)时,就需要对变量或代码块做同步,使其在修改这种变量时能够线性执行消除并发修改变量,而同步的本质是通过锁来实现的。如 Java 中 synchronize 是在对象头设置标记,Lock 接口的实现类基本上都只是某一个 volitile 修饰的 int 型变量其保证每个线程都能拥有对该 int 的可见性和原子修改
1.3 什么是分布式锁
任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。CAP
当在分布式模型下,数据只有一份(或有限制),此时需要利用锁的技术控制某一时刻修改数据的进程数。
分布式锁: 在分布式环境下,多个程序/线程都需要对某一份(或有限制)的数据进行修改时,针对程序进行控制,保证同一时间节点下,只有一个程序/线程对数据进行操作的技术。
1.4 分布式锁具备的条件
互斥性:同一时刻只能有一个服务(或应用)访问资源,特殊情况下有读写锁
原子性:一致性要求保证加锁和解锁的行为是原子性的
安全性:锁只能被持有该锁的服务(或应用)释放,避免死锁
容错性:在持有锁的服务崩溃时,锁仍能得到释放避免死锁
可重用性:同一个客户端获得锁后可递归调用---重入锁和不可重入锁
公平性:看业务是否需要公平,避免饿死--公平锁和非公平锁
支持阻塞和非阻塞:和 ReentrantLock 一样支持 lock 和 trylock 以及 tryLock(long timeOut)---阻塞锁和非阻塞锁==PS:::自旋锁==
高可用:获取锁和释放锁 要高可用
高性能:获取锁和释放锁的性能要好
持久性:锁按业务需要自动续约/自动延期
2.分布式锁的解决方案
2.1 数据库实现分布式锁
2.1.1 基于数据库表实现
准备工作:创建tb_program表,用于记录当前哪个程序正在使用数据
CREATE TABLE `tb_program` (
`program_no` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '程序的编号'
PRIMARY KEY (`program_no`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
实现步骤:
程序访问数据时,将程序的编号(insert)存入tb_program表;
当insert成功,代表该程序获得了锁,即可执行逻辑;
当program_no相同的其他程序进行insert是,由于主键冲突会导致insert失败,则代表获取锁失败;
获取锁成功的程序在逻辑执行完以后,删除该数据,代表释放锁。
2.1.2 基于数据库的排它锁实现
除了可以通过增删操作数据表中的记录以外,其实还可以借助数据中自带的锁来实现分布式的锁。我们还用刚刚创建的那张数据库表,基于MySql的InnoDB引擎(MYSQL的引擎种类)可以通过数据库的排他锁来实现分布式锁。
实现步骤:
在查询语句后面增加for update,数据库会在查询过程中给数据库表增加排他锁。当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁
获得排它锁的线程即可获得分布式锁,执行方法的业务逻辑
执行完方法之后,再通过connection.commit();操作来释放锁。
实现代码
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.itheima</groupId>
<artifactId>mysql-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<!--依赖包-->
<dependencies>
<!-- MySql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.32</version>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Book
public class Book {
// 图书ID
private Integer id;
// 图书名称
private String name;
// 图书价格
private Float price;
// 图书图片
private String pic;
// 图书描述
private String desc;
}
BookDao
public interface BookDao {
/**
* 查询所有的book数据
* @return
*/
List<Book> queryBookList(String name) throws Exception;
}
BookDaoImpl实现类
public class BookDaoImpl implements BookDao {
/***
* 查询数据库数据
* @return
* @throws Exception
*/
public List<Book> queryBookList(String name) throws Exception{
// 数据库链接
Connection connection = null;
// 预编译statement
PreparedStatement preparedStatement = null;
// 结果集
ResultSet resultSet = null;
// 图书列表
List<Book> list = new ArrayList<Book>();
try {
// 加载数据库驱动
Class.forName("com.mysql.jdbc.Driver");
// 连接数据库
connection = DriverManager.getConnection("jdbc:mysql://192.168.200.129:3306/spring_db", "root", "root");
//关闭自动提交
connection.setAutoCommit(false);
// SQL语句
String sql = "SELECT * FROM book where id = 1 for update";
// 创建preparedStatement
preparedStatement = connection.prepareStatement(sql);
// 获取结果集
resultSet = preparedStatement.executeQuery();
// 结果集解析
while (resultSet.next()) {
Book book = new Book();
book.setId(resultSet.getInt("id"));
book.setName(resultSet.getString("name"));
list.add(book);
}
System.out.println(name + "执行了for update");
System.out.println("结果为:" + list);
//锁行后休眠5秒
Thread.sleep(5000);
//休眠结束释放
connection.commit();
System.out.println(name + "结束");
} catch (Exception e) {
e.printStackTrace();
}
return list;
}
}
测试类
public class Test {
private BookDao bookDao = new BookDaoImpl();
@org.junit.Test
public void testLock() throws Exception {
new Thread(new LockRunner("线程1")).start();
new Thread(new LockRunner("线程2")).start();
new Thread(new LockRunner("线程3")).start();
new Thread(new LockRunner("线程4")).start();
new Thread(new LockRunner("线程5")).start();
Thread.sleep(200000L);
}
class LockRunner implements Runnable {
private String name;
public LockRunner(String name) {
this.name = name;
}
public void run() {
try {
bookDao.queryBookList(name);
}catch (Exception e){
e.printStackTrace();
}
}
}
}
2.1.3 优点及缺点
优点:简单,方便,快速实现
缺点:基于数据库,开销比较大,对数据库性能可能会存在影响,服务数量比较多的情况下,数据库也要做集群,使用数据库加锁,锁状态不能同步到其他机器上面,使用insert的方式有一个同步的过程,如果访问从机是可以加到锁的。
2.2 Redis实现分布式锁
2.2.1 实现原理
基于 REDIS 的 SETNX()、EXPIRE() 、GETSET()方法做分布式锁
setnx():setnx 的含义就是 SET if Not Exists,其主要有两个参数 setnx(key, value)。该方法是原子性的。
如果 key 不存在,则设置当前 key 成功,返回 1;
如果当前 key 已经存在,则设置当前 key 失败,返回 0
expire():expire 设置过期时间,要注意的是 setnx 命令不能设置 key 的超时时间,只能通过 expire() 来对 key 设置。
getset():这个命令主要有两个参数 getset(key,newValue)。该方法是原子的,对 key 设置 newValue 这个值,并且返回 key 原来的旧值。假设 key 原来是不存在的,那么多次执行这个命令,会出现下边的效果:
getset(key, “value1”) 返回 null 此时 key 的值会被设置为 value1 getset(key, “value2”) 返回 value1 此时 key 的值会被设置为 value2
实现流程:
setnx(lockkey, 当前时间+过期超时时间),如果返回 1,则获取锁成功;如果返回 0 则没有获取到锁。
get(lockkey) 获取值 oldExpireTime ,并将这个 value 值与当前的系统时间进行比较,如果小于当前系统时间,则认为这个锁已经超时,可以允许别的请求重新获取。
计算 newExpireTime = 当前时间+过期超时时间,然后 getset(lockkey, newExpireTime) 会返回当前 lockkey 的值currentExpireTime。判断 currentExpireTime 与 oldExpireTime 是否相等,如果相等,说明当前 getset 设置成功,获取到了锁。如果不相等,说明这个锁又被别的请求获取走了,那么当前请求可以直接返回失败,或者继续重试。
在获取到锁之后,当前线程可以开始自己的业务处理,当处理完毕后,比较自己的处理时间和对于锁设置的超时时间,如果小于锁设置的超时时间,则直接执行 delete 释放锁;如果大于锁设置的超时时间,则不需要再锁进行处理。
在Linux虚拟机中创建redis容器
docker run -di --name=redis -p 6379:6379 redis
代码实现
pom.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.itheima</groupId>
<artifactId>redis</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>redis</name>
<description>redis实现分布式锁测试</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
RedisUtil工具类
@Component
public class RedisUtil {
//定义默认超时时间:单位毫秒
private static final Integer LOCK_TIME_OUT = 10000;
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 外部调用加锁方法
*/
public Boolean tryLock(String key, Long timeout) throws Exception{
//获取当前系统时间设置为开始时间
Long startTime = System.currentTimeMillis();
//设置返回默认值-false:加锁失败
boolean flag = false;
//死循环获取锁:1.获取锁成功退出 2.获取锁超时退出
while(true){
//判断是否超时
if((System.currentTimeMillis() - startTime) >= timeout){
break;
}else{
//获取锁
flag = lock(key);
//判断是否获取成功
if(flag){
break;
}else{
//休息0.1秒重试,降低服务压力
Thread.sleep(100);
}
}
}
return flag;
}
/**
* 加锁实现
* @param key
* @return
*/
private Boolean lock(String key){
return (Boolean) stringRedisTemplate.execute((RedisCallback) redisConnection -> {
//获取当前系统时间
Long time = System.currentTimeMillis();
//设置锁超时时间
Long timeout = time + LOCK_TIME_OUT + 1;
//setnx加锁并获取解锁结果
Boolean result = redisConnection.setNX(key.getBytes(), String.valueOf(timeout).getBytes());
//加锁成功返回true
if(result){
return true;
}
//加锁失败判断锁是否超时
if(checkLock(key, timeout)){
//getset设置值成功后,会返回旧的锁有效时间
byte[] newtime = redisConnection.getSet(key.getBytes(), String.valueOf(timeout).getBytes());
if(time > Long.valueOf(new String(newtime))){
return true;
}
}
//默认加锁失败
return false;
});
}
/**
* 释放锁
*/
public Boolean release(String key){
return (Boolean) stringRedisTemplate.execute((RedisCallback) redisConnection -> {
Long del = redisConnection.del(key.getBytes());
if (del > 0){
return true;
}
return false;
});
}
/**
* 判断锁是否超时
*/
private Boolean checkLock(String key, Long timeout){
return (Boolean) stringRedisTemplate.execute((RedisCallback) redisConnection -> {
//获取锁的超时时间
byte[] bytes = redisConnection.get(key.getBytes());
try {
//判断锁的有效时间是否大与当前时间
if(timeout > Long.valueOf(new String(bytes))){
return true;
}
}catch (Exception e){
e.printStackTrace();
return false;
}
return false;
});
}
}
RedisController测试类
@RestController
@RequestMapping(value = "/redis")
public class RedisController {
@Autowired
private RedisUtil redisUtil;
/**
* 获取锁
* @return
*/
@GetMapping(value = "/lock/{name}")
public String lock(@PathVariable(value = "name")String name) throws Exception{
Boolean result = redisUtil.tryLock(name, 3000L);
if(result){
return "获取锁成功";
}
return "获取锁失败";
}
/**
* 释放锁
* @param name
*/
@GetMapping(value = "/unlock/{name}")
public String unlock(@PathVariable(value = "name")String name){
Boolean result = redisUtil.release(name);
if(result){
return "释放锁成功";
}
return "释放锁失败";
}
}
2.2.2 优点及缺点
优点:性能极高
缺点:失效时间设置没有定值。设置的失效时间太短,方法没等执行完锁就自动释放了,那么就会产生并发问题。如果设置的时间太长,其他获取锁的线程就可能要平白的多等一段时间,用户体验会降低。
Redisson 解决加锁的超时时间的问题
2.3 Redisson 实现分布式锁
2.3.1 Redisson简介
github地址:https://github.com/redisson/redisson/
Redisson是架设在Redis基础上的一个Java驻内存数据网格(In-Memory Data Grid)。充分的利用了Redis键值数据库提供的一系列优势。
Redisson采用了基于NIO的Netty框架。
提供对Redis各种组态形式的连接功能。
提供操作原生的Redis数据结构封装
Redisson还实现了分布式锁Lock这样的更高阶应用场景
Redisson支持Redis 2.8以上版本,支持Java1.6+以上版本。
2.3.2 Redisson实现流程
线程去获取锁,获取成功: 执行lua脚本,保存数据到redis数据库。
线程去获取锁,获取失败: 一直通过while循环尝试获取锁,获取成功后,执行lua脚本,保存数据到redis数据库。
watch dog自动延期机制
在一个分布式环境下,假如一个线程获得锁后,突然服务器宕机了,那么这个时候在一定时间后这个锁会自动释放,你也可以设置锁的有效时间(不设置默认30秒),这样的目的主要是防止死锁的发生。
加锁成功后,根据Hash算法选择redis集群某一个节点,执行lua脚本保存到redis中
业务执行成功后,释放当前锁
2.3.3 Redisson实现
(1)添加依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.11.5</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.11.4</version>
</dependency>
(2)配置文件和Redisson配置类
@Bean
public Redisson redisson() {
Config config = new Config();
if (cacheManagerProperties.isCluster()) {
ClusterServersConfig clusterServersConfig = new ClusterServersConfig();
clusterServersConfig.addNodeAddress(cacheManagerProperties.getAddresses().split(","));
config.useClusterServers();
} else {
// 单机
config.useSingleServer().setAddress("redis://" + redisProperties.getHost() + ":" + redisProperties.getPort());
}
return (Redisson) Redisson.create(config);
}
(3)具体实现类
public RLock lock(String key, long leaseTime, TimeUnit unit, boolean isFair) {
RLock lock = getLock(key, isFair);
lock.lock(leaseTime, unit);
return lock;
}
public void unlock(Object lock) {
if (lock != null) {
if (lock instanceof RLock) {
RLock rLock = (RLock)lock;
if (rLock.isLocked()) {
rLock.unlock();
}
} else {
throw new RuntimeException("requires RLock type");
}
}
}
(4)业务代码中引入
package com.heima.alarm.controller;
import com.heima.redis.lock.RedissonDistributedLock;
import com.heima.redis.template.RedisRepository;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Description:
* @Version: V1.0
*/
@RestController
@RequestMapping("count")
public class RedisController {
@Autowired
RedisRepository redisRepository;
@Autowired(required = false)
RedissonDistributedLock lock;
@Autowired
Environment env;
@GetMapping
public String unCount() {
RLock rLock = lock.lock("redisson");
try {
int incr = (int) redisRepository.get("count:11111");
if (incr > 0) {
incr = incr - 1;
redisRepository.set("count:11111", incr);
System.out.println(env.getProperty("server.port") +"--当前商品库存:" + incr);
} else {
System.out.println(env.getProperty("server.port") +"当前商品----库存不足-----" + incr);
}
} finally {
lock.unlock(rLock);
}
return env.getProperty("server.port") + "SUCCESS";
}
}
(5)测试
启动 Jmeter测试
2.3.4 优点及缺点
优点:性能极高,看门狗解决续期问题,解决原子性问题
缺点:Master Slave集群下,可能导致锁上加锁现象
2.4 zookeeper实现分布式锁
2.4.1 zookeeper 锁相关基础知识
zookeeper 一般由多个节点构成(单数),采用 zab 一致性协议。因此可以将 zk 看成一个单点结构,对其修改数据其内部自动将所有节点数据进行修改而后才提供查询服务。
zookeeper 的数据以目录树的形式,每个目录称为 znode, znode 中可存储数据(一般不超过 1M),还可以在其中增加子节点。
子节点有三种类型。序列化节点,每在该节点下增加一个节点自动给该节点的名称上自增。临时节点,一旦创建这个 znode 的客户端与服务器失去联系,这个 znode 也将自动删除。最后就是普通节点。
Watch 机制,client 可以监控每个节点的变化,当产生变化会给 client 产生一个事件。
2.4.2 zookeeper 分布式锁的原理
获取和释放锁原理:利用临时节点与 watch 机制。每个锁占用一个普通节点 /lock,当需要获取锁时在 /lock 目录下创建一个临时节点,创建成功则表示获取锁成功,失败则 watch/lock 节点,有删除操作后再去争锁。临时节点好处在于当进程挂掉后能自动上锁的节点自动删除即取消锁。
获取锁的顺序原理:上锁为创建临时有序节点,每个上锁的节点均能创建节点成功,只是其序号不同。只有序号最小的可以拥有锁,如果这个节点序号不是最小的则 watch 序号比本身小的前一个节点 (公平锁)。
2.4.3 zookeeper实现分布式锁流程
简易流程
获取锁流程:
临时顺序节点 + 监听删除机制 == 分布式锁
核心思想:当客户端要获取锁,则创建节点,使用完锁,则删除该节点。
客户端获取锁时,在lock节点下创建临时顺序节点。
然后获取lock下面的所有子节点,客户端获取到所有的子节点之后,如果发现自己创建的子节点序号最小,那么就认为该客户端获取到了锁。使用完锁后,将该节点删除。
如果发现自己创建的节点并非lock所有子节点中最小的,说明自己还没有获取到锁,此时客户端需要找到比自己小的那个节点,同时对其注册事件监听器,监听删除事件。
如果发现比自己小的那个节点被删除,则客户端的 Watcher会收到相应通知,此时再次判断自己创建的节点是否是lock子节点中序号最小的,如果是则获取到了锁,如果不是则重复以上步骤继续获取到比自己小的一个节点 并注册监听。
2.4.4 Zookeeper 分布式锁-模拟12306售票案例
Curator实现分布式锁API
在Curator中有五种锁方案:
InterProcessSemaphoreMutex:分布式排它锁(非可重入锁)
InterProcessMutex:分布式可重入排它锁
InterProcessReadWriteLock:分布式读写锁
InterProcessMultiLock:将多个锁作为单个实体管理的容器
InterProcessSemaphoreV2:共享信号量
2.4.4 Zookeeper 分布式锁-模拟12306售票案例
Curator实现分布式锁API
在Curator中有五种锁方案:
InterProcessSemaphoreMutex:分布式排它锁(非可重入锁)
InterProcessMutex:分布式可重入排它锁
InterProcessReadWriteLock:分布式读写锁
InterProcessMultiLock:将多个锁作为单个实体管理的容器
InterProcessSemaphoreV2:共享信号量
1,创建线程进行加锁设置
// 通过 多线程 来模拟多台服务器卖票
public class Ticket12306 implements Runnable{
private int tickets = 10;//数据库的票数
private InterProcessMutex lock;
@Override
public void run() {
while(true){
//获取锁
try {
lock.acquire(3, TimeUnit.SECONDS);
if(tickets > 0){
System.out.println(Thread.currentThread()+":"+tickets);
Thread.sleep(100);
tickets--;
}
} catch (Exception e) {
e.printStackTrace();
}finally {
//释放锁
try {
lock.release();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
2,创建连接,并且初始化锁
public Ticket12306(){
//重试策略
RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000, 10);
//2.第二种方式
//CuratorFrameworkFactory.builder();
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("192.168.200.150:2181")
.sessionTimeoutMs(60 * 1000)
.connectionTimeoutMs(15 * 1000)
.retryPolicy(retryPolicy)
.build();
//开启连接
client.start();
lock = new InterProcessMutex(client,"/lock");
}
3,运行多个线程进行测试
public class LockTest {
public static void main(String[] args) {
Ticket12306 ticket12306 = new Ticket12306();
//创建客户端
Thread t1 = new Thread(ticket12306,"携程");
Thread t2 = new Thread(ticket12306,"飞猪");
t1.start();
t2.start();
}
}
2.4.5 优点及缺点
优点:
客户端如果出现宕机故障的话,锁可以马上释放
可以实现阻塞式锁,通过 watcher 监听,实现起来也比较简单
集群模式,稳定性比较高
缺点:
一旦网络有任何的抖动,Zookeeper 就会认为客户端已经宕机,就会断掉连接,其他客户端就可以获取到锁。
性能不高,因为每次在创建锁和释放锁的过程中,都要动态创建、销毁临时节点来实现锁功能。ZK 中创建和删除节点只能通过 Leader 服务器来执行,然后将数据同步到所有的 Follower 机器上。(zookeeper对外提供服务的只有leader)
2.5 总结
上面几种方式,哪种方式都无法做到完美。就像CAP一样,在复杂性、可靠性、性能等方面无法同时满足,所以,根据不同的应用场景选择最适合自己的才是王道。
从理解的难易程度角度(从低到高)
数据库 > 缓存 > Zookeeper
从实现的复杂性角度(从低到高)
Zookeeper >= 缓存 > 数据库
从性能角度(从高到低)
缓存 > Zookeeper >= 数据库
从可靠性角度(从高到低)
Zookeeper > 缓存 > 数据库