字符集问题的初步探讨(四)

 6. 乱码的生成

通常在我们的现实环境中,存在3个字符集设置。

第一: 客户端应用字符集(Client Application Character Set)

第二: 客户端NLS_LANG参数设置

第三: 服务器端,数据库字符集(Character Set)设置

 

我们说,一个字符在客户端应用(比如SQLPLUS,CMD,NOTEPAD等)中以怎样的字符显示取决于客户端操作系统,客户端能够显示怎样的字符,
我们就可以在应用中录入这些字符,至于这些字符能否在数据库中正常存储,就和另外的两个字符集设置紧密相关了。

在传输过程中,客户端NLS_LANG主要用于进行转换判断

如果NLS_LANG等于数据库字符集,则不进行任何转换直接把字符插入数据库

如果不同则进行转换,转换主要有两个任务

  • 如果存在对应关系,则把相应二进制编码经过映射后(这一步映射以后,所代表的字符可能发生转换)传递给数据库
  • 如果不存在对应关系,则传递一个替换字符(很多平台就是?)

 

数据库字符集,在和客户端NLS_LANG不同时,会把经过NLS_LANG转换的字符进行进一步处理

  • 对于?(即不存在对应关系的字符)直接以?形式存放入数据库
  • 对于其他字符,在NLS_LANG和数据库字符集之间进行转换后存入。

 

以下我们来看一下最为常见的字符集及乱码的产生:

1.当NLS_LANG字符集与数据库字符集不同,同时NLS_LANG不同于Server端字符集设置

在这种情况下,存在两种可能:

  • 客户端输入的字符在NLS_LANG中没有对应的字符,这时无法转换,NLS_LANG使用替换字符替代这些无法映射的字符(这一步转换在TTS中
    完成),在很多字符集中这个替代字符就是”?”
  • 当客户端的字符在NLS_LANG中对应了不同的字符时,传递给数据库以后发生转换,存储的是字符,但是已经丢失了元数据,数据库中
    的字符不再代表客户端的输入。而且这个过程不可逆,这也就是为什么很多时候在客户端输入的是正常的编码,查询之后会得到未知字符的原因。

我们通过上图来简单说明一下这个过程,当客户端在WE8ISO8859P15字符集时,输入欧元符号: €,这时客户端NLS_LANG和数据库端字符集不同,
进行第一次转换,客户端€符号编码是A4,在NLS_LANG转换时,A4对应了NLS_LANG中的‘¤’,这一步的转换产生了错误映射。由于数据库字符集不
同于NLS_LANG设置,这时进一步的转换发生了,存入数据库的编码变成了C2A4,虽然同NLS_LANG进行了正确的转换,但是客户端录入的数据已经
损坏或者丢失了。

我们可以用我们熟悉的字符集做一个简单的测试:

测试环境:

客户端应用为中文18030字符集

NLS_LANG设置为US7ASCII字符集

数据库CHARACTER SET为ZHS16GBK

 

c: > set  NLS_LANG = AMERICAN_AMERICA.US7ASCII

c:
> sqlplus eygle / eygle

SQL
* Plus: Release  9.2 . 0.4 . 0   -  Production  on  Tue Nov  4   01 : 19 : 57   2003

Copyright (c) 
1982 2002 , Oracle Corporation.   All  rights reserved.


Connected 
to :
Oracle9i Enterprise Edition Release 
9.2 . 0.4 . 0   -  Production
With  the Partitioning, Oracle Label Security, OLAP  and  Oracle Data Mining options
JServer Release 
9.2 . 0.4 . 0   -  Production

SQL
>   insert   into  test  values ( ' 测试 ' );

1  row created.

SQL
>   select  name, dump (name)  from  test;

NAME    
DUMP (NAME)
-- ------------------------------------------------
2bJT    Typ = 1   Len = 4 50 , 98 , 74 , 84

这时候我们发现,查询出来的是混乱的字符,我们把这些字符转换为2进制就是
110010     1100010     1001010     1010100
补全8位就是       
00110010    01100010    01001010    01010100
我们把首位换成1   
10110010    11100010    11001010    11010100

我们来看正确的存储:c:
> set  nls_lang = AMERICAN_AMERICA.ZHS16GBK
c:
> sqlplus eygle / eygle

SQL
* Plus: Release  9.2 . 0.4 . 0   -  Production  on  Tue Nov  4   01 : 40 : 18   2003

Copyright (c) 
1982 2002 , Oracle Corporation.  All  rights reserved.


Connected 
to :
Oracle9i Enterprise Edition Release 
9.2 . 0.4 . 0   -  Production
With  the Partitioning, Oracle Label Security, OLAP  and  Oracle Data Mining options
JServer Release 
9.2 . 0.4 . 0   -  Production

SQL
>   insert   into  test  values ( ' 测试 ' );

1  row created.

SQL
>  col  dump (name)  for  a30
SQL
>   select  name, dump (name)  from  test;

NAME 
DUMP (NAME)
-- -------- ------------------------------
测试 Typ = 1   Len = 4 178 , 226 , 202 , 212

1  row selected.


我们把这个结果转换为2进制表示          
10110010    11100010    11001010    11010100
这个结果正是我们前面乱码首位补全1后的结果。

这个测试说明在US7ASCII转换中文的时候除去了首位的 
1 ,这样就丢失了元数据,导致乱码出现,NLS_LANG的转换作用由此可加一斑!

 

3. NLS_LANG和数据库字符集相同时
在这种情况下,数据库端对客户端传递过来的编码不进行任何转换(这样可以提高性能),直接存储进入数据库,那么这时候就存在和上面同样的问题,
如果客户端传递过来的字符集在数据库中有正确的对应就可以正确存储,如果没有,就会被替换字符置换成?,乱码就这样产生了。

如上图所示,当NLS_LANG和数据库字符集设置相同都为UTF8时,客户端的欧元符号的编码A4就不会经过任何转换就插入到数据库中,而在UTF8的数
据库中,A4代表的是一个非法字符。

 

我们来看一个简单的测试

测试环境:

客户端字符集应用为中文GB18030

客户端NLS_LANG为US7ASCII

数据库字符集为US7ASCII

我们知道这个时候,存入的数据,数据库不进行任何转换,在以下的测试中,我们看到中文在US7ASCII字符集下得以正确显示。

 

c: > set  nls_lang = AMERICAN_AMERICA.US7ASCII

c:
> sqlplus eygle / eygle

SQL
* Plus: Release  9.2 . 0.4 . 0   -  Production  on  Tue Nov  4   01 : 02 : 04   2003

Copyright (c) 
1982 2002 , Oracle Corporation.   All  rights reserved.


Connected 
to :
Oracle9i Enterprise Edition Release 
9.2 . 0.4 . 0   -  Production
With  the Partitioning, Oracle Label Security, OLAP  and  Oracle Data Mining options
JServer Release 
9.2 . 0.4 . 0   -  Production

SQL
>   insert   into  test  values ( ' 测试 ' );

1  row created.

SQL
>   commit ;

Commit  complete.

SQL
>   select   *   from  test;

NAME
-- --------
测试

1  row selected.

SQL
>  col  dump (name)  for  a30
SQL
>   select  name, dump (name)  from  test;

NAME       
DUMP (NAME)
-- -------- ------------------------------
测试       Typ = 1   Len = 4 178 , 226 , 202 , 212

1  row selected.

SQL
>   select   *   from  nls_database_parameters;

PARAMETER                      VALUE
-- ---------------------------- ----------------------------------------
NLS_LANGUAGE                   AMERICAN
NLS_TERRITORY                  AMERICA
NLS_CURRENCY                   $
NLS_ISO_CURRENCY               AMERICA
NLS_NUMERIC_CHARACTERS         .,
NLS_CHARACTERSET               US7ASCII
NLS_CALENDAR                   GREGORIAN
NLS_DATE_FORMAT                DD
- MON - RR
NLS_DATE_LANGUAGE              AMERICAN
NLS_SORT                       
BINARY
NLS_TIME_FORMAT                HH.MI.SSXFF AM

PARAMETER                      VALUE
-- ---------------------------- ----------------------------------------
NLS_TIMESTAMP_FORMAT           DD - MON - RR HH.MI.SSXFF AM
NLS_TIME_TZ_FORMAT             HH.MI.SSXFF AM TZR
NLS_TIMESTAMP_TZ_FORMAT        DD
- MON - RR HH.MI.SSXFF AM TZR
NLS_DUAL_CURRENCY              $
NLS_COMP                       
BINARY
NLS_LENGTH_SEMANTICS           BYTE
NLS_NCHAR_CONV_EXCP            FALSE
NLS_NCHAR_CHARACTERSET         AL16UTF16
NLS_RDBMS_VERSION              
9.2 . 0.4 . 0

20  rows selected.

SQL
>

 

结语:

对于DBA来说,有一个很重要的原则就是:不要把你的数据库置于危险的境地!

这就要求我们,在进行任何可能对数据库结构发生改变的操作之前,先做有效的备份,很多DBA没有备份的操作中得到了惨痛的教训。

 

7. 关于字符集更改的内部操作

 

前面我们提到,通过修改props$的方式更改字符集在Oracle7之后是一种极其危险的方式,应该尽量避免。

我们又知道,通过ALTER DATABASE CHARACTER SET更改字符集虽然安全可靠,但是有严格的子集和超集的约束,实际上我们很少能够
用到这种方法。


实际上Oracle还存在另外一种更改字符集的方式.

如果你注意过的话,在Oracle的alert<sid>.log文件中,你可能看到过这样的日志信息:

alter   database   character   set  INTERNAL_CONVERT ZHS16GBK
Updating 
character   set   in  controlfile  to  ZHS16GBK
 SYS.SNAP$ (REL_QUERY) 
-  CLOB representation altered
 SYS.METASTYLESHEET (STYLESHEET) 
-  CLOB representation altered
 SYS.EXTERNAL_TAB$ (PARAM_CLOB) 
-  CLOB representation altered
 XDB.XDB$RESOURCE (SYS_NC00027$) 
-  CLOB representation altered
 ODM.ODM_PMML_DTD (DTD) 
-  CLOB representation altered
 OE.WAREHOUSES (SYS_NC00003$) 
-  CLOB representation altered
 PM.ONLINE_MEDIA (SYS_NC00042$) 
-  CLOB representation altered
 PM.ONLINE_MEDIA (SYS_NC00062$) 
-  CLOB representation altered
 PM.ONLINE_MEDIA (PRODUCT_TEXT) 
-  CLOB representation altered
 PM.ONLINE_MEDIA (SYS_NC00080$) 
-  CLOB representation altered
 PM.PRINT_MEDIA (AD_SOURCETEXT) 
-  CLOB representation altered
 PM.PRINT_MEDIA (AD_FINALTEXT) 
-  CLOB representation altered
Completed: 
alter   database   character   set  INTERNAL_CONVERT ZHS1

 

在这里面,我们看到这样一条重要的,Oracle非公开的命令:

 

alter   database   character   set  INTERNAL_CONVERT /  INTERNAL_USE ZHS16GBK

 

这个命令是当你选择了使用典型方式创建了种子数据库以后,Oracle会根据你选择的字符集设置,把当前种子数据库的字符集更改为期望字符
集,这就是这条命令的作用.

在使用这个命令时,Oracle会跳过所有子集及超集的检查,在任意字符集之间进行强制转换,所以,使用这个命令时你必须十分小心,你必须
清楚这一操作会带来的风险.
我们之前讲过的内容仍然有效,你可以使用csscan扫描整个数据库,如果在转换的字符集之间确认没有严重的数据损坏,或者你可以使用有效
的方式更改,你就可以使用这种方式进行转换.
我们来看一下具体的操作过程及Oracle的内部操作:

 

SQL >   shutdown  immediate
Database  closed.
Database  dismounted.
ORACLE instance shut down.
SQL
>  startup mount
ORACLE instance started.

Total System Global Area  
135337420  bytes
Fixed Size                   
452044  bytes
Variable Size             
109051904  bytes
Database  Buffers            25165824  bytes
Redo Buffers                 
667648  bytes
Database  mounted.

SQL
>   ALTER  SYSTEM ENABLE RESTRICTED SESSION;

System altered.

SQL
>   ALTER  SYSTEM  SET  JOB_QUEUE_PROCESSES = 0 ;

System altered.

SQL
>   ALTER  SYSTEM  SET  AQ_TM_PROCESSES = 0 ;

System altered.

SQL
>   ALTER   DATABASE   OPEN ;

Database  altered.

SQL
>   alter  session  set  events  ' 10046 trace name context forever,level 12 ' ;

Session altered.

SQL
>   alter   database   character   set  INTERNAL_USE ZHS16CGB231280

Database  altered.

SQL
>

 

这是alert.log文件中的记录信息:

Tue Oct 19 16:26:30 2004
Database Characterset is ZHS16GBK
replication_dependency_tracking turned off (no async multimaster replication found)
Completed: ALTER DATABASE OPEN
Tue Oct 19 16:27:07 2004
alter database character set INTERNAL_USE ZHS16CGB231280
Updating character set in controlfile to ZHS16CGB231280
Tue Oct 19 16:27:15 2004
Thread 1 advanced to log sequence 118
Current log# 2 seq# 118 mem# 0: /opt/oracle/oradata/primary/redo02.log
Tue Oct 19 16:27:15 2004
ARC0: Evaluating archive log 3 thread 1 sequence 117
ARC0: Beginning to archive log 3 thread 1 sequence 117
Creating archive destination LOG_ARCHIVE_DEST_1: '/opt/oracle/oradata/primary/archive/1_117.dbf'
ARC0: Completed archiving log 3 thread 1 sequence 117
Tue Oct 19 16:27:20 2004
Completed: alter database character set INTERNAL_USE ZHS16CGB231280
Shutting down instance: further logons disabled
Shutting down instance (immediate)
License high water mark = 1
Tue Oct 19 16:29:06 2004
ALTER DATABASE CLOSE NORMAL
...

 

格式化10046跟踪文件,得到以下信息(摘要):

 

alter  session  set  events  ' 10046 trace name context forever,level 12 '


alter   database   character   set  INTERNAL_USE ZHS16CGB231280


call     
count        cpu    elapsed        disk       query     current         rows
-- ----- ------  -------- ---------- ---------- ---------- ----------  ----------
Parse         1        0.00         0.00            0            0            0             0
Execute        1        4.88         6.04          910        16825        18099             0
Fetch          0        0.00         0.00            0            0            0             0
-- ----- ------  -------- ---------- ---------- ---------- ----------  ----------
total         2        4.88         6.04          910        16825        18099             0

Misses 
in  library cache during parse:  1
Optimizer goal: CHOOSE
Parsing 
user  id: SYS

Elapsed times include waiting 
on  following events:
  Event waited 
on                              Times    Max . Wait  Total Waited
  
-- --------------------------------------   Waited  ----------  ------------
  control  file  sequential  read                      4          0.00            0.00
  control 
file  parallel write                      2          0.05            0.08
  
log   file  sync                                    2          0.08            0.08
  SQL
* Net message  to  client                        1          0.00            0.00
  SQL
* Net message  from  client                      1         18.06           18.06
********************************************************************************

....

update  col$  set  charsetid  =  : 1  
where
 charsetform 
=  : 2

....

update  argument$  set  charsetid  =  : 1  
where
 charsetform 
=  : 2

....

update  collection$  set  charsetid  =  : 1  
where
 charsetform 
=  : 2

....

update  attribute$  set  charsetid  =  : 1  
where
 charsetform 
=  : 2
....

update  parameter$  set  charsetid  =  : 1  
where
 charsetform 
=  : 2
....

update  result$  set  charsetid  =  : 1  
where
 charsetform 
=  : 2

....

update  partcol$  set  spare1  =  : 1  
where
 charsetform 
=  : 2

....

update  subpartcol$  set  spare1  =  : 1  
where
 charsetform 
=  : 2

....

update  props$  set  value$  =  : 1  
where
 name 
=  : 2

....

update  "SYS"."KOTAD$"  set  SYS_NC_ROWINFO$  =  : 1  
where
 SYS_NC_OID$ 
=  : 2
....

update  seq$  set  increment$ = : 2 ,minvalue = : 3 ,maxvalue = : 4 ,cycle# = : 5 , order $ = : 6 ,
  cache
= : 7 ,highwater = : 8 ,audit$ = : 9 ,flags = : 10  
where
 obj#
= : 1

....

update  kopm$  set  metadata  =  : 1 ,  length 
  
=  : 2  
where
 name
= ' DB_FDO '

....

ALTER   DATABASE   CLOSE  NORMAL

 

此处生成的日志你可以在这里下载(供参考):

http://www.eygle.com/special/primary_ora_13730.zip
http://www.eygle.com/special/primary_ora_13730.tkf.log

我们看到这个过程和之前ALTER DATABASE CHARACTER SET操作的内部过程是完全相同的,也就是说INTERNAL_USE提供的帮助就是使
Oracle数据库绕过了子集与超集的校验.
这一方法在某些方面是有用处的,比如测试;应用于产品环境大家应该格外小心,除了你以外,没有人会为此带来的后果负责:

结语(我们不妨再说一次):

对于DBA来说,有一个很重要的原则就是:不要把你的数据库置于危险的境地!

这就要求我们,在进行任何可能对数据库结构发生改变的操作之前,先做有效的备份,很多DBA没有备份的操作中得到了惨痛的教训。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值