热门回答:
公司做了自己的分库分表组件。下面就自己的经验来看下分库分表的优点和碰到的问题!
分库分表的背景:随着信息数据的急剧增长。单点数据库会有宕机。或者单库单表性能低下。查询和存储效率低的问题。使用分库分表实现数据的分布存储。性能更好。适合现在数据量多。用户需求高的特点!
分库分表遇到的问题:
1。多库多表需要不重复的ID生成策略。但是数据重复!
解决方案:UUID。全局序列号等等!
2。如果是按照hash等方式实现的分库分表。可能难以扩展
解决方案:1。使用时间段或者ID等进行划分。可持续扩展(会带来别的问题)。2。数据重新迁移!
3。连接查询。统计等出现困难:
解决方案:1。按照某个指定的分库分表字段(ID)(分布在同一个库中)进行连接查询!2。将主要字段进行冗余。方便统计和连接查询!
分库分表的组件有很多。mycat是最流行的一个!
怎么使用mycat进行分库分表?
1。下载安装mycat。配置环境变量!
2。配置文件(启动内存等)。使用命令行启动和停止!
3。创建多库多表!
4。选择分库分表策略(水平和垂直)。算法等!
5。配置server.xml。schema.xml。rule.xml用于配置映射。规则等!
具体的mycat应用不是几句话可以说的清的。最好是自己搭建环境。自己写Demo测试。方能掌握!
我的Demo还在写。到时候分享。需要的朋友。敬请关注。。。
其他观点:
分库/分表
顾名思义。分库分表就是按照一定的规则。对原有的数据库和表进行拆分。把一个数据库分成多个库。把一张表拆分成为多张表。
为什么要分库/分表
随着时间和业务的发展。数据库和表中的数据量会越来越多。对硬件特别是磁盘消耗是越来越大。磁盘寻址、IO操作、系统内存都有很大的开销。为了提升数据库操作的效率。因此以空间换时间。打个比方。你把五颜六色的玻璃球放到一个篮子里。如果去找各种颜色球的数量是不是很麻烦。如果按照颜色分开放。找绿色的球直接去绿色区。不用顾及其他颜色。是不是效率就高了很多。就是这个原理
垂直分库/分表
垂直划分数据库是根据业务进行划分。将一张表的几个字段分成几张表。把大表变成小表。把一个大库按照业务 拆分几个专门库。比如商品。按照商品类别。商品详情。订单详情。订单 。划分到 商品库。订单库中 。通过降低单库(表)的大小来提高性能 放入不同的库中。
水平分库/分表
水平划分是根据一定规则。例如时间或hash值等进行数据的拆分。这种拆分都是行级别拆分。也就是所有拆分的都是完整的一条数据。比如把用户表按照年份拆分2018年注册的用户。2019年的用户。2018之前注册的用户。
MyCat
Mycat是一款开源的数据库中间件。主要是用作数据库代理。也就是消费数据库数据的消费方不直接接触数据库。通过mycat来代理访问。就像明星一样。你有商演想找明星。不会直接找到明星。会找到经纪人。经纪人来谈具体事宜。经纪人再告诉明星。
Mycat的原理中最重要是在sql执行前可以拦截处理一些逻辑。它拦截了用户发送过来的SQL语句。首先对SQL语句做了一些特定的分析:如分片分析、路由分析、读写分离分析、缓存分析等。然后将此SQL发往后端的真实数据库。并将返回的结果做适当的处理。最终再返回给用户。因此对于分库分表来说把分库分表的规则写入拦截器。拦截器通过分析来指向真实的地址获取数据。还可以将数据进行逻辑处理后返回消费方。这样就完成了分库分表的操作。这样就达成了对开发无感知的目的。避免分库分表造成了路由逻辑的编写。
以下就是网上找来的mycat分库分表的示意图。相信很容易看懂。
以上就是分库分表和mycat分库分表的简单介绍。如果深入了解可到mycat社区进一步学习。希望我的回答能够解决你的问题。如果有不同观点可留言讨论。点个赞关注一下吧。
其他观点:
MySQL的使用场景中。读写分离只是方案中的一部分。想要扩展。势必会用到分库分表。可喜的是Mycat里已经做到了。今天花时间测试了一下。感觉还不错。
关于分库分表
当然自己也理了一下。分库分表的这些内容。如果分成几个策略或者阶段。大概有下面的几种。
最上面的第一种是直接拆表。比如数据库db1下面有test1,test2,test3三个表。通过中间件看到的还是表test,里面的数据做了这样的拆分。能够在一定程度上分解压力。如果细细品来。和分区表的套路有些像。
接下来的几类也是不断完善。把表test拆解到多个库中。多个服务器中。如果做了读写分离。全套的方案这样的拆解改进还是很大的。如此来看。数据库中间件做了很多应用和数据库之间的很多事情。能够流行起来除了技术原因还是有很多其他的因素。
分库分表的测试环境模拟
如果要在一台服务器上测试分库分表。而且要求架构方案要全面。作为技术可行性的一个判定参考。是否可以实现呢。
如果模拟一主两从的架构。模拟服务分布在3台服务器上。这样的方案需要创建9个实例,每个实例上有3个db需要分别拆分。
大体的配置如下:
master1: 端口33091
(m1)slave1: 端口33092
(m1)slave2: 端口33093
master2: 端口33071
(m2)slave1: 端口33072
(m2)slave2: 端口33073
master3: 端口33061
(m3)slave1: 端口33062
(m3)slave2: 端口33063
画个图来说明一下。其中db1,db2,db3下面有若干表。需要做sharding
所以我们需要模拟的就是这个事情。
使用Mycat碰到的几个小问题解惑
使用Mycat的时候碰到了几个小问题。感觉比较有代表性。记录了一下。
问题1:
首先是使用Mycat连接到数据库之后。如果不切换到具体的数据库下。使用[数据库名].[表名]的方式会抛出下面的错误。可见整个过程中。Mycat拦截了SQL信息做了过滤。在转换的时候找不到目标路由。当然实际使用中。规范使用肯定不会有这个问题。
mysql> select * from db1.shard_auto;
ERROR 1064 (HY000): find no Route:select * from db1.shard_auto
问题2:
在配置了sharding策略之后。insert语句抛出了下面的错误。这个是对语法的一个基本的要求。
mysql> insert into shard_mod_long values(1,'aa',date);
ERROR 1064 (HY000): partition table, insert must provide ColumnList
问题3:
如果sharding策略配置有误。很可能出现表访问正常。但是DML会有问题。提示数据冲突了。至于如何配置sharding,下面会讲。
mysql> select * from shard_mod_long;
Empty set (0.00 sec)
mysql> insert into shard_mod_long(ID,name,shard_date) values(1,'aa',current_date);
ERROR 1105 (HY000): Duplicate entry '1' for key 'PRIMARY'
问题4:
如果sharding的配置有误。很可能出现多份冗余数据。
查看执行计划就一目了然。通过data_node可以看到数据指向了多个目标库。
mysql> explain insert into shard_auto(ID,name,shard_date) values(1,'aa',current_date);
+-----------+------------------------------------------------+
| DATA_NODE | SQL |
+-----------+------------------------------------------------+
| pxcNode11 | insert into shard_auto(ID,name,shard_date) values(1,'aa',current_date) |
| pxcNode21 | insert into shard_auto(ID,name,shard_date) values(1,'aa',current_date) |
| pxcNode31 | insert into shard_auto(ID,name,shard_date) values(1,'aa',current_date) |
+-----------+------------------------------------------------+
这种情况如果有一定的需求还是蛮不错的。做sharding可惜了。问题就在于下面的这个table配置。
<table name=\"shard_auto\" primaryKey=\"ID\" type=\"global\" dataNode=\"pxcNode11,pxcNode21,pxcNode31\" rule=\"auto-sharding-long\" />
需要去掉 type=\"global\"的属性。让它sharding。
Mycat里面的sharding策略
Mycat的分片策略很丰富。这个是超出自己的预期的。也是Mycat的一大亮点。
大体分片规则如下。另外还有一些其他分片方式这里不全部列举:
(1)分片枚举:sharding-by-intfile
(2)主键范围:auto-sharding-long
(3)一致性hash:sharding-by-murmur
(4)字符串hash解析:sharding-by-stringhash
(5)按日期(天)分片:sharding-by-date
(6)按单月小时拆分:sharding-by-hour
(7)自然月分片:sharding-by-month
在开始之前。我们要创建下面的表来模拟几个sharding的场景,表名根据需求可以改变。
create table shard_test(ID int primary key, name varchar(20),shard_date date);
主键范围分片
主键范围分片是参考了主键值。按照主键值的分布来分布数据库在不同的库中。我们先在对应的sharding节点上创建同样的表结构。
关于sharding的策略。需要修改rule.xml文件。
常用的sharding策略已经在Mycat里面实现了。如果要自行实现也可以定制。比如下面的规则。是基于主键字段ID来做sharding,分布的算法是rang-long,引用了function rang-long,这个function是在对应的一个Java类中实现的。
<tableRule name=\"auto-sharding-long\">
<rule>
<columns>ID</columns>
<algorithm>rang-long</algorithm>
</rule>
<function name=\"rang-long\"
class=\"io.mycat.route.function.AutoPartitionByLong\">
<property name=\"mapFile\">autopartition-long.txt</property>
当然主键的范围是不固定的。可以根据需求来定制。比如按照一百万为单位。或者1000位单位。文件是 autopartition-long.txt 文件的内容默认如下。模板里是分为了3个分片。如果要定制更多的就需要继续配置了。目前来看这个配置只能够承载15亿的数据量。可以根据需求继续扩展定制。
# range start-end ,data node index
# K=1000,M=10000.
0-500M=0
500M-1000M=1
1000M-1500M=2
插入一些数据来验证一下。我们可以查看执行计划来做基本的验证。配置无误。数据就根据规则流向了指定的数据库下的表里。
mysql> explain insert into shard_auto(ID,name,shard_date) values(1,'aa',current_date);
+-----------+------------------------------------------------+
| DATA_NODE | SQL |
+-----------+------------------------------------------------+
| pxcNode11 | insert into shard_auto(ID,name,shard_date) values(1,'aa',current_date) |
+-----------+------------------------------------------------+
还有一个查看sharding效果的小方法。比如我插入一个极大的值。保证和其他数据不在一个分片上。我们运行查询语句两次。结果会有点变化。
sharing的效果
mysql> select * from shard_auto;
+---------+------+------------+
| ID | name | shard_date |
+---------+------+------------+
| 1 | aa | 2017-09-06 |
| 2 | bb | 2017-09-06 |
| 5000001 | aa | 2017-09-06 |
+---------+------+------------+
3 rows in set (0.00 sec)
稍作停顿。继续运行。
mysql> select * from shard_auto;
+---------+------+------------+
| ID | name | shard_date |
+---------+------+------------+
| 5000001 | aa | 2017-09-06 |
| 1 | aa | 2017-09-06 |
| 2 | bb | 2017-09-06 |
+---------+------+------------+
3 rows in set (0.01 sec)
Hash分片
Hash分片其实企业级应用尤其广泛。我觉得一个原因是通过这种数据路由的方式。得到的数据情况是基本可控的。和业务的关联起来比较直接。很多拆分方法都是根据mod方法来平均分布数据。
sharding的策略在rule.xml里面配置。还是默认的mod-long规则。引用了算法mod-long,这里是根据sharding的节点数来做的。默认是3个。
<tableRule name=\"mod-long\">
<rule>
<columns>id</columns>
<algorithm>mod-long</algorithm>
</rule>
</tableRule>
<function name=\"mod-long\" class=\"io.mycat.route.function.PartitionByMod\">
<!-- how many data nodes -->
<property name=\"count\">3</property>
</function>
比如查看两次insert的结果情况。
mysql> explain insert into shard_mod_long(ID,name,shard_date) values(4,'dd',current_date);
+-----------+------------------------------------------------+
| DATA_NODE | SQL |
+-----------+------------------------------------------------+
| pxcNode22 | insert into shard_mod_long(ID,name,shard_date) values(4,'dd',current_date) |
+-----------+------------------------------------------------+
mysql> explain insert into shard_mod_long(ID,name,shard_date) values(5,'ee',current_date);
+-----------+------------------------------------------------+
| DATA_NODE | SQL |
+-----------+------------------------------------------------+
| pxcNode23 | insert into shard_mod_long(ID,name,shard_date) values(5,'ee',current_date) |
+-----------+------------------------------------------------+
可以看到数据还是遵循了节点的规律。平均分布。
至于schema.xml的配置。是整个分库的核心。我索性也给出一个配置来。供参考。
<?xml version=\"1.0\"?>
<!DOCTYPE mycat:schema SYSTEM \"schema.dtd\">
<mycat:schema xmlns:mycat=\"http://io.mycat/\">
<!-- 定义MyCat的逻辑库 -->
<schema name=\"db1\" checkSQLschema=\"false\" sqlMaxLimit=\"100\" >
<table name=\"shard_mod_long\" primaryKey=\"ID\" type=\"global\" dataNode=\"pxcNode11,pxcNode21,pxcNode31\" rule=\"mod-long\" />
<table name=\"shard_auto\" primaryKey=\"ID\" type=\"global\" dataNode=\"pxcNode11,pxcNode21,pxcNode31\" rule=\"auto-sharding-long\" />
</schema>
<!-- 定义MyCat的数据节点 -->
<dataNode name=\"pxcNode11\" dataHost=\"dtHost\" database=\"db1\" />
<dataNode name=\"pxcNode21\" dataHost=\"dtHost2\" database=\"db1\" />
<dataNode name=\"pxcNode31\" dataHost=\"dtHost3\" database=\"db1\" />
<!-- 定义数据主机dtHost。连接到MySQL读写分离集群 ,schema中的每一个dataHost中的host属性值必须唯一-->
<!-- dataHost实际上配置就是后台的数据库集群。一个datahost代表一个数据库集群 -->
<!-- balance=\"1\"。全部的readHost与stand by writeHost参与select语句的负载均衡-->
<!-- writeType=\"0\"。所有写操作发送到配置的第一个writeHost。这里就是我们的hostmaster。第一个挂了切到还生存的第二个writeHost-->
<dataHost name=\"dtHost\" maxCon=\"500\" minCon=\"20\" balance=\"1\"
writeType=\"0\" dbType=\"mysql\" dbDriver=\"native\" switchType=\"1\" slaveThreshold=\"100\">
<!--心跳检测 -->
<heartbeat>show slave status</heartbeat>
<!--配置后台数据库的IP地址和端口号。还有账号密码 -->
<writeHost host=\"hostMaster\" url=\"192.168.163.128:33091\" user=\"mycat_user\" password=\"mycat\" />
</dataHost>
<dataHost name=\"dtHost2\" maxCon=\"500\" minCon=\"20\" balance=\"1\"
writeType=\"0\" dbType=\"mysql\" dbDriver=\"native\" switchType=\"1\" slaveThreshold=\"100\">
<!--心跳检测 -->
<heartbeat>show slave status</heartbeat>
<!--配置后台数据库的IP地址和端口号。还有账号密码 -->
<writeHost host=\"hostMaster\" url=\"192.168.163.128:33071\" user=\"mycat_user\" password=\"mycat\" />
</dataHost>
<dataHost name=\"dtHost3\" maxCon=\"500\" minCon=\"20\" balance=\"1\"
writeType=\"0\" dbType=\"mysql\" dbDriver=\"native\" switchType=\"1\" slaveThreshold=\"100\">
<!--心跳检测 -->
<heartbeat>show slave status</heartbeat>
<!--配置后台数据库的IP地址和端口号。还有账号密码 -->
<writeHost host=\"hostMaster\" url=\"192.168.163.128:33061\" user=\"mycat_user\" password=\"mycat\" />
</dataHost>
</mycat:schema>
=================================================================================================
用Mycat。学会数据库读写分离、分表分库
php疑难杂症铺 2017-09-13 14:31
用Mycat。学会数据库读写分离、分表分库
系统开发中。数据库是非常重要的一个点。除了程序的本身的优化。如:SQL语句优化、代码优化。数据库的处理本身优化也是非常重要的。主从、热备、分表分库等都是系统发展迟早会遇到的技术问题问题。Mycat是一个广受好评的数据库中间件。已经在很多产品上进行使用了。希望通过这篇文章的介绍。能学会Mycat的使用。
安装
Mycat官网:http://www.mycat.io/
可以了解下Mycat的背景和应用情况。这样使用起来比较有信心。
Mycat下载地址:http://dl.mycat.io/
官网有个文档。属于详细的介绍。初次入门。看起来比较花时间。
下载:
建议大家选择 1.6-RELEASE 版本。毕竟是比较稳定的版本。
安装:
根据不同的系统选择不同的版本。包括linux、windows、mac,作者考虑还是非常周全的。当然。也有源码版的。(ps:源码版的下载后。只要配置正确。就可以正常运行调试。这个赞一下。)
Mycat的安装其实只要解压下载的目录就可以了。非常简单。
安装完成后。目录如下:
目录说明binmycat命令。启动、重启、停止等catletcatlet为Mycat的一个扩展功能confMycat 配置信息,重点关注libMycat引用的jar包。Mycat是java开发的logs日志文件。包括Mycat启动的日志和运行的日志。
配置
Mycat的配置文件都在conf目录里面。这里介绍几个常用的文件:
文件说明server.xmlMycat的配置文件。设置账号、参数等schema.xmlMycat对应的物理数据库和数据库表的配置rule.xmlMycat分片(分库分表)规则
Mycat的架构其实很好理解。Mycat是代理。Mycat后面就是物理数据库。和Web服务器的Nginx类似。对于使用者来说。访问的都是Mycat。不会接触到后端的数据库。
我们现在做一个主从、读写分离。简单分表的示例。结构如下图:
服务器IP说明Mycat192.168.0.2mycat服务器。连接数据库时。连接此服务器database1192.168.0.3物理数据库1。真正存储数据的数据库database2192.168.0.4物理数据库2。真正存储数据的数据库
Mycat作为主数据库中间件。肯定是与代码弱关联的。所以代码是不用修改的。使用Mycat后。连接数据库是不变的。默认端口是8066。连接方式和普通数据库一样。如:jdbc:mysql://192.168.0.2:8066/
server.xml
示例
重点关注下面这段。其他默认即可。
参数说明user用户配置节点--name登录的用户名。也就是连接Mycat的用户名--password登录的密码。也就是连接Mycat的密码--schemas数据库名。这里会和schema.xml中的配置关联。多个用逗号分开。例如需要这个用户需要管理两个数据库db1,db2。则配置db1,dbs--privileges配置用户针对表的增删改查的权限。具体见文档吧
我这里配置了一个账号test 密码也是test,针对数据库lunch,读写权限都有。没有针对表做任何特殊的权限。
schema.xml
schema.xml是最主要的配置项。首先看我的配置文件。
参数说明schema数据库设置。此数据库为逻辑数据库。name与server.xml中schema对应dataNode分片信息。也就是分库相关配置dataHost物理数据库。真正存储数据的数据库
每个节点的属性逐一说明:
schema:
属性说明name逻辑数据库名。与server.xml中的schema对应checkSQLschema数据库前缀相关设置。建议看文档。这里暂时设为folsesqlMaxLimitselect 时默认的limit。避免查询全表
table:
属性说明name表名。物理数据库中表名dataNode表存储到哪些节点。多个节点用逗号分隔。节点为下文dataNode设置的nameprimaryKey主键字段名。自动生成主键时需要设置autoIncrement是否自增rule分片规则名。具体规则下文rule详细介绍
dataNode
属性说明name节点名。与table中dataNode对应datahost物理数据库名。与datahost中name对应database物理数据库中数据库名
dataHost
属性说明name物理数据库名。与dataNode中dataHost对应balance均衡负载的方式writeType写入方式dbType数据库类型heartbeat心跳检测语句。注意语句结尾的分号要加。
应用场景
数据库分表分库
配置如下:
我在192.168.0.2、192.168.0.3均有数据库lunch。
lunchmenu、restaurant、userlunch、users这些表都只写入节点dn1。也就是192.168.0.2这个服务。而dictionary写入了dn1、dn2两个节点。也就是192.168.0.2、192.168.0.3这两台服务器。分片的规则为:mod-long。
主要关注rule属性。rule属性的内容来源于rule.xml这个文件。Mycat支持10种分表分库的规则。基本能满足你所需要的要求。这个必须赞一个。其他数据库中间件好像都没有这么多。
table中的rule属性对应的就是rule.xml文件中tableRule的name,具体有哪些分表和分库的实现。建议还是看下文档。我这里选择的mod-long就是将数据平均拆分。因为我后端是两台物理库。所以rule.xml中mod-long对应的function count为2。见下面部分代码:
数据库读写分离
配置如下:
这样的配置与前一个示例配置改动如下:
删除了table分配的规则,以及datanode只有一个
datahost也只有一台。但是writehost总添加了readhost,balance改为1。表示读写分离。
以上配置达到的效果就是102.168.0.2为主库。192.168.0.3为从库。
注意:Mycat主从分离只是在读的时候做了处理。写入数据的时候。只会写入到writehost。需要通过mycat的主从复制将数据复制到readhost。这个问题当时候我纠结了好久。数据写入writehost后。readhost一直没有数据。以为是自己配置的问题。后面才发现Mycat就没有实现主从复制的功能。毕竟数据库本身自带的这个功能才是最高效稳定的。
至于其他的场景。如同时主从和分表分库也是支持的了。只要了解这个实现以后再去修改配置。都是可以实现的。而热备及故障专业官方推荐使用haproxy配合一起使用。大家可以试试。
使用
Mycat的启动也很简单。启动命令在Bin目录:
如果在启动时发现异常。在logs目录中查看日志。
wrapper.log 为程序启动的日志。启动时的问题看这个
mycat.log 为脚本执行时的日志。SQL脚本执行报错后的具体错误内容,查看这个文件。mycat.log是最新的错误日志。历史日志会根据时间生成目录保存。
mycat启动后。执行命令不成功。可能实际上配置有错误。导致后面的命令没有很好的执行。
Mycat带来的最大好处就是使用是完全不用修改原有代码的。在mycat通过命令启动后。你只需要将数据库连接切换到Mycat的地址就可以了。如下面就可以进行连接了:
连接成功后可以执行sql脚本了。
所以。可以直接通过sql管理工具(如:navicat、datagrip)连接。执行脚本。我一直用datagrip来进行日常简单的管理。这个很方便。
Mycat还有一个管理的连接。端口号是9906.
连接后可以根据管理命令查看Mycat的运行情况。当然。喜欢UI管理方式的人。可以安装一个Mycat-Web来进行管理。有兴趣自行搜索。
简而言之。开发中使用Mycat和直接使用Mysql机会没有差别。
常见问题
使用Mycat后总会遇到一些坑。我将自己遇到的一些问题在这里列一下。希望能与大家有共鸣:
Mycat是不是配置以后。就能完全解决分表分库和读写分离问题?
Mycat配合数据库本身的复制功能。可以解决读写分离的问题。但是针对分表分库的问题。不是完美的解决。或者说。至今为止。业界没有完美的解决方案。
分表分库写入能完美解决。但是。不能完美解决主要是联表查询的问题。Mycat支持两个表联表的查询。多余两个表的查询不支持。 其实。很多数据库中间件关于分表分库后查询的问题。都是需要自己实现的。而且节本都不支持联表查询。Mycat已经算做地非常先进了。
分表分库的后联表查询问题。大家通过合理数据库设计来避免。
Mycat支持哪些数据库。其他平台如 .net、PHP能用吗?
官方说了。支持的数据库包括MySQL、SQL Server、Oracle、DB2、PostgreSQL 等主流数据库。很赞。
尽量用Mysql,我试过SQL Server。会有些小问题。因为部分语法有点差异。
Mycat 非JAVA平台如 .net、PHP能用吗?
可以用。这一点MyCat做的也很棒。
您还感兴趣的文章推荐- 我是一个创业小白,想要开一家店铺,大家有什么好的推荐吗?
- 初次创业,有什么好的建议,请高人指点下?
- 刚开始创业你们会选什么行业?
- 5-6万开店创业,做什么生意好?
- 文学上有种体裁叫古风,它和赋、诗词有何不同?
以上就是由互联网推广工程师 网创网 整理编辑的,如果觉得有帮助欢迎收藏转发~
本文地址:https://www.wangchuang8.com/164016.html,转载请说明来源于:网创推广网
声明:本站部分文章来自网络,如无特殊说明或标注,均为本站原创发布。如若本站内容侵犯了原著者的合法权益,可联系进行处理。分享目的仅供大家学习与参考,不代表本站立场。
评论(2)
数据库,数据,分片,节点,都是,规则,几个,端口,主从,多个
没想到大家都对Java开发分库分表需要解决的问题及mycat是怎样实现分库分表的?感兴趣,不过这这篇解答确实也是太好了
公司做了自己的分库分表组件。下面就自己的经验来看下分库分表的优点和碰到的问题!分库分表的背景:随着信息数据的急剧增长。单