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没有备份的操作中得到了惨痛的教训。