1、请求接口,响应参数中资源总数量为 30 个。包含 id 等于 37918 的资源是重复的。总计为 2 个。如图1
{ "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", "user_pic": "http://uploads.cmc.lzgbdst.com/uploads/cmc_user_avatar/default_header.png", "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", "user_pic": "http://uploads.cmc.lzgbdst.com/uploads/cmc_user_avatar/default_header.png", "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": { "href": "http://pcsapi.cmc.lzgbdst.com/v1/cmc-users?login_id=7309cba1c93fc0a80663007612b784b8&login_tid=25f49c543b8ec31c6d905def0ad99913&per-page=30&page=1" } }, "_meta": { "totalCount": 30, "pageCount": 1, "currentPage": 1, "perPage": 30 } } }
2、查看模型类,/common/models/redis/cmc_console/User.php
<?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 键。
<?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 个。
{ "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", "user_pic": "http://uploads.cmc.lzgbdst.com/uploads/cmc_user_avatar/default_header.png", "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": { "href": "http://pcsapi.cmc.lzgbdst.com/v1/cmc-users?login_id=7309cba1c93fc0a80663007612b784b8&login_tid=25f49c543b8ec31c6d905def0ad99913&per-page=29&page=1" } }, "_meta": { "totalCount": 29, "pageCount": 1, "currentPage": 1, "perPage": 29 } } }
9、准备在本地环境复现一下 ID 主键重复的情况。请求接口,响应参数中资源总数量为 13 个。ID 主键未重复。如图6
{ "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", "user_pic": "https://pgcupload.flydev.chinamcloud.cn/uploads/cmc_user_avatar/default_header.png", "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", "user_pic": "https://pgcupload.flydev.chinamcloud.cn/uploads/cmc_user_avatar/default_header.png", "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", "user_pic": "https://pgcupload.flydev.chinamcloud.cn/uploads/cmc_user_avatar/default_header.png", "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", "user_pic": "https://pgcupload.flydev.chinamcloud.cn/uploads/cmc_user_avatar/default_header.png", "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", "user_pic": "https://pgcupload.flydev.chinamcloud.cn/uploads/cmc_user_avatar/default_header.png", "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", "user_pic": "https://pgcupload.flydev.chinamcloud.cn/uploads/cmc_user_avatar/default_header.png", "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", "user_pic": "https://pgcupload.flydev.chinamcloud.cn/uploads/cmc_user_avatar/default_header.png", "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", "user_pic": "https://pgcupload.flydev.chinamcloud.cn/uploads/cmc_user_avatar/default_header.png", "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", "user_pic": "https://pgcupload.flydev.chinamcloud.cn/uploads/cmc_user_avatar/default_header.png", "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", "user_pic": "https://pgcupload.flydev.chinamcloud.cn/uploads/cmc_user_avatar/default_header.png", "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", "user_pic": "https://pgcupload.flydev.chinamcloud.cn/uploads/cmc_user_avatar/default_header.png", "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", "user_pic": "https://pgcupload.flydev.chinamcloud.cn/uploads/cmc_user_avatar/default_header.png", "id": "166", "add_time": "2020-03-02 14:31:36", "user_birthday": "1990-01-01", "user_email": "", "user_token": "ff94d3f7b1dafada193ae7d6a82fa628" } ], "_links": { "self": { "href": "http://api.pcs-api.localhost/v1/cmc-users?login_id=2e368664c41b8bf511bcc9c65d86dbc3&login_tid=efda7e64b7747f8c4360b29aa9c18f77&per-page=13&page=1" } }, "_meta": { "totalCount": 13, "pageCount": 1, "currentPage": 1, "perPage": 13 } } }
10、在程序执行过程中,插入数据至 Redis 中时,会判断 ID 是否存在,存在则更新,不存在则插入。初步怀疑是在并发请求的情况下。皆执行了插入的操作。
$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
if (!isset($redisCmcConsoleUserItem)) { $redisCmcConsoleUser->attributes = $redisCmcConsoleUserAttributes; $redisCmcConsoleUser->insert(); // 设置用户身份为已认证 Yii::$app->user->setIdentity($redisCmcConsoleUser); } else { $redisCmcConsoleUserItem->attributes = $redisCmcConsoleUserAttributes; $redisCmcConsoleUserItem->insert(); // 设置用户身份为已认证 Yii::$app->user->setIdentity($redisCmcConsoleUserItem); }
{ "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": { "href": "http://api.pcs-api.localhost/v1/cmc-users?login_id=2e368664c41b8bf511bcc9c65d86dbc3&login_tid=efda7e64b7747f8c4360b29aa9c18f77&per-page=14&page=1" } }, "_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
// 查询当前用户所属租户 ID 下的 CMC 的用户模型(Redis) $query = $modelClass::find() ->where([ 'group_id' => $identity->group_id, 'is_open' => $modelClass::STATUS_ENABLED ]) ->indexBy('id');
{ "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": { "href": "http://api.pcs-api.localhost/v1/cmc-users?login_id=2e368664c41b8bf511bcc9c65d86dbc3&login_tid=efda7e64b7747f8c4360b29aa9c18f77&per-page=14&page=1" } }, "_meta": { "totalCount": 14, "pageCount": 1, "currentPage": 1, "perPage": 14 } } }
13、因此,尝试采用第 1 种方案。不过由于 此处程序实现 是所有请求皆会执行到的流程,担心锁定实现降低程序性能,最终决定,放弃插入,仅做更新。如果 Redis 中用户不存在,则插入;否则更新。调整为:如果 Redis 中用户不存在,则响应 404;否则更新。
/* 如果资源不存在,则响应 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 后,才可使用。
原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/tech/pnotes/250529.html