1. 问题
需求:课程点赞功能,可以取消点赞。
可取消的点赞功能是需要记录点赞状态的,因此需要持久化 课程 与 用户id (后面分别用 coureseId 和 uid 代替)。
要应对用户多次的点击点赞(或取消点赞)按钮,所以可以用 redis 来协助处理,再通过定时任务 crontab 持久化到 mysql 。
需要考虑到有一定的并发问题。
2. 处理过程
- 思路1:使用 redis 的
bitmap。
通俗理解:bitmap是位图的概念,可以想象成一个铁轨的俯视图,中间有若干根杆隔开,杆与杆包围的空间就是一个小格子,随机取一个格子到起点格子的格子数量就是当前格子的序号,对应uid,格子里面只能放 0 或 1 对应 点赞 与 没有点赞(或取消点赞),而不同的铁轨对应不同的课程。
那么这时再审视一次,大量的bitmap需要大量的空间,而且课程的点赞数与uid总数严重不匹配,因为点赞者比不点赞者多太多,再有是大量的不需要即时访问的点赞缓存在redis中其实是缓存在服务器的内存中的,造成内存内的使用性价比不高。
- 思路2:使用 redis 的
hash table。
hash表可以将课程点赞以表的形式存起来,key为coursesId,field为uid,value为0或1,代表当前是否已点赞。无论用户在某个时间段点击了多少次,都不会影响效率。
<?php// 点赞类class Like extends Component{const OFF = 0;const ON = 1;public static $valueMap = ['on' => self::OFF,'off' => self::ON];/** @var Connection redis对象 */public $redis;// 点赞key名称public $key;// 点赞计数key名称public $countKey;public function __construct($key, $countKey, $config = []){$this->key = $key;$this->countKey = $countKey;$this->init();parent::__construct($config);}public function init(){$this->redis = Yii::$app->redis;}// 点赞public function setRedisLike(string $field, $value){if (!in_array($value, self::$valueMap)) {return ['status' => 9001, 'message' => '参数错误', 'data' => []];}// 点赞总数增减$amount = ($value == self::ON) ? 1 : -1;// 开启redis事务$this->redis->multi();// 记录总数$this->redis->incrby($this->countKey, $amount);// 记录$ret = $this->redis->hset($this->key, $field, $value);$this->redis->exec();return ['status' => 0, 'message' => $ret, 'data' => []];}// 获取是否点赞public function getRedisLike(string $field){$fieldValue = $this->redis->hget($this->key, $field);$count = $this->redis->get($this->countKey);return ['status' => 0, 'message' => '', 'data' => ['field' => $fieldValue, 'count' => $count]];}}
<?php// 调用点赞class FirmCollegeLikeV4Services{// 点赞key前缀private static $likeTmpRecordKeyPrefix = 'firmCollegeLikeRecordV4';// 点赞计数key名称private static $likeTmpRecordCountKeyPrefix = 'firmCollegeLikeRecordCountV4';public function __construct(){}// 点赞/取消点赞public function like(int $coursesId, int $uid, int $value){$key = self::$likeTmpRecordKeyPrefix . ':' . $coursesId;$countKey = self::$likeTmpRecordCountKeyPrefix . ':' . $coursesId;$likeCls = new Like($key, $countKey);if (!in_array($value, Like::$valueMap)) {return ['status' => 9001, 'message' => '参数错误', 'data' => []];}return $likeCls->setRedisLike($uid, $value);}// 是否已点赞public function isLike(int $coursesId, int $uid){$key = self::$likeTmpRecordKeyPrefix . ':' . $coursesId;$countKey = self::$likeTmpRecordCountKeyPrefix . ':' . $coursesId;$likeCls = new Like($key, $countKey);return $likeCls->getRedisLike($uid);}// 持久化public function PersistLikeRedis2Mysql(){/** @var Connection $redis */$redis = Yii::$app->redis;$keys = $redis->keys(self::$likeTmpRecordKeyPrefix . "*");/** @var array $batchInsertUserLikeDatum 批量新增到数据库点赞变量 */$batchInsertUserLikeDatum = [];/** @var array $deleteRedisKeyCoursesId 需要删除redis key的课程ids */$deleteRedisKeyCoursesId = [];foreach ($keys as $key) {// 课程Id$coursesId = substr($key, strpos($key, ':') + 1);// 获取课程$coursesModel = TblFirmCollegeCoursesDao::getRawModelById($coursesId);if ($coursesModel) {$coursesModel->is_delete && $deleteRedisKeyCoursesId[] = $coursesId;// 更新点赞数量$coursesModel->like_times = $this->getCoursesLikeCount($coursesId);$coursesModel->updated_at = time();$coursesModel->save();}// 获取该课程点赞$fields = $redis->hkeys($key);if (!$fields) {continue;}// 处理点赞foreach ($fields as $field) {// 检查是否存在$isExist = $redis->hexists($key, $field);if (!$isExist) continue;// 取值$value = $redis->hget($key, $field);// 删除$redis->hdel($key, $field);$dbLikeDTO = TblFirmCollegeCoursesUserLikeDao::getByCoursesIdUid($coursesId, $field, 'id, is_like');if (isset($dbLikeDTO['id'])) {($dbLikeDTO['is_like'] != $value) && TblFirmCollegeCoursesUserLikeDao::updateLikeById($dbLikeDTO['id'], $value);} else {$batchInsertUserLikeDatum[] = ['courses_id' => $coursesId,'user_id' => $field,'is_like' => $value,'created_at' => time(),'updated_at' => time(),];}}}// 批量新增点赞记录到数据库if ($batchInsertUserLikeDatum) {TblFirmCollegeCoursesUserLikeDao::batchInsert($batchInsertUserLikeDatum);}// 课程已删除 && 删除redis keyif ($deleteRedisKeyCoursesId) {foreach ($deleteRedisKeyCoursesId as $coursesId) {$recordKey = self::$likeTmpRecordKeyPrefix . ':' . $coursesId;$redis->del($recordKey);$countKey = self::$likeTmpRecordCountKeyPrefix . ':' . $coursesId;$redis->del($countKey);}}}}
