在 Yii 2.0 中,Redis ActiveRecord 出现主键 ID 重复的情况的分析解决
1、请求接口,响应参数中资源总数量为 30 个。包含 id 等于 37918 的资源是重复的。总计为 2 个。如图1
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 | { "code": 10000, "message": "获取 CMC 用户列表成功", "data": { "items": [ { "user_birthday": "1990-01-01", "login_name": "xianwanzhou", "add_time": "2020-03-31 09:41:07", "user_email": "", "group_id": "643d80843ae23bcfa95b75bae30a7656", "id": "37918", "update_time": "2020-03-31 09:43:16", "is_open": "1", "user_nick": "鲜万州", "user_mobile": "15208396209", "user_token": "6a1de11e594d61790963eaaf1d9bee8d", "user_chat_id": "f98e3b834f577cc3d83d74323cd3094d", "user_type": "2", "user_sex": "2" }, { "user_birthday": "1990-01-01", "login_name": "xianwanzhou", "add_time": "2020-03-31 09:41:07", "user_email": "", "group_id": "643d80843ae23bcfa95b75bae30a7656", "id": "37918", "update_time": "2020-03-31 09:43:16", "is_open": "1", "user_nick": "鲜万州", "user_mobile": "15208396209", "user_token": "6a1de11e594d61790963eaaf1d9bee8d", "user_chat_id": "f98e3b834f577cc3d83d74323cd3094d", "user_type": "2", "user_sex": "2" } ], "_links": { "self": { } }, "_meta": { "totalCount": 30, "pageCount": 1, "currentPage": 1, "perPage": 30 } } } |
2、查看模型类,/common/models/redis/cmc_console/User.php
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 | <?php namespace common\models\redis\cmc_console; use Yii; use common\components\redis\ActiveRecord; /** * This is the model class for table "{{%user}}". * * @property string $id * @property string $group_id * @property string $login_name * @property string $user_token * @property string $user_nick * @property string $user_pic * @property string $user_mobile * @property string $user_email * @property string $user_sex * @property string $user_birthday * @property string $user_type * @property string $user_chat_id * @property string $is_open * @property string $add_time * @property string $update_time */ class User extends ActiveRecord { /** * @return array the list of attributes for this record */ public function attributes() { return [ 'id' , 'group_id' , 'login_name' , 'user_token' , 'user_nick' , 'user_pic' , 'user_mobile' , 'user_email' , 'user_sex' , 'user_birthday' , 'user_type' , 'user_chat_id' , 'is_open' , 'add_time' , 'update_time' ]; } /** * @inheritdoc */ public function rules() { return [ [[ 'id' , 'group_id' , 'login_name' , 'user_token' , 'user_nick' , 'user_pic' , 'user_mobile' , 'user_email' , 'user_sex' , 'user_birthday' , 'user_type' , 'user_chat_id' , 'is_open' , 'add_time' , 'update_time' ], 'safe' ], ]; } /** * @inheritdoc */ public function attributeLabels() { return [ 'id' => Yii::t( 'model/redis/cmc-console/user' , 'ID' ), 'group_id' => Yii::t( 'model/redis/cmc-console/user' , 'Group ID' ), 'login_name' => Yii::t( 'model/redis/cmc-console/user' , 'Login Name' ), 'user_token' => Yii::t( 'model/redis/cmc-console/user' , 'User Token' ), 'user_nick' => Yii::t( 'model/redis/cmc-console/user' , 'User Nick' ), 'user_pic' => Yii::t( 'model/redis/cmc-console/user' , 'User Pic' ), 'user_mobile' => Yii::t( 'model/redis/cmc-console/user' , 'User Mobile' ), 'user_email' => Yii::t( 'model/redis/cmc-console/user' , 'User Email' ), 'user_sex' => Yii::t( 'model/redis/cmc-console/user' , 'User Sex' ), 'user_type' => Yii::t( 'model/redis/cmc-console/user' , 'User Type' ), 'user_birthday' => Yii::t( 'model/redis/cmc-console/user' , 'User Birthday' ), 'user_chat_id' => Yii::t( 'model/redis/cmc-console/user' , 'User Chat Id' ), 'is_open' => Yii::t( 'model/redis/cmc-console/user' , 'Is Open' ), 'add_time' => Yii::t( 'model/redis/cmc-console/user' , 'Add Time' ), 'update_time' => Yii::t( 'model/redis/cmc-console/user' , 'Update Time' ), ]; } } |
3、查看 /common/components/redis/ActiveRecord.php ,定义了前缀,适用于所有 AR 键。
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 | <?php /** * Created by PhpStorm. * User: Administrator * Date: 2018/02/05 * Time: 9:47 */ namespace common\components\redis; use Yii; class ActiveRecord extends \yii\redis\ActiveRecord { /** * Declares prefix of the key that represents the keys that store this records in redis. * By default this method returns the class name as the table name by calling [[Inflector::camel2id()]]. * For example, 'Customer' becomes 'customer', and 'OrderItem' becomes * 'order_item'. You may override this method if you want different key naming. * @return string the prefix to apply to all AR keys */ public static function keyPrefix() { return Yii:: $app ->params[ 'redisActiveRecord' ][ 'keyPrefix' ] . parent::keyPrefix(); } } |
4、打开 RedisDesktopManager,查看 用户 总数,确定为 29 个,如图2
5、打开 RedisDesktopManager,确定主键值为 37918 的记录为 1 个,并未重复。如图3
6、查看 pa:ar:user 的值,发现行 29、30 的值皆为 37918。由此确定,是此键的值导致了 id 等于 37918 的资源数等于 2 个。如图4
7、删除 pa:ar:user 的第 30 行,如图5
8、再次请求接口,响应参数中资源总数量为 29 个。包含 id 等于 37918 的资源未重复。仅剩下 1 个。
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 | { "code": 10000, "message": "获取 CMC 用户列表成功", "data": { "items": [ { "user_birthday": "1990-01-01", "login_name": "xianwanzhou", "add_time": "2020-03-31 09:41:07", "user_email": "", "group_id": "643d80843ae23bcfa95b75bae30a7656", "id": "37918", "update_time": "2020-03-31 09:43:16", "is_open": "1", "user_nick": "鲜万州", "user_mobile": "15208396209", "user_token": "6a1de11e594d61790963eaaf1d9bee8d", "user_chat_id": "f98e3b834f577cc3d83d74323cd3094d", "user_type": "2", "user_sex": "2" } ], "_links": { "self": { } }, "_meta": { "totalCount": 29, "pageCount": 1, "currentPage": 1, "perPage": 29 } } } |
9、准备在本地环境复现一下 ID 主键重复的情况。请求接口,响应参数中资源总数量为 13 个。ID 主键未重复。如图6
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 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 | { "code": 10000, "message": "获取 CMC 用户列表成功", "data": { "items": [ { "login_name": "13281105967", "update_time": "2019-12-11 14:28:26", "group_id": "015ce30b116ce86058fa6ab4fea4ac63", "user_nick": "13281105967", "user_sex": "2", "user_mobile": "13281105967", "user_type": "1", "is_open": "1", "user_chat_id": "ede7616c5e4232896453202cd0c3f7ec", "user_pic": "https://pgcupload.flydev.chinamcloud.cn/uploads/cmc_user_avatar/20190219/1550570817-4LLQJJ.png", "id": "3", "add_time": "2018-04-26 10:05:28", "user_birthday": "1990-01-01", "user_email": "13281105967@chinamcloud.com", "user_token": "fb46626f0e71e423ca8ab4c750620a85" }, { "login_name": "test10", "update_time": "2019-12-11 15:00:38", "group_id": "015ce30b116ce86058fa6ab4fea4ac63", "user_nick": "test10", "user_sex": "2", "user_mobile": "13980074657", "user_type": "2", "is_open": "1", "user_chat_id": "ce524778954bd10d5a0ff65dadc0354b", "id": "4", "add_time": "2019-12-11 14:55:07", "user_birthday": "1990-01-01", "user_email": "", "user_token": "bc8daab7ba8620c132cdf4e5de1d4758" }, { "login_name": "jmj12130003", "update_time": "2019-12-13 14:41:55", "group_id": "015ce30b116ce86058fa6ab4fea4ac63", "user_nick": "jmj1213", "user_sex": "2", "user_mobile": "18412130003", "user_type": "2", "is_open": "1", "user_chat_id": "84e1bb32ffa895e56d950bbfa24fefc7", "id": "49", "add_time": "2019-12-13 14:41:55", "user_birthday": "1990-01-01", "user_email": "", "user_token": "ba0ca65da7c3048fd9d449bb0d21a39b" }, { "login_name": "test11", "update_time": "2019-12-17 11:14:53", "group_id": "015ce30b116ce86058fa6ab4fea4ac63", "user_nick": "test11", "user_sex": "2", "user_mobile": "13980074650", "user_type": "2", "is_open": "1", "user_chat_id": "bc1650a6834fc490de65a4527dfc8ae5", "id": "64", "add_time": "2019-12-17 11:14:53", "user_birthday": "1990-01-01", "user_email": "", "user_token": "04532bb62deb99bf229698c9e1a81da1" }, { "login_name": "test12", "update_time": "2019-12-17 11:15:14", "group_id": "015ce30b116ce86058fa6ab4fea4ac63", "user_nick": "test12", "user_sex": "2", "user_mobile": "13980074651", "user_type": "2", "is_open": "1", "user_chat_id": "fa154673421e5a3731991498397d712d", "id": "65", "add_time": "2019-12-17 11:15:14", "user_birthday": "1990-01-01", "user_email": "", "user_token": "bcdc9e3781180b7bc18bc0e38777a467" }, { "login_name": "test13", "update_time": "2019-12-17 11:15:35", "group_id": "015ce30b116ce86058fa6ab4fea4ac63", "user_nick": "test13", "user_sex": "2", "user_mobile": "13980074652", "user_type": "2", "is_open": "1", "user_chat_id": "35ce7075c1c57bb6c66d0f67a7840383", "id": "66", "add_time": "2019-12-17 11:15:35", "user_birthday": "1990-01-01", "user_email": "", "user_token": "633cae769e274cabd4441acb7be1c5a1" }, { "login_name": "test14", "update_time": "2019-12-17 11:15:55", "group_id": "015ce30b116ce86058fa6ab4fea4ac63", "user_nick": "test14", "user_sex": "2", "user_mobile": "13980074654", "user_type": "2", "is_open": "1", "user_chat_id": "3190c55b297b51b38463da4eae6bbd95", "id": "67", "add_time": "2019-12-17 11:15:55", "user_birthday": "1990-01-01", "user_email": "", "user_token": "84c5871da19f7a489234af73c491f74f" }, { "login_name": "test15", "update_time": "2019-12-17 11:16:16", "group_id": "015ce30b116ce86058fa6ab4fea4ac63", "user_nick": "test15", "user_sex": "2", "user_mobile": "13980074655", "user_type": "2", "is_open": "1", "user_chat_id": "fd072f598ec1a978f5fb968dfa386f0d", "id": "68", "add_time": "2019-12-17 11:16:16", "user_birthday": "1990-01-01", "user_email": "", "user_token": "5bb37b4045e9ad6eb1cc398d3170d33e" }, { "login_name": "test16", "update_time": "2019-12-17 11:16:36", "group_id": "015ce30b116ce86058fa6ab4fea4ac63", "user_nick": "test16", "user_sex": "2", "user_mobile": "13980074656", "user_type": "2", "is_open": "1", "user_chat_id": "88178648656ae259bae213ee00ccb4c7", "id": "69", "add_time": "2019-12-17 11:16:36", "user_birthday": "1990-01-01", "user_email": "", "user_token": "2926796eb8e990aa8bd726e121cb91e8" }, { "login_name": "test17", "update_time": "2019-12-17 11:17:07", "group_id": "015ce30b116ce86058fa6ab4fea4ac63", "user_nick": "test17", "user_sex": "2", "user_mobile": "13980074653", "user_type": "2", "is_open": "1", "user_chat_id": "b03d6e7b4db061d52b3d420844a5dde8", "id": "70", "add_time": "2019-12-17 11:17:07", "user_birthday": "1990-01-01", "user_email": "", "user_token": "bf028bc0353bf8ff070bfd62490ae284" }, { "login_name": "test18", "update_time": "2019-12-17 11:17:25", "group_id": "015ce30b116ce86058fa6ab4fea4ac63", "user_nick": "test18", "user_sex": "2", "user_mobile": "13980074658", "user_type": "2", "is_open": "1", "user_chat_id": "c1eddd55f8efcec8da1e0cc6fc1a5624", "id": "71", "add_time": "2019-12-17 11:17:25", "user_birthday": "1990-01-01", "user_email": "", "user_token": "4adbe2313b7e4967ac518e4e0a73f5c9" }, { "login_name": "test19", "update_time": "2019-12-17 11:17:41", "group_id": "015ce30b116ce86058fa6ab4fea4ac63", "user_nick": "test19", "user_sex": "2", "user_mobile": "13980074659", "user_type": "2", "is_open": "1", "user_chat_id": "5bf1e9f7e4bf9aa2d8a633eddf628710", "id": "72", "add_time": "2019-12-17 11:17:41", "user_birthday": "1990-01-01", "user_email": "", "user_token": "dd7ab3f1fb3e8651b35ba070d10594c7" }, { "login_name": "15708495493", "update_time": "2020-03-02 14:31:36", "group_id": "015ce30b116ce86058fa6ab4fea4ac63", "user_nick": "clover", "user_sex": "2", "user_mobile": "15708495493", "user_type": "2", "is_open": "1", "user_chat_id": "251f91338781d5354e08c1351ca3342d", "id": "166", "add_time": "2020-03-02 14:31:36", "user_birthday": "1990-01-01", "user_email": "", "user_token": "ff94d3f7b1dafada193ae7d6a82fa628" } ], "_links": { "self": { } }, "_meta": { "totalCount": 13, "pageCount": 1, "currentPage": 1, "perPage": 13 } } } |
10、在程序执行过程中,插入数据至 Redis 中时,会判断 ID 是否存在,存在则更新,不存在则插入。初步怀疑是在并发请求的情况下。皆执行了插入的操作。
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 | $redisCmcConsoleUser = new RedisCmcConsoleUser(); $redisCmcConsoleUserResult = $redisCmcConsoleUser ->initSync([ 'group_id' => $groupInfo [ 'group_id' ]], $loginId , $loginTid ); if ( $redisCmcConsoleUserResult === false) { throw new NotFoundHttpException(Yii::t( 'error' , Yii::t( 'error' , Yii::t( 'error' , '201041' ), [ 'id' => $loginId ])), 201041); } $redisCmcConsoleUserItem = $redisCmcConsoleUser ::find()->where([ 'id' => $userInfo [ 'id' ], 'group_id' => Yii:: $app ->params[ 'groupId' ]])->one(); /* 如果资源不存在,则插入;否则更新 */ $redisCmcConsoleUserAttributes = [ 'id' => $userInfo [ 'id' ], 'group_id' => $groupInfo [ 'group_id' ], 'login_name' => $userInfo [ 'login_name' ], 'user_token' => $userInfo [ 'user_token' ], 'user_nick' => $userInfo [ 'user_nick' ], 'user_pic' => $userInfo [ 'user_pic' ], 'user_mobile' => $userInfo [ 'user_mobile' ] ? $userInfo [ 'user_mobile' ] : '' , 'user_email' => $userInfo [ 'user_email' ] ? $userInfo [ 'user_email' ] : '' , 'user_sex' => $userInfo [ 'user_sex' ], 'user_type' => $userInfo [ 'user_type' ], 'user_birthday' => $userInfo [ 'user_birthday' ], 'user_chat_id' => $userInfo [ 'user_chat_id' ] ? $userInfo [ 'user_chat_id' ] : '' , 'is_open' => $userInfo [ 'is_open' ], 'add_time' => $userInfo [ 'add_time' ], 'update_time' => $userInfo [ 'update_time' ], ]; if (!isset( $redisCmcConsoleUserItem )) { $redisCmcConsoleUser ->attributes = $redisCmcConsoleUserAttributes ; $redisCmcConsoleUser ->insert(); // 设置用户身份为已认证 Yii:: $app ->user->setIdentity( $redisCmcConsoleUser ); } else { $redisCmcConsoleUserItem ->attributes = $redisCmcConsoleUserAttributes ; $redisCmcConsoleUserItem ->save(); // 设置用户身份为已认证 Yii:: $app ->user->setIdentity( $redisCmcConsoleUserItem ); } |
11、在程序执行过程中,插入数据至 Redis 中时,不论 ID 是否存在,皆插入。请求接口,响应参数中资源总数量为 14 个。包含 id 等于 3 的资源是重复的。总计为 2 个。如图7
1 2 3 4 5 6 7 8 9 10 11 | if (!isset( $redisCmcConsoleUserItem )) { $redisCmcConsoleUser ->attributes = $redisCmcConsoleUserAttributes ; $redisCmcConsoleUser ->insert(); // 设置用户身份为已认证 Yii:: $app ->user->setIdentity( $redisCmcConsoleUser ); } else { $redisCmcConsoleUserItem ->attributes = $redisCmcConsoleUserAttributes ; $redisCmcConsoleUserItem ->insert(); // 设置用户身份为已认证 Yii:: $app ->user->setIdentity( $redisCmcConsoleUserItem ); } |
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 | { "code": 10000, "message": "获取 CMC 用户列表成功", "data": { "items": [ { "login_name": "13281105967", "update_time": "2019-12-11 14:28:26", "group_id": "015ce30b116ce86058fa6ab4fea4ac63", "user_nick": "13281105967", "user_sex": "2", "user_mobile": "13281105967", "user_type": "1", "is_open": "1", "user_chat_id": "ede7616c5e4232896453202cd0c3f7ec", "user_pic": "https://pgcupload.flydev.chinamcloud.cn/uploads/cmc_user_avatar/20190219/1550570817-4LLQJJ.png", "id": "3", "add_time": "2018-04-26 10:05:28", "user_birthday": "1990-01-01", "user_email": "13281105967@chinamcloud.com", "user_token": "fb46626f0e71e423ca8ab4c750620a85" }, { "login_name": "13281105967", "update_time": "2019-12-11 14:28:26", "group_id": "015ce30b116ce86058fa6ab4fea4ac63", "user_nick": "13281105967", "user_sex": "2", "user_mobile": "13281105967", "user_type": "1", "is_open": "1", "user_chat_id": "ede7616c5e4232896453202cd0c3f7ec", "user_pic": "https://pgcupload.flydev.chinamcloud.cn/uploads/cmc_user_avatar/20190219/1550570817-4LLQJJ.png", "id": "3", "add_time": "2018-04-26 10:05:28", "user_birthday": "1990-01-01", "user_email": "13281105967@chinamcloud.com", "user_token": "fb46626f0e71e423ca8ab4c750620a85" } ], "_links": { "self": { } }, "_meta": { "totalCount": 14, "pageCount": 1, "currentPage": 1, "perPage": 14 } } } |
12、由此可以确认,Redis ActiveRecord 不能够确保主键 ID 的唯一性的。现阶段有 2 种方案,第 1 种方案是插入记录时基于 Redis 实现唯一性的锁定。第 2 种方案是查询时去重。决定采用第 2 种方案。由于 redis 不支持 SQL 查询,因此查询 API 仅限于以下方法: where(),limit(),offset(),orderBy() 和 indexBy()。 (orderBy() 尚未实现:#1305)。基于 indexBy()。请求接口,响应参数中资源总数量为 13 个。ID 主键未重复。但是,totalCount 的值为 14。统计错误。如图8
1 2 3 4 5 6 7 | // 查询当前用户所属租户 ID 下的 CMC 的用户模型(Redis) $query = $modelClass ::find() ->where([ 'group_id' => $identity ->group_id, 'is_open' => $modelClass ::STATUS_ENABLED ]) ->indexBy( 'id' ); |
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 | { "code": 10000, "message": "获取 CMC 用户列表成功", "data": { "items": [ { "login_name": "13281105967", "update_time": "2019-12-11 14:28:26", "group_id": "015ce30b116ce86058fa6ab4fea4ac63", "user_nick": "13281105967", "user_sex": "2", "user_mobile": "13281105967", "user_type": "1", "is_open": "1", "user_chat_id": "ede7616c5e4232896453202cd0c3f7ec", "user_pic": "https://pgcupload.flydev.chinamcloud.cn/uploads/cmc_user_avatar/20190219/1550570817-4LLQJJ.png", "id": "3", "add_time": "2018-04-26 10:05:28", "user_birthday": "1990-01-01", "user_email": "13281105967@chinamcloud.com", "user_token": "fb46626f0e71e423ca8ab4c750620a85" } ], "_links": { "self": { } }, "_meta": { "totalCount": 14, "pageCount": 1, "currentPage": 1, "perPage": 14 } } } |
13、因此,尝试采用第 1 种方案。不过由于 此处程序实现 是所有请求皆会执行到的流程,担心锁定实现降低程序性能,最终决定,放弃插入,仅做更新。如果 Redis 中用户不存在,则插入;否则更新。调整为:如果 Redis 中用户不存在,则响应 404;否则更新。
1 2 3 4 5 6 7 8 9 10 | /* 如果资源不存在,则响应 404 */ if (!isset( $redisCmcConsoleUserItem )) { throw new NotFoundHttpException(Yii::t( 'error' , Yii::t( 'error' , Yii::t( 'error' , '201011' ), [ 'user_nick' => $userInfo [ 'user_nick' ]])), 201011); } /* 更新 */ $redisCmcConsoleUserItem ->attributes = $redisCmcConsoleUserAttributes ; $redisCmcConsoleUserItem ->save(); // 设置用户身份为已认证 Yii:: $app ->user->setIdentity( $redisCmcConsoleUserItem ); |
14、如果 Redis 中用户不存在,则响应 404。如图9
15、但是,此修复仅防止以后出现用户重复的问题,之前已经重复的用户,仍然是重复的。如果要去重,需要手动操作 Redis。且此修复会衍生出一个新的影响体验的问题,即在框架处新添加了一个用户 B 后,如果用户 B 在 Redis 中不存在,则用户 B 无法使用策划指挥。需要等待用户 B 同步至 Redis 后,才可使用。
近期评论