大数据WEB阶段(七)JDBC、数据库批处理、数据库连接池
来源:互联网 发布:刷流量软件下载 编辑:程序博客网 时间:2024/04/28 17:40
JDBC
一、概述
数据库驱动: 数据库厂商提供的用来操作数据库的jar包
- JDBC简介
- 由于各大数据库厂商提供的数据库驱动各不相同, 导致了开发人员的学习成本十分的高. SUN公司为了简化数据库的操作, 提供了一套规范, 本质上就是一大堆的接口, 要求各大数据库厂商在提供驱动时都要实现JDBC这套接口, 实现之后, 只要学会JDBC这套接口, 所有的数据库驱动就都会使用了!
- 六个步骤实现JDBC程序
在开发之前 , 首先要导入jar包
Connection conn = null;Statement stat = null;ResultSet rs = null;try { //1.注册数据库驱动 Class.forName("com.mysql.jdbc.Driver"); //2.获取数据库连接 conn = DriverManager.getConnection("jdbc:mysql:///mydb5", "root", "root"); //3.获取传输器 stat = conn.createStatement(); //4.利用传输器,发送sql到数据库执行,返回执行结果 rs = stat.executeQuery("select * from account"); //5.处理结果 while(rs.next()){ int id = rs.getInt(1); String name = rs.getString("name"); double money = rs.getDouble("money"); System.out.println(id+name+money); }} catch (Exception e) { e.printStackTrace(); throw new RuntimeException();}finally{ //6.释放资源 if(rs!=null){ try { rs.close(); } catch (SQLException e) { e.printStackTrace(); }finally{ rs = null; } } if(stat!=null){ try { stat.close(); } catch (SQLException e) { e.printStackTrace(); }finally{ stat = null; } } if(conn!=null){ try { conn.close(); } catch (SQLException e) { e.printStackTrace(); }finally{ conn = null; } }}
- JDBC API详解
- 注册数据库驱动
- 使用DriverManager.registerDriver(new Driver());注册数据库有两个缺点,首先,通过观察mysql的中Driver接口的实现类发现在静态代码块中注册驱动的逻辑,所以这种方式会造成驱动被注册两次。另外,这种方式导致了程序和具体的数据库驱动绑死在了一起,程序的灵活性比较低。
- 所以推荐使用:Class.forName(“com.mysql.jdbc.Driver”);的方式注册数据库驱动。
- 获取数据库连接 conn = DriverManager.getConnection(url,name,password);
数据库URL
URL用于标识数据库的位置,程序员通过URL地址告诉JDBC程序连接哪个数据库,URL的写法为:
jdbc:mysql://localhost:3306/test ?参数名=参数值
常用数据库URL地址的写法:
Oracle写法:jdbc:oracle:thin:@localhost:1521:sidSqlServer写法:jdbc:microsoft:sqlserver://localhost:1433;DatabaseName=sidMySql:jdbc:mysql://localhost:3306/sidMysql的url地址的简写形式: jdbc:mysql:///sid
Connection
Jdbc程序中的Connection,它用于代表数据库的链接,Connection是数据库编程中最重要的一个对象,客户端与数据库所有交互都是通过connection对象完成的,这个对象的常用方法:
createStatement():创建向数据库发送sql的statement对象。prepareStatement(sql):创建向数据库发送预编译sql的PreparedSatement对象。prepareCall(sql):创建执行存储过程的callableStatement对象。 setAutoCommit(boolean autoCommit):设置事务是否自动提交。 commit():在链接上提交事务。rollback():在此链接上回滚事务。
Statement
Jdbc程序中的Statement对象用于向数据库发送SQL语句, Statement对象常用方法:
executeQuery(String sql) :用于向数据库发送查询语句。executeUpdate(String sql):用于向数据库发送insert、update或delete语句execute(String sql):用于向数据库发送任意sql语句addBatch(String sql):把多条sql语句放到一个批处理中。executeBatch():向数据库发送一批sql语句执行。
ResultSet
- Jdbc程序中的ResultSet用于代表Sql语句的执行结果。Resultset封装执行结果时,采用的类似于表格的方式。ResultSet 对象维护了一个指向表格数据行的游标,初始的时候,游标在第一行之前,调用ResultSet.next() 方法,可以使游标指向具体的数据行,进行调用方法获取该行的数据。
ResultSet既然用于封装执行结果的,所以该对象提供的都是用于获取数据的get方法:
获取任意类型的数据getObject(int index)getObject(string columnName)获取指定类型的数据,例如:getString(int index)getString(String columnName)getInt(columnIndex)getInt(columnLabel)getDouble(columnIndex)getDouble(columnLabel)...操作游标的方法,例如:next():移动到下一行Previous():移动到前一行absolute(int row):移动到指定行beforeFirst():移动resultSet的最前面。afterLast() :移动到resultSet的最后面。...
- 释放资源
- Jdbc程序运行完后,切记要释放程序在运行过程中,创建的那些与数据库进行交互的对象,这些对象通常是ResultSet, Statement和Connection对象。
- 特别是Connection对象,它是非常稀有的资源,用完后必须马上释放,如果Connection不能及时、正确的关闭,极易导致系统宕机。Connection的使用原则是尽量晚创建,尽量早的释放。
- 为确保资源释放代码能运行,资源释放代码也一定要放在finally语句中。
- JDBC的增删改查
增
//jdbc 增public class Demo_01 { public static void main(String[] args) { Connection conn = null; Statement stat = null; ResultSet rs = null; try { //注册驱动 Class.forName("com.mysql.jdbc.Driver"); //获取连接 conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/cszj","root","root"); //获取传输器 stat = conn.createStatement(); //通过传输器将sql语句传输到mysql中 int rows = stat.executeUpdate("insert into user values(7 ,'刘备','1234565')"); //结果 System.out.println("该操作影响了:"+rows+"行"); } catch (Exception e) { e.printStackTrace(); }finally{ if(rs !=null){ try { rs.close(); } catch (SQLException e) { e.printStackTrace(); }finally{ rs = null; } } if(stat !=null){ try { stat.close(); } catch (SQLException e) { e.printStackTrace(); }finally{ stat = null; } } if(conn !=null){ try { conn.close(); } catch (SQLException e) { e.printStackTrace(); }finally{ conn = null; } } } }}
删
//jdbc 删public class Demo_01 { public static void main(String[] args) { Connection conn = null; Statement stat = null; ResultSet rs = null; try { //注册驱动 Class.forName("com.mysql.jdbc.Driver"); //获取连接 conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/cszj","root","root"); //获取传输器 stat = conn.createStatement(); //通过传输器将sql语句传输到mysql中 int rows = stat.executeUpdate("delete from user where id = 5"); //结果 System.out.println("该操作影响了:"+rows+"行"); } catch (Exception e) { e.printStackTrace(); }finally{ if(rs !=null){ try { rs.close(); } catch (SQLException e) { e.printStackTrace(); }finally{ rs = null; } } if(stat !=null){ try { stat.close(); } catch (SQLException e) { e.printStackTrace(); }finally{ stat = null; } } if(conn !=null){ try { conn.close(); } catch (SQLException e) { e.printStackTrace(); }finally{ conn = null; } } } }}
改
//jdbc 改public class Demo_01 { public static void main(String[] args) { Connection conn = null; Statement stat = null; ResultSet rs = null; try { //注册驱动 Class.forName("com.mysql.jdbc.Driver"); //获取连接 conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/cszj","root","root"); //获取传输器 stat = conn.createStatement(); //通过传输器将sql语句传输到mysql中 int rows = stat.executeUpdate("update user set id = 5 where user='奥特曼'"); //结果 System.out.println("该操作影响了:"+rows+"行"); } catch (Exception e) { e.printStackTrace(); }finally{ if(rs !=null){ try { rs.close(); } catch (SQLException e) { e.printStackTrace(); }finally{ rs = null; } } if(stat !=null){ try { stat.close(); } catch (SQLException e) { e.printStackTrace(); }finally{ stat = null; } } if(conn !=null){ try { conn.close(); } catch (SQLException e) { e.printStackTrace(); }finally{ conn = null; } } } }}
查
//jdbc 查public class Demo_01 { public static void main(String[] args) { Connection conn = null; Statement stat = null; ResultSet rs = null; try { //注册驱动 Class.forName("com.mysql.jdbc.Driver"); //获取连接 conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/cszj","root","root"); //获取传输器 stat = conn.createStatement(); //通过传输器将sql语句传输到mysql中 rs = stat.executeQuery("select * from user"); //结果 while(rs.next()){ int id = rs.getInt("id"); String user = rs.getString("user"); String password = rs.getString("password"); System.out.println(id+":"+user+":"+password); } } catch (Exception e) { e.printStackTrace(); }finally{ if(rs !=null){ try { rs.close(); } catch (SQLException e) { e.printStackTrace(); }finally{ rs = null; } } if(stat !=null){ try { stat.close(); } catch (SQLException e) { e.printStackTrace(); }finally{ stat = null; } } if(conn !=null){ try { conn.close(); } catch (SQLException e) { e.printStackTrace(); }finally{ conn = null; } } } }}
自定义JDBCUtils 工具类
创建配置文件 config.properties
driverClass=com.mysql.jdbc.DriverjdbcUrl=jdbc:mysql://localhost:3306/cszjuser=rootpassword=root
创建数据库工具类
/** * 数据库工具类 * */public class JDBCUtils { //只在类被加载时读取一次配置文件 private static Properties prop = new Properties(); static{ try { String path = JDBCUtils.class.getClassLoader().getResource("config.properties").getPath(); prop.load(new FileInputStream(path)); } catch (Exception e) { e.printStackTrace(); } } //获取连接 public static Connection getConnection(){ String driverClass = prop.getProperty("driverClass"); String jdbcUrl = prop.getProperty("jdbcUrl"); String user = prop.getProperty("user"); String password = prop.getProperty("password"); Connection conn = null; try { //注册驱动 Class.forName(driverClass); //获取连接 conn = DriverManager.getConnection(jdbcUrl,user,password); } catch (Exception e) { e.printStackTrace(); } return conn; } //释放资源 public static void close(Connection conn , Statement stat, ResultSet rs){ if(rs !=null){ try { rs.close(); } catch (SQLException e) { e.printStackTrace(); }finally{ rs =null ; } } if(stat !=null){ try { stat.close(); } catch (SQLException e) { e.printStackTrace(); }finally{ stat =null ; } } if(conn !=null){ try { conn.close(); } catch (SQLException e) { e.printStackTrace(); }finally{ conn =null ; } } }}
修改数据路连接代码
//jdbc 查public class Demo_01 { public static void main(String[] args) { Connection conn = null; Statement stat = null; ResultSet rs = null; try { //获取连接 conn = JDBCUtils.getConnection(); //获取传输器 stat = conn.createStatement(); //通过传输器将sql语句传输到mysql中 rs = stat.executeQuery("select * from user"); //结果 while(rs.next()){ int id = rs.getInt("id"); String user = rs.getString("user"); String password = rs.getString("password"); System.out.println(id+":"+user+":"+password); } } catch (Exception e) { e.printStackTrace(); }finally{ JDBCUtils.close(conn, stat, rs); } }}
PreparedStatement
注入攻击演示
public static void main(String[] args) { Scanner scan = new Scanner(System.in); //提示用户登陆 System.out.println("请登陆"); //提示输入用户名 System.out.println("请输入用户名:"); String username = scan.nextLine(); //提示输入密码 System.out.println("请输入密码:"); String password = scan.nextLine(); login(username,password); } private static void login(String username, String password) { Connection conn = null; Statement stat = null; ResultSet rs = null; try { conn = JDBCUtils.getConnection(); stat = conn.createStatement(); String sql = "select * from user where username='"+username+"' and password='"+password+"'"; //当输入“张三'#”后,sql变为: //select * from user where username='张三'#' and password='' //当输入“张三' or '2=2”后,sql变为: //select * from user where username='张三' or '2=2' and password='' rs = stat.executeQuery(sql); if(rs.next()){ System.out.println("恭喜,登陆成功"); }else{ System.out.println("用户名或密码错误"); } } catch (Exception e) { e.printStackTrace(); throw new RuntimeException(); }finally{ JDBCUtils.close(conn, stat, rs); } }
- 由于后台的SQL是拼接而来的, 其中的参数是用户提交的, 如果用户在提交参数时, 参杂了一些SQL关键字或者特殊符号, 就有可能会导致SQL语句语意的改变, 从而造成一些意外的操作!
Preparedment
优点:
- 可以防止SQL注入攻击
- 通过PreparedStatement对象发送sql, 是先把sql语句的骨架发送给数据库编译并确定下来, 后面发送的只能是参数的值, 不能影响sql语句的骨架, 即使参数中包含sql关键字或特殊符号, 也只会当成普通的文本来处理!
- 通过 方法来设置参数 , 省去了拼接sql语句的麻烦
可以提高程序运行的效率
PreparedStatement对象发送的sql语句(骨架)到数据库编译后会被数据缓存下来, 如果下次执行的sql与缓存中的相匹配, 就不再编译而是直接使用缓存中的语句, 可以减少sql语句编译的次数, 提高程序执行的效率!
Statement对象发送的sql语句到数据库之后也会编译, 但是Statement对象是先拼接好再发送sql到数据库, 如果每次参数不同, 整条sql也就不同. 所以每次都需要编译!
- 可以防止SQL注入攻击
代码改造
//jdbc 查public class Demo_01 { public static void main(String[] args) { Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try { //获取连接 conn = JDBCUtils.getConnection(); //获取传输器 ps = conn.prepareStatement(" select * from user"); //通过传输器将sql语句传输到mysql中 rs = ps.executeQuery(); //结果 while(rs.next()){ int id = rs.getInt("id"); String user = rs.getString("user"); String password = rs.getString("password"); System.out.println(id+":"+user+":"+password); } } catch (Exception e) { e.printStackTrace(); }finally{ JDBCUtils.close(conn, ps, rs); } }}
二、批处理
- 批处理概述
- 假设现有一大堆的sql要到数据库执行, 如果一条一条发送, 有多少条就 需要发送多少次, 效率低下
- 可以通过批处理提高发送sql语句的效率: 可以将这一大堆的sql添加到 一个批中, 一次性将批发送给数据库, 数据库收到后打开批, 依次执行其中 sql语句, 这样可以减少sql语句发送的次数, 从而提高程序执行的效率!
Statement方式实现批处理
- 优点: 可以再一次批处理中添加结构不同的语句
- 缺点:
- 不能防止sql注入
- 没有预编译机制 , 效率贼低
- 当发送结构相同的sql语句时 , sql语句的骨架每次都需要编译 。
案例:
//批处理public class Demo_02 { public static void main(String[] args) { Connection conn = null; Statement stat = null; ResultSet rs = null; try{ //获取连接 conn = JDBCUtils.getConnection(); //获取传输器 stat = conn.createStatement(); stat.addBatch("use cszj;"); stat.addBatch("create if not exists table tb_batch(id int primary key auto_increment, name varchar(20))"); stat.addBatch("insert into tb_batch values(null,'a')"); stat.addBatch("insert into tb_batch values(null,'b')"); stat.addBatch("insert into tb_batch values(null,'c')"); stat.addBatch("insert into tb_batch values(null,'d')"); stat.executeBatch(); System.out.println("执行完成"); }catch(Exception e){ e.printStackTrace(); }finally{ JDBCUtils.close(conn, stat, rs); } }}
PreparedStatement方式实现批处理
- 优点:
- 可以防止sql注入攻击
- 采用预编译机制 , 可以提高程序执行效率
- 当发送多条结构相同的sql时 , sql骨架只发送编译一次
- 缺点:
- 不能在一次批处理中添加不同结构的语句
案例:
//批处理public class Demo_02 { public static void main(String[] args) { Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try{ //获取连接 conn = JDBCUtils.getConnection(); //获取传输器 ps = conn.prepareStatement("insert into tb_batch values(null ,? )"); for( int i = 0;i<10;i++){ ps.setString(1, "a"+i); ps.addBatch(); } ps.executeBatch(); System.out.println("执行完成"); }catch(Exception e){ e.printStackTrace(); }finally{ JDBCUtils.close(conn, ps, rs); } }}
- 优点:
三、数据库连接池
- 概述
- 用户每次请求都需要向数据库获得链接,而数据库创建连接通常需要消耗相对较大的资源,创建时间也较长。假设网站一天10万访问量,数据库服务器就需要创建10万次连接,极大的浪费数据库的资源,并且极易造成数据库服务器内存溢出、宕机。
- 频繁的开关连接相当的耗费资源,所以我们可以设置一个连接池,在程序启动时就初始化一批连接,在程序中共享,需要连接时从池中获取,用完再还回池中,通过池共享连接,减少开关连接的次数,提高程序的效率。
自定义一个简易的数据库连接池
import java.io.PrintWriter;import java.sql.Connection;import java.sql.DriverManager;import java.sql.SQLException;import java.sql.SQLFeatureNotSupportedException;import java.util.LinkedList;import java.util.List;import java.util.logging.Logger;import javax.sql.DataSource;public class MyPool implements DataSource{ private static List<Connection> list = new LinkedList<Connection>(); static{ try { Class.forName("com.mysql.jdbc.Driver"); for (int i = 0; i < 5; i++) { Connection conn = DriverManager.getConnection("jdbc:mysql:///mydb5","root","root"); list.add(conn); } } catch (Exception e) { e.printStackTrace(); throw new RuntimeException(); } } @Override public Connection getConnection() throws SQLException { if(list.isEmpty()){ for (int i = 0; i < 3; i++) { Connection conn = DriverManager.getConnection("jdbc:mysql:///mydb1"); list.add(conn); } } Connection conn = list.remove(0); System.out.println("成功从数据库中获取一个连接,连接池中还剩"+list.size()+"个连接.."); return conn; } public void returnConn(Connection conn){ try { if(conn != null && !conn.isClosed()){ list.add(conn); System.out.println("成功向数据库还回一个连接,连接池中还剩"+list.size()+"个连接.."); } } catch (SQLException e) { e.printStackTrace(); throw new RuntimeException(); } } @Override public PrintWriter getLogWriter() throws SQLException { // TODO Auto-generated method stub return null; } @Override public void setLogWriter(PrintWriter out) throws SQLException { // TODO Auto-generated method stub } @Override public void setLoginTimeout(int seconds) throws SQLException { // TODO Auto-generated method stub } @Override public int getLoginTimeout() throws SQLException { // TODO Auto-generated method stub return 0; } @Override public Logger getParentLogger() throws SQLFeatureNotSupportedException { // TODO Auto-generated method stub return null; } @Override public <T> T unwrap(Class<T> iface) throws SQLException { // TODO Auto-generated method stub return null; } @Override public boolean isWrapperFor(Class<?> iface) throws SQLException { // TODO Auto-generated method stub return false; } @Override public Connection getConnection(String username, String password) throws SQLException { // TODO Auto-generated method stub return null; }}测试自定义连接池import java.sql.Connection;import java.sql.ResultSet;import java.sql.SQLException;import java.sql.Statement;public class TestMyPool { public static void main(String[] args) { MyPool pool = new MyPool(); Connection conn= null; Statement stat = null; ResultSet rs = null; try { conn = pool.getConnection(); stat = conn.createStatement(); rs = stat.executeQuery("select * from account where id=1"); if(rs.next()){ System.out.println(rs.getInt("id")+":"+rs.getString("name")+":"+rs.getDouble("money")); } System.out.println("执行完毕"); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException(); }finally{ if(rs!=null){ try { rs.close(); } catch (SQLException e) { e.printStackTrace(); }finally{ rs = null; } } if(stat!=null){ try { stat.close(); } catch (SQLException e) { e.printStackTrace(); }finally{ stat = null; } } pool.returnConn(conn); } }}
- 如上代码写的连接池,还需要在使用完连接后记得不能关闭连接,而是要调用returnConn方法将连接还回池中。
- 我们想能不能想办法改造conn的close方法,使close方法不会真的关闭连接而是将连接还回池中。
改造close方法
- 继承
- 写一个类继承要改造的类,对于不想改造的方法不覆盖,对于想要改造的方法复写该方法,将代码改造为自己需要的逻辑代码。
- 这种方式只能在还没有对象的情况下使用,现在Connection对象已经存在了,再用继承复写的方式是不行的,所以我们不采用。
- 装饰
- 实现装饰设计模式:
- 写一个装饰类, 要求装饰类和被装饰者所属的类实现同一个接口或者继承同一个父类
- 装饰类必须提供构造方法接收被装饰者, 并将被装饰者保存在类的内部
- 对于想要改造的方法直接进行改造, 对于不想改造的方法, 直接调用原有对象(被装设者)上的方法
- 实现装饰设计模式:
案例:
装饰者类public class ConnectionDecorate implements Connection{ private Connection conn = null; private MyPool pool = null; public ConnectionDecorate(Connection conn,MyPool pool){ this.conn = conn; this.pool = pool; } @Override public void close() throws SQLException { pool.returnConn(conn); }修改MyPool类:在获取连接方法中,return前加入如下代码://先进行包装 Connection connDecorate = new ConnectionDecorate(conn,this); return connDecorate;测试类最后只需关闭连接即可
- 继承
开源数据库连接池c3p0
- 概述
- 我们手写的连接池是比较简陋的,是为了讲解连接池的原理。其实在真实开发中可以使用开源的数据库连接池。其中C3P0是比较常用的一种。
c3p0示例
- 导入c3p0包
创建测试类
public static void main(String[] args) { Connection conn = null; Statement stat = null; ResultSet rs = null; try { //创建连接池 ComboPooledDataSource cpds = new ComboPooledDataSource(); //设置连接数据库的基本信息 cpds.setDriverClass("com.mysql.jdbc.Driver"); cpds.setJdbcUrl("jdbc:mysql:///mydb5"); cpds.setUser("root"); cpds.setPassword("root"); //从连接池中获取连接 conn = cpds.getConnection(); stat = conn.createStatement(); rs = stat.executeQuery("select * from account where id=1"); if(rs.next()){ System.out.println(rs.getInt("id")+" : " +rs.getString("name")+" : " +rs.getDouble("money")); } } catch (Exception e) { e.printStackTrace(); throw new RuntimeException(); }finally{ JDBCUtils.close(conn, stat, rs); } }
上面的方法是将数据写死在代码中,不利于代码的维护 , 需要对代码进行改造
C3p0会默认读取一个配置文件,为c3p0-config.xml,(注意,名字不能改变,否则c3p0将无法读取)我们在src或者类似的源码目录下,创建一个c3p0-config.xml文件, 配置内容如下:
<?xml version="1.0" encoding="UTF-8"?><c3p0-config> <default-config> <property name="driverClass">com.mysql.jdbc.Driver</property> <property name="jdbcUrl">jdbc:mysql:///mydb5</property> <property name="user">root</property> <property name="password">root</property> </default-config></c3p0-config>
另一种方式:在src或者类似的源代码目录下, 创建一个c3p0.properties文件 , 配置文件内容如下:
c3p0.driverClass=com.mysql.jdbc.Driverc3p0.jdbcUrl=jdbc:mysql:///mydb5c3p0.user=rootc3p0.password=root
- 概述
- 大数据WEB阶段(七)JDBC、数据库批处理、数据库连接池
- 大数据WEB阶段(十八)数据库事务
- jdbc批处理大数据
- jdbc批处理大数据
- JDBC处理大数据批处理
- 大数据WEB阶段(三)CSS
- 大数据WEB阶段(四)JavaScript
- 大数据WEB阶段(五)jQuery
- 大数据WEB阶段(十九)Threadlocal
- JAVAWEB开发之JDBC详解(连接操作数据库、处理大数据、批处理)
- 大数据WEB阶段总结
- JavaWeb-JDBC处理大数据、批处理、事物
- web 学习笔记15-JDBC大数据 批处理 存储过程 事务
- 大数据WEB阶段 (六)MySql详解(一)
- 大数据WEB阶段(六)MySql详解(二)
- 大数据WEB阶段(一)XML文件的操作
- 大数据WEB阶段(九)Servlet+Request
- 大数据WEB阶段(十一)Ajax、URL编码
- 图解重定向的原理
- angularJS——路由
- JavaEE 绝对路径、相对路径获取方式
- 二叉树
- FPGA之QuartusII 下载 配置 (入门篇)
- 大数据WEB阶段(七)JDBC、数据库批处理、数据库连接池
- 【Trick】数据预处理的常用方法
- vim最常用的命令集合
- Java之Json和数组、字符串、对象之间的转换
- Const的作用
- C#双行计算器和单行计算器
- LintCode之5 第k大元素
- 用Python下载音悦台最清晰版mv
- 中小型电商数据库较完整的结构表