转载请注明出处: http://www.voidcn.com/article/p-eyavkxup-bkz.html
通过java程序去连接数据库时,使用的协议是TCP/IP协议,TCP/IP协议需要进行3次握手。如果每一次数据库操作都需要创建一个新的连接,都要进行3次握手,这是十分浪费资源的,程序的效率也不是很高。为了解决这个问题,我们想可不可以自己维护一些数据库连接,需要数据库操作的时候,直接使用这其中的一个连接,用完了,在还给它,这样的话就不需要每次数据库操作都创建一个新的连接了。这种思维模式就是今天的博客主题数据库连接池。
基本原理
连接池技术的核心思想是:连接复用,通过建立一个数据库连接池以及一套连接使用、分配、管理策略,使得该连接池中的连接可以得到高效、安全的复用,避免了数据库连接频繁建立、关闭的开销。另外,由于对JDBC中的原始连接进行了封装,从而方便了数据库应用对于连接的使用(特别是对于事务处理),提高了开发效率,也正是因为这个封装层的存在,隔离了应用的本身的处理逻辑和具体数据库访问逻辑,使应用本身的复用成为可能。连接池主要由三部分组成(如下图所示):连接池的建立、连接池中连接的使用管理、连接池的关闭。
连接池实现
这里的介绍的连接池是proxool,这里对其做了进一步的封装,使数据库操作更加简单。
DBPool
DBPool类用于指定数据库连接池的配置文件,源程序如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| 1 /**
2 *@Description: 数据库连接池配置
3 */
4package com.lulei.db.manager;
5
6import org.apache.log4j.Logger;
7
8import com.lulei.util.ClassUtil;
9
10public class DBPool {
11 private static DBPool dbPool = null;
12 private String poolPath;
13 private static Logger log = Logger.getLogger(DBPool.class);
14 private static String path = ClassUtil.getClassRootPath(DBPool.class);
15
16 public static DBPool getDBPool(){
17 if (dbPool == null){
18 synchronized(DBPool.class){
19 if (dbPool == null){
20 dbPool = new DBPool();
21 }
22 }
23 }
24 return dbPool;
25 }
26
27 private DBPool(){
28
29 }
30
31 /**
32 * @param poolPath
33 * @Author: lulei
34 * @Description: 设置数据库连接池配置文件路径
35 */
36 public void setPoolPath(String poolPath){
37 this.poolPath = poolPath;
38 }
39
40 /**
41 * @return
42 * @Author: lulei
43 * @Description: 返回数据库连接池配置文件路径
44 */
45 protected String getPoolPath(){
46 //如果没有指定配置文件,则使用默认配置文件
47 if (poolPath == null){
48 poolPath = path + "proxool.xml";
49 log.info("Database's poolpath is null, use default path:" + poolPath);
50 }
51 return poolPath;
52 }
53}
54
55 |
fatal-sql-exception: 它是一个逗号分割的信息片段.当一个SQL异常发生时,他的异常信息将与这个信息片段进行比较.如果在片段中存在,那么这个异常将被认为是个致命错误(Fatal SQL Exception ).这种情况下,数据库连接将要被放弃.无论发生什么,这个异常将会被重掷以提供给消费者.用户最好自己配置一个不同的异常来抛出.
fatal-sql-exception-wrapper-class:正如上面所说,你最好配置一个不同的异常来重掷.利用这个属性,用户可以包装SQLException,使他变成另外一个异常.这个异常或者继承SQLException或者继承字RuntimeException.proxool自带了2个实现:'org.logicalcobwebs.proxool.FatalSQLException' 和'org.logicalcobwebs.proxool.FatalRuntimeException' .后者更合适.
house-keeping-sleep-time: house keeper 保留线程处于睡眠状态的最长时间,house keeper 的职责就是检查各个连接的状态,并判断是否需要销毁或者创建.
house-keeping-test-sql: 如果发现了空闲的数据库连接.house keeper 将会用这个语句来测试.这个语句最好非常快的被执行.如果没有定义,测试过程将会被忽略。
injectable-connection-interface: 允许proxool实现被代理的connection对象的方法.
injectable-statement-interface: 允许proxool实现被代理的Statement 对象方法.
injectable-prepared-statement-interface: 允许proxool实现被代理的PreparedStatement 对象方法.
injectable-callable-statement-interface: 允许proxool实现被代理的CallableStatement 对象方法.
jmx: 略
jmx-agent-id: 略
jndi-name: 数据源的名称
maximum-active-time: 如果housekeeper 检测到某个线程的活动时间大于这个数值.它将会杀掉这个线程.所以确认一下你的服务器的带宽.然后定一个合适的值.默认是5分钟.
maximum-connection-count: 最大的数据库连接数.
maximum-connection-lifetime: 一个线程的最大寿命.
minimum-connection-count: 最小的数据库连接数
overload-without-refusal-lifetime: 略
prototype-count: 连接池中可用的连接数量.如果当前的连接池中的连接少于这个数值.新的连接将被建立(假设没有超过最大可用数).例如.我们有3个活动连接2个可用连接,而我们的prototype-count是4,那么数据库连接池将试图建立另外2个连接.这和 minimum-connection-count不同. minimum-connection-count把活动的连接也计算在内.prototype-count 是spare connections 的数量.
recently-started-threshold: 略
simultaneous-build-throttle: 略
statistics: 连接池使用状况统计。 参数“10s,1m,1d”
statistics-log-level: 日志统计跟踪类型。 参数“ERROR”或 “INFO”
test-before-use: 略
test-after-use: 略
trace: 如果为true,那么每个被执行的SQL语句将会在执行期被log记录(DEBUG LEVEL).你也可以注册一个ConnectionListener (参看ProxoolFacade)得到这些信息.
verbose: 详细信息设置。 参数 bool 值
在本例中数据库连接池配置文件如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| 1<?xml version="1.0" encoding="UTF-8"?>
2<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
3<proxool-config>
4<proxool>
5 <alias>novelSelect</alias>
6 <driver-url><![CDATA[jdbc:mysql://172.20.37.73:3306/novel?characterEncoding=utf-8]]></driver-url>
7 <driver-class>com.mysql.jdbc.Driver</driver-class>
8 <driver-properties>
9 <property name="user" value="root"/>
10 <property name="password" value="root"/>
11 </driver-properties>
12 <house-keeping-sleep-time>900000</house-keeping-sleep-time>
13 <maximum-active-time>500000</maximum-active-time>
14 <maximum-connection-count>40</maximum-connection-count>
15 <minimum-connection-count>4</minimum-connection-count>
16 <house-keeping-test-sql>select 1</house-keeping-test-sql>
17 <prop key="hibernate.connection.release_mode">after_transaction</prop>
18</proxool>
19<proxool>
20 <alias>novelEdit</alias>
21 <driver-url><![CDATA[jdbc:mysql://172.20.37.73:3306/novel?characterEncoding=utf-8]]></driver-url>
22 <driver-class>com.mysql.jdbc.Driver</driver-class>
23 <driver-properties>
24 <property name="user" value="root"/>
25 <property name="password" value="root"/>
26 </driver-properties>
27 <house-keeping-sleep-time>900000</house-keeping-sleep-time>
28 <maximum-active-time>500000</maximum-active-time>
29 <maximum-connection-count>10</maximum-connection-count>
30 <minimum-connection-count>4</minimum-connection-count>
31 <house-keeping-test-sql>select 1</house-keeping-test-sql>
32 <prop key="hibernate.connection.release_mode">after_transaction</prop>
33</proxool>
34</proxool-config>
35
36 |
DBManager在系统中是单例模式,在初始化只需要简单的两句代码:
1 2 3
| 1 JAXPConfigurator.configure(DBPool.getDBPool().getPoolPath(), false);
2 Class.forName("org.logicalcobwebs.proxool.ProxoolDriver");
3 |
1 2
| 1return DriverManager.getConnection(poolName);
2 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| 1 /**
2 *@Description: 数据库连接池管理
3 */
4package com.lulei.db.manager;
5
6import java.sql.Connection;
7import java.sql.DriverManager;
8import java.sql.SQLException;
9
10import org.logicalcobwebs.proxool.configuration.JAXPConfigurator;
11
12public class DBManager {
13
14 private static DBManager dBManager = null;
15
16 private DBManager(){
17 try {
18 JAXPConfigurator.configure(DBPool.getDBPool().getPoolPath(), false);
19 Class.forName("org.logicalcobwebs.proxool.ProxoolDriver");
20 } catch (Exception e){
21 e.printStackTrace();
22 }
23 }
24
25 /**
26 * @return DBManager
27 * @Author: lulei
28 * @Description: 获取数据库连接池管理对象
29 */
30 protected static DBManager getDBManager(){
31 if (dBManager == null){
32 synchronized(DBManager.class){
33 if (dBManager == null){
34 dBManager = new DBManager();
35 }
36 }
37 }
38 return dBManager;
39 }
40
41 /**
42 * @param poolName
43 * @return Connection
44 * @throws SQLException
45 * @Author: lulei
46 * @Description: 获取数据库链接
47 */
48 protected Connection getConnection(String poolName) throws SQLException{
49 return DriverManager.getConnection(poolName);
50 }
51}
52
53 |
为了简化数据库的操作,对数据库操作进行再一次封装成DBOperation类。在setPres方法中,这里只做了几种简单的数据类型,关于其他复杂的数据类型可以根据项目需要添加。源代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195
| 1 /**
2 *@Description: 数据库操作
3 */
4package com.lulei.db.manager;
5
6import java.sql.Connection;
7import java.sql.PreparedStatement;
8import java.sql.ResultSet;
9import java.sql.SQLException;
10import java.sql.Statement;
11import java.util.HashMap;
12
13import org.apache.log4j.Logger;
14
15public class DBOperation {
16
17 private static Logger log = Logger.getLogger(DBOperation.class);
18 private Connection conn = null;
19 private String poolName;
20
21 /**
22 * @param poolName
23 */
24 public DBOperation(String poolName){
25 this.poolName = poolName;
26 }
27
28 /**
29 * @throws SQLException
30 * @Author: lulei
31 * @Description: 获取Connection
32 */
33 private void open() throws SQLException{
34 this.conn = DBManager.getDBManager().getConnection(poolName);
35 }
36
37 /**
38 * @Author: lulei
39 * @Description: 关闭Connection
40 */
41 public void close() {
42 try {
43 if (this.conn != null) {
44 this.conn.close();
45 }
46 } catch (SQLException e) {
47 // TODO Auto-generated catch block
48 e.printStackTrace();
49 }
50 }
51
52 /**
53 * @param sql组装的sql字符串
54 * @param params传入的参数
55 * @throws SQLException
56 * @throws ClassNotFoundException
57 * @Author: lulei
58 * @Description: 组装PreparedStatement
59 */
60 private PreparedStatement setPres(String sql, HashMap<Integer, Object> params) throws SQLException, ClassNotFoundException{
61 if (null != params) {
62 if (0 < params.size()){
63 PreparedStatement pres = this.conn.prepareStatement(sql);
64 for (int i = 1; i <= params.size(); i++){
65 if (params.get(i).getClass() == Class.forName("java.lang.String")){
66 pres.setString(i, params.get(i).toString());
67 } else if (params.get(i).getClass() == Class.forName("java.lang.Integer")){
68 pres.setInt(i, (Integer) params.get(i));
69 } else if (params.get(i).getClass() == Class.forName("java.lang.Boolean")){
70 pres.setBoolean(i, (Boolean) params.get(i));
71 } else if (params.get(i).getClass() == Class.forName("java.lang.Float")){
72 pres.setFloat(i, (Float) params.get(i));
73 } else if (params.get(i).getClass() == Class.forName("java.lang.Double")){
74 pres.setDouble(i, (Double) params.get(i));
75 } else if (params.get(i).getClass() == Class.forName("java.lang.Long")){
76 pres.setLong(i, (Long) params.get(i));
77 } else if (params.get(i).getClass() == Class.forName("java.sql.Date")){
78 pres.setDate(i, java.sql.Date.valueOf(params.get(i).toString()));
79 } else {
80 log.info("not found class : " + params.get(i).getClass().toString());
81 return null;
82 }
83 }
84 return pres;
85 }
86 }
87 return null;
88 }
89
90 /**
91 * @param sql
92 * @return int
93 * @throws SQLException
94 * @Author: lulei
95 * @Description: executeUpdate
96 */
97 protected int executeUpdate(String sql) throws SQLException{
98 this.open();
99 Statement state = this.conn.createStatement();
100 int re = state.executeUpdate(sql);
101 return re;
102 }
103
104 /**
105 * executeUpdate
106 * @param sql
107 * @param params
108 * @return int
109 * @throws SQLException
110 * @throws ClassNotFoundException
111 * @Author: lulei
112 * @Description:
113 */
114 protected int executeUpdate(String sql, HashMap<Integer, Object> params) throws SQLException, ClassNotFoundException{
115 this.open();
116 PreparedStatement pres = setPres(sql, params);
117 int re = 0;
118 if (null != pres) {
119 re = pres.executeUpdate();
120 }
121 return re;
122 }
123
124 /**
125 * getGeneratedKeys
126 * @param sql
127 * @return ResultSet
128 * @throws SQLException
129 * @Author: lulei
130 * @Description:
131 */
132 protected ResultSet getGeneratedKeys(String sql) throws SQLException{
133 this.open();
134 Statement state = this.conn.createStatement();
135 state.executeUpdate(sql, Statement.RETURN_GENERATED_KEYS);
136 ResultSet re = state.getGeneratedKeys();
137 return re;
138 }
139
140 /**
141 * getGeneratedKeys
142 * @param sql
143 * @param params
144 * @return ResultSet
145 * @throws SQLException
146 * @throws ClassNotFoundException
147 * @Author: lulei
148 * @Description:
149 */
150 protected ResultSet getGeneratedKeys(String sql, HashMap<Integer, Object> params) throws SQLException, ClassNotFoundException{
151 this.open();
152 PreparedStatement pres = setPres(sql, params);
153 if (null != pres) {
154 pres.executeUpdate(sql, Statement.RETURN_GENERATED_KEYS);
155 ResultSet re = pres.getGeneratedKeys();
156 return re;
157 }
158 return null;
159 }
160
161 /**
162 * @param sql
163 * @return ResultSet
164 * @throws SQLException
165 * @Author: lulei
166 * @Description: executeQuery
167 */
168 protected ResultSet executeQuery(String sql) throws SQLException{
169 this.open();
170 Statement state = this.conn.createStatement();
171 ResultSet re = state.executeQuery(sql);
172 return re;
173 }
174
175 /**
176 * @param sql
177 * @param params
178 * @return ResultSet
179 * @throws SQLException
180 * @throws ClassNotFoundException
181 * @Author: lulei
182 * @Description: executeQuery
183 */
184 protected ResultSet executeQuery(String sql, HashMap<Integer, Object> params) throws SQLException, ClassNotFoundException{
185 this.open();
186 PreparedStatement pres = setPres(sql, params);
187 if (null != pres) {
188 ResultSet re = pres.executeQuery();
189 return re;
190 }
191 return null;
192 }
193}
194
195 |
DBServer对数据库的增删该查操作进行进一步的细化,源代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214
| 1 /**
2 *@Description: 增删改查四个数据库操作接口
3 */
4package com.lulei.db.manager;
5
6import java.sql.ResultSet;
7import java.sql.SQLException;
8import java.util.HashMap;
9
10public class DBServer {
11
12 private DBOperation dBOperation;
13
14 /**
15 * @param poolName
16 * @Description: 在使用该类之前,请保证函数DBPool.getDBPool().setPoolPath()已经运行
17 */
18 public DBServer(String poolName){
19 dBOperation = new DBOperation(poolName);
20 }
21
22 /**
23 * @Author: lulei
24 * @Description: 释放链接,在执行完数据库操作,必须执行此命令
25 */
26 public void close(){
27 dBOperation.close();
28 }
29
30 /**
31 * @param table
32 * @param columns
33 * @param params
34 * @return int
35 * @throws SQLException
36 * @throws ClassNotFoundException
37 * @Author: lulei
38 * @Description: insert 执行完此命令后,执行close()操作
39 */
40 public int insert(String table, String columns, HashMap<Integer, Object> params) throws SQLException, ClassNotFoundException{
41 String sql = insertSql(columns, table);
42 return dBOperation.executeUpdate(sql, params);
43 }
44
45 /**
46 * @param sql
47 * @return int
48 * @throws SQLException
49 * @Author: lulei
50 * @Description: insert 执行完此命令后,执行close()操作
51 */
52 public int insert(String sql) throws SQLException {
53 return dBOperation.executeUpdate(sql);
54 }
55
56 /**
57 * @param table
58 * @param columns
59 * @param params
60 * @return ResultSet
61 * @throws SQLException
62 * @throws ClassNotFoundException
63 * @Author: lulei
64 * @Description: insertGetGeneratedKeys 执行完此命令后,执行close()操作
65 */
66 public ResultSet insertGetGeneratedKeys(String table, String columns, HashMap<Integer, Object> params) throws SQLException, ClassNotFoundException{
67 String sql = insertSql(columns, table);
68 return dBOperation.getGeneratedKeys(sql, params);
69 }
70
71 /**
72 * @param sql
73 * @return ResultSet
74 * @throws SQLException
75 * @Author: lulei
76 * @Description: insertGetGeneratedKeys 执行完此命令后,执行close()操作
77 */
78 public ResultSet insertGetGeneratedKeys(String sql) throws SQLException{
79 return dBOperation.getGeneratedKeys(sql);
80 }
81
82 /**
83 * @param table
84 * @param condition
85 * @return int
86 * @throws SQLException
87 * @Author: lulei
88 * @Description: delete 执行完此命令后,执行close()操作
89 */
90 public int delete(String table, String condition) throws SQLException{
91 if(null == table){
92 return 0;
93 }
94 String sql = "delete from " + table + " " + condition;
95 return dBOperation.executeUpdate(sql);
96 }
97
98 /**
99 * @param sql
100 * @return int
101 * @throws SQLException
102 * @Author: lulei
103 * @Description: delete 执行完此命令后,执行close()操作
104 */
105 public int delete(String sql) throws SQLException{
106 return dBOperation.executeUpdate(sql);
107 }
108
109 /**
110 * @param columns
111 * @param table
112 * @param condition
113 * @return ResultSet
114 * @throws SQLException
115 * @Author: lulei
116 * @Description: select 执行完此命令后,执行close()操作
117 */
118 public ResultSet select(String columns, String table, String condition) throws SQLException {
119 String sql = "select " + columns + " from " + table + " " + condition;
120 return dBOperation.executeQuery(sql);
121 }
122
123 /**
124 * @param sql
125 * @return ResultSet
126 * @throws SQLException
127 * @Author: lulei
128 * @Description: select 执行完此命令后,执行close()操作
129 */
130 public ResultSet select(String sql) throws SQLException{
131 return dBOperation.executeQuery(sql);
132 }
133
134 /**
135 * @param table
136 * @param columns
137 * @param condition
138 * @param params
139 * @return int
140 * @throws SQLException
141 * @throws ClassNotFoundException
142 * @Author: lulei
143 * @Description: update 执行完此命令后,执行close()操作
144 */
145 public int update(String table, String columns, String condition, HashMap<Integer, Object> params) throws SQLException, ClassNotFoundException{
146 String sql = updateString(table, columns, condition);
147 return dBOperation.executeUpdate(sql, params);
148 }
149
150 /**
151 * @param sql
152 * @return int
153 * @throws SQLException
154 * @Author: lulei
155 * @Description: update 执行完此命令后,执行close()操作
156 */
157 public int update(String sql) throws SQLException{
158 return dBOperation.executeUpdate(sql);
159 }
160
161 /**
162 * @param table
163 * @param columns
164 * @param condition
165 * @return String
166 * @Author: lulei
167 * @Description: 组装updateString
168 */
169 private String updateString(String table, String columns, String condition) {
170 if (null == columns || null == table) {
171 return "";
172 }
173 String[] column = columns.split(",");
174 StringBuilder stringBuilder = new StringBuilder("update ");
175 stringBuilder.append(table);
176 stringBuilder.append(" set ");
177 stringBuilder.append(column[0]);
178 stringBuilder.append("=?");
179 for (int i = 1; i < column.length; i++){
180 stringBuilder.append(", ");
181 stringBuilder.append(column[i]);
182 stringBuilder.append("=?");
183 }
184 stringBuilder.append(" ");
185 stringBuilder.append(condition);
186 return stringBuilder.toString();
187 }
188
189 /**
190 * @param columns
191 * @param table
192 * @return String
193 * @Author: lulei
194 * @Description: 组装insertSql
195 */
196 private String insertSql(String columns, String table){
197 if (null == columns || null == table) {
198 return "";
199 }
200 int colNum = columns.split(",").length;
201 StringBuilder stringBuilder = new StringBuilder("insert into ");
202 stringBuilder.append(table);
203 stringBuilder.append(" (");
204 stringBuilder.append(columns);
205 stringBuilder.append(") values (?");
206 for (int i = 1; i < colNum; i++) {
207 stringBuilder.append(",?");
208 }
209 stringBuilder.append(")");
210 return stringBuilder.toString();
211 }
212}
213
214 |
1 2
| 1 下面是的使用事例是其他项目中的一个例子,这里可以简单的看下,代码如下:
2 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 1 public boolean support(String docNo){
2 DBServer dbServer = new DBServer(dbPoolName);
3 String editTime = System.currentTimeMillis() + "";
4 String sql = "update " + SolutionTable.tableName + " set " + SolutionTable.support + "=" + SolutionTable.support + "+1, " + SolutionTable.editTime + "='"+ editTime+"' where " +SolutionTable.docNo + "='" + docNo + "'";
5 try {
6 return dbServer.update(sql) > 0;
7 } catch (SQLException e) {
8 e.printStackTrace();
9 } finally {
10 dbServer.close();
11 }
12 return false;
13 }
14 |
1 2
| 1 ps:最近发现其他网站可能会对博客转载,上面并没有源链接,如想查看更多关于 基于lucene的案例开发 请 点击这里。或访问网址http://blog.csdn.net/xiaojimanman/article/category/2841877
2 |